Skip to main content
Back to writing

Eamon Boyle Writing

Engineering notes7 min read

What a reliable React migration actually looks like

Notes from an incremental migration: where shared components helped, where ceremony got in the way, and how to modernize a business-critical frontend without pretending greenfield rules apply.

ReactTypeScript.NETArchitecture

There is a genre of migration writeup that quietly assumes you are allowed to stop the world.

You get a clean team boundary, a fresh design system, one obvious API contract, and a product organization that will politely postpone all interruptions until the rewrite is complete. It is neat, cinematic, and almost never the job in front of you.

What I actually ran into was messier and more familiar: internal applications with years of jQuery and server-rendered views, uneven patterns between screens, and a steady stream of business work that could not be paused while the frontend was modernized.

The migration only worked because we treated it as operational engineering, not as a purity project.

The first mistake is thinking the framework is the migration

React and TypeScript were not the plan. They were tools inside the plan.

The real migration problem was this:

  • how to introduce modern UI patterns without a big-bang release
  • how to avoid rebuilding the same buttons, forms, and validation flows five different ways
  • how to keep the existing .NET Core API as the source of truth while new routes appeared beside legacy ones

Once you define the problem that way, the sequence becomes clearer. You do not start by rewriting every screen. You start by reducing entropy.

Shared components paid for themselves faster than expected

The best decision was building a shared component library early.

That choice can feel expensive when people are waiting for visible feature work. But in a portfolio of internal apps, consistency compounds. Once forms, status patterns, layout primitives, and interaction states were reusable, each migrated screen became less of a one-off negotiation.

The practical payoff was not theoretical elegance. It was this:

  1. engineers stopped debating spacing and interaction details from scratch
  2. product got more predictable UI behavior across apps
  3. new React surfaces stopped looking like isolated experiments

The library also created a forcing function. If a pattern was painful to generalize, that usually meant the underlying UI logic was still too tangled.

Incremental migration wins because it respects the host system

The important architecture move was not flashy. New React routes sat alongside the legacy application rather than trying to erase it in one move.

That let us ship screen by screen, measure where the new stack improved delivery, and leave the business with a working product throughout. In practice, that meant accepting some temporary duplication and some ugly seams. Those are usually fine tradeoffs. The dangerous version of "temporary" is the kind that never gets paid down; the healthy version is a seam with a clear ownership boundary and a shrinking footprint.

I prefer that over migration plans that stay clean on paper and chaotic in production.

If users still get their work done while the architecture is changing, the migration is alive. If users become collateral damage for the rewrite, the rewrite has already failed.

TypeScript helped most at the boundaries

Type safety mattered less for "hello world" UI work and much more for the places where frontend assumptions met backend reality.

Generated API clients and typed contracts made it much easier to move quickly without the low-grade anxiety of wondering which endpoint shape had silently drifted. That is where TypeScript earns trust in business systems: not in making simple code feel more formal, but in shrinking the blast radius of change.

It also changed code review quality. Conversations moved away from "I think this payload contains that property" and towards more useful questions about behavior, fallback states, and ownership.

There are tradeoffs worth making on purpose

Not every good migration decision looks efficient in the moment.

We deliberately slowed some feature work to invest in the shared library and the migration path. That is uncomfortable, because the cost is immediate and visible while the return arrives gradually. But once multiple applications began using the same primitives, the velocity gain was obvious.

The more interesting lesson was that deliberate tradeoffs are easier to defend than accidental complexity. If you can explain why duplication exists, why a route remains hybrid, or why one part of the app moved first, the team can operate around it. Undefined mess is harder to maintain than known imperfection.

The migration changed delivery, not just code

The outcome I care about most is not that the frontend became "modern." It is that the team could ship faster with more confidence after the change.

That is the standard I would use for any migration now:

  • did the work reduce future friction?
  • did it improve the quality of change, not just the style of code?
  • did it leave the product in a continuously shippable state?

When the answer is yes, the migration was worth doing.

And when it is done well, it does not look like a heroic rewrite. It looks almost boring from the outside: fewer surprises, cleaner iterations, and software that becomes easier to move without becoming fragile.