Skip to main content
Back to writing
Engineering notes3 min read

Updated 11 October 2026

What a reliable React migration actually looks like

An incremental migration at Watch Portfolio Management: shared components, generated API contracts and React routes alongside a working legacy application.

ReactTypeScript.NETArchitecture

At Watch Portfolio Management, I led a migration from jQuery and server-rendered interfaces to React and TypeScript. The existing applications still had to support ongoing business work while the frontend changed.

The useful decisions were about where to introduce the new stack, what to share between applications, and which existing contracts to retain. This account focuses on those choices; internal measurements and proprietary implementation details remain private.

Choose a boundary that can be delivered

New React routes sat alongside legacy pages. That let the migration proceed in bounded changes instead of making every screen depend on one replacement release.

The C# .NET Core API remained the source of truth. The frontend could change without requiring a simultaneous rewrite of the backend.

The shape was straightforward:

Legacy routes ──┐
                ├── Existing .NET API
React routes ───┘
      │
      ├── Shared component library
      └── Generated TypeScript API client

This kept the migration attached to the system people already used. It also meant accepting a period where old and new interfaces coexisted.

Share the parts that repeat

The applications had their own UI patterns. Rebuilding a form or modal separately in each application would have carried that duplication into the new stack.

We built a shared component library with design tokens and reusable buttons, forms, modals and layout primitives. That gave the React routes a common foundation and made improvements reusable beyond one screen.

There was an immediate cost: work on shared components took time away from individual features. The reason to accept it was practical reuse across the applications, rather than the idea of a design system by itself.

Make the API boundary explicit

We generated a TypeScript client from OpenAPI so frontend code could work against a defined contract with the existing API.

This helped expose mismatches during development and made assumptions about payloads easier to inspect. It also gave code review a clearer boundary: a frontend change could be considered against the API definition it depended on.

Generated types are useful, but they are not runtime validation. An application still needs to handle missing data, failed requests and behaviour that a type definition cannot establish.

Accept the cost of an incremental approach

Route-by-route migration kept changes manageable, but it did not make the application instantly uniform. There were temporary seams between the old interface and the new one.

I would rather name those seams and keep reducing them than hide the whole migration behind a distant release. The important questions are whether the boundary is understandable, whether the remaining legacy work is visible, and whether users can continue their work.

What changed

The migration introduced a shared React and TypeScript foundation, reusable UI components and a generated API client. The library was adopted beyond the original migration work.

The lesson I carried forward is to treat migration as delivery work. Pick useful boundaries, retain working contracts, and invest in shared components where several applications actually need them.

Read the migration case study for the architecture and tradeoffs.