Rewriting a legacy frontend is usually described as a coding problem. It almost never is. It is a specification problem wearing a coding problem's clothes, and the teams that get this wrong ship a different, undocumented application instead of a modernized version of the one operators already trust.
Here is how a streaming operations portal came off AngularJS and onto React without breaking the people who use it every day.
The portal that would not stay still
Dense, long lived screens. Driven by asynchronous backend jobs. Built originally on AngularJS, then grown for years until two way binding had become unpredictable and features that should have touched one part of the screen reached into several controllers at once.
The API documentation behind it was incomplete. That one fact decided everything that followed, because it meant the only honest description of how the system actually behaved was the UI itself, inferred from watching network traffic rather than read from a spec.
So the rewrite had to reproduce the old portal's behavior exactly, not a cleaned up guess at what that behavior was supposed to be. The old screen was the real specification.
Three tiers, one place to change things
The React replacement was structured in three component tiers:
- Base library supplying generic, unopinionated components.
- Shared middle layer fixing spacing, typography, form controls, table density and empty states in one place.
- Feature components holding only domain logic, nothing about how things look.
The payoff is boring but real: a design change is made once. Before, a spacing fix meant hunting through every controller that happened to touch that part of the UI. After, it is one edit in the middle tier.
Async state needs more than a reducer
Redux-Saga sits between actions and reducers, and on this project it earned its place doing two specific jobs a plain reducer does not do cleanly:
- Sequencing requests that depend on each other.
- Cancelling work still in flight when an operator navigates away from a screen mid load.
Without that second one, a slow response from a screen the user already left lands late and overwrites newer state with stale data.
Every entity in the store was normalized, so the same underlying record could not drift into two different shapes in two different parts of the state tree. That is the classic source of bugs where two views of the same object quietly disagree with each other.
The rule that was actually about trust, not architecture
The single decision that mattered most was not a pattern name. It was a rule about when the UI is allowed to guess.
Live telemetry and device health data got their own state slice, entirely separate from form state. An incoming status update could refresh on screen without ever interrupting someone mid edit on a form elsewhere on the same page. Two different data lifecycles, kept from colliding.
Optimistic updates were allowed for ordinary metadata changes. They were banned outright for hardware commands.
A screen that shows a device as on before the device has confirmed it turned on is not a faster screen. It is a screen that lies, and the cost of that lie is paid in operator trust the first time the device does not actually respond. Confirmed state only, no exceptions, for anything connected to physical hardware.
If you take one thing from this write up, take that line and draw it in your own app.
Building and shipping without a finished backend
The API contract was settled before a line of UI code was written. Mock requests and responses generated from that contract did double duty: they were the data the frontend team built against before the real backend existed, and later they became contract tests that caught any drift once it was live.
Cutover ran route by route rather than as a single flag flip. The legacy AngularJS layer and the new React layer lived side by side under one authenticated session, so operators moved one screen at a time and nothing was ever stood up that the old portal could not fall back to.
What actually changed
No benchmark numbers here, and the outcomes that mattered were not benchmarks anyway:
- Behavior stayed consistent with what operators already expected, right through the migration.
- Race conditions on quickly navigated screens stopped happening.
- Live health data kept refreshing without disrupting whatever someone was in the middle of editing.
- A single design change finally propagated everywhere instead of being re-applied controller by controller.
None of that required new business logic. It required treating the existing screen as ground truth, drawing one hard line around physical hardware state, and shipping the migration in pieces small enough that nothing operators depended on broke along the way.
Full case study: https://innerluxes.dev/case-studies/netherlands-client-updates-streaming-software?utm_source=devto&utm_medium=article
Curious where other people draw the optimistic update line. Do you allow it anywhere near physical state, or is it metadata only for you too?

