Case 05 React · JavaScript · jQuery
React modernization: asynchronous flows you can read top to bottom
I took part in migrating a legacy JavaScript and jQuery frontend to React. Beyond reusable components, a migration is a chance to decide how asynchronous flows will read for the people maintaining the code.
Context
On DPMS, part of the interface relied on legacy JavaScript and jQuery. The team migrated those screens to React, aiming for maintainability, component reuse, extensibility and a clearer rendering model.
The migration was a team effort. I took part in it and in the discussion about how to structure components and flows.
Problem
Replacing jQuery with React does not, on its own, make code easier to maintain. One specific pattern tends to weigh heavily: user actions that trigger several dependent API calls.
Each step can fail and needs feedback, and the final operation should only run when every required step succeeds. If that process is split across several useEffect calls reacting to state changes, the sequence is no longer written down anywhere: it has to be inferred.
My responsibility
I took part in the migration and helped define the React architecture that replaced the legacy interfaces, paying attention to component reuse, separation of responsibilities and the readability of asynchronous flows.
I was not the sole author of the migration; this case records an engineering decision I advocate and apply.
Decision
When a user action starts a process, I prefer to make the orchestration explicit: a function whose name reveals the intent, steps in a readable sequence and error handling where the failure happens.
Depending on the problem, an event handler, an async function, a custom hook or a data-fetching abstraction may be the best fit. Effects remain useful for synchronizing with external systems, such as subscriptions and connections, with cleanup or cancellation when appropriate.
Alternatives and trade-offs
Present-day analysis written for this site. It is not a record of the alternatives evaluated at the time.
- Chained effects can work and sometimes feel more "reactive". The cost shows up when reading: dependencies hide in dependency arrays and state flags, and an effect has no name telling you which business process it runs.
- An explicit function keeps the flow in one place, but needs care with cancellation when the component unmounts and with loading states. Custom hooks help name those responsibilities without scattering them again.
- The point is judgment and readability, not a rule against
useEffect.
Implementation
The criteria I bring to React code reviews and architecture decisions:
Intention-revealing names
The business process appears as a function, for example
approveReport, instead of emerging from a combination of effects.Visible sequence
Dependent calls sit in order in the code; readers do not have to rebuild the chain from state changes.
Failures handled where they happen
Each step can fail with a useful message, and the final operation only runs after the previous ones succeed.
Effects for synchronization
useEffectis reserved for synchronizing with external systems, with cleanup when needed.
Visualization
Chained effects
const [report, setReport] = useState(null);const [metrics, setMetrics] = useState(null);const [pdf, setPdf] = useState(null);const handleApprove = () => saveReport(draft) .then(setReport) .catch(showError);useEffect(() => { if (!report) return; recalculateMetrics(report.id) .then(setMetrics) .catch(showError);}, [report]);useEffect(() => { if (!metrics) return; generatePdf(metrics.reportId) .then(setPdf) .catch(showError);}, [metrics]);useEffect(() => { if (!pdf) return; publish(pdf.reportId).catch(showError);}, [pdf]);Places to read to understand the sequence: 3 effects, 3 intermediate states and the dependency arrays
Named function
async function approveReport(draft: Draft) { const report = await saveReport(draft); const metrics = await recalculateMetrics(report.id); const pdf = await generatePdf(metrics.reportId); await publish(pdf.reportId);}const handleApprove = async () => { try { await approveReport(draft); showSuccess(); } catch (error) { showError(error); }};Places to read to understand the sequence: 1 function, read top to bottom
- Save report Waiting
- Recalculate metrics Waiting
- Generate PDF Waiting
- Publish Waiting
The interaction becomes available once JavaScript loads. The code above already describes both flows.
useEffect is still the right tool for synchronizing with external systems, such as subscriptions and connections. Custom hooks can also give responsibilities a name.Outcome
The migration aimed for reusable components and a clearer rendering model than the jQuery screens offered. The outcome I highlight here is qualitative: an explicit criterion so asynchronous flows can be read, reviewed and changed without hunting for dependencies across the page.
The interactive comparison above uses illustrative code and simulated calls; it does not reproduce the project's code.