Run Angular and React in the same application, in production
Angular custom elements mounted inside a React host. React fragments mounted inside an Angular shell. One page, shared routing, one authentication session. You migrate a screen at a time while the product keeps shipping, and there is never a launch day.
Why the rewrite keeps getting cancelled
A frontend rewrite is normally sold as a cliff. The old application runs until a launch date, the new one takes over afterwards, and in between there is a period where two codebases have to be kept at feature parity by a team that was already fully committed. Every product owner who has lived through one knows how that ends.
The arithmetic is what kills it. Eighteen months of capacity, a feature freeze the business won't accept, and a single irreversible switch that lands on whoever signed it off. So the rewrite slides, and the AngularJS application that went end-of-life years ago is still there, still unhireable for.
Coexistence removes the cliff. Both frameworks run in one page, in production, and the boundary between them moves one screen at a time.
The architecture
There is a host application and there are guests. The host owns the shell: routing, authentication, layout, the global chrome. Guests are self-contained pieces of UI mounted into the host at defined points. Which framework plays which role depends on which one you're moving toward, and the technique works in both directions.
Angular inside React
Angular Elements compiles an Angular component into a standard custom element. Once it's registered, React renders it like any other tag. Props go in as attributes or properties, and anything coming back out is a DOM event that React listens to. The React side doesn't know or care that the element is Angular internally, which is the whole point.
The awkward part is that custom elements take strings for attributes. Anything richer than a string has to be set as a property on the element instance, which in React means a ref and an effect rather than plain JSX. It's a small piece of glue and we wrap it once per element rather than at every call site.
React inside Angular
Going the other way, a React fragment mounts into a container element that an Angular component owns. Angular controls the lifecycle: it creates the container, mounts the React root, passes data down, and unmounts on destroy. The trap is forgetting to unmount, which leaks a React root every time the Angular route changes, and it doesn't show up until someone leaves a tab open for an afternoon.
Module federation
Where the two sides are separately deployed, module federation lets the host load a remote bundle at runtime and share common dependencies rather than duplicating them. It's the right tool when the teams release independently. It's overkill when both halves ship from one repository on one pipeline, and reaching for it there buys you a build configuration nobody enjoys maintaining.
Routing across the boundary
Two frameworks both wanting to own the URL is the most common way this goes wrong. The rule we hold to is that one router owns the address bar and the other is subordinate to it. The host router matches the top-level route and decides which guest to mount; the guest router, if it has one, only handles paths beneath the segment it was given.
Back-button behaviour is where the bugs hide. A guest that pushes history entries the host doesn't know about produces a back button that appears to do nothing, and users read that as the application being broken. This gets tested explicitly on every screen we move.
Styles
Custom elements can use Shadow DOM, and where it fits it gives you genuine isolation rather than a naming convention you hope everyone follows. It doesn't always fit. Global theming gets harder, some component libraries assume they can reach into the document, and form participation inside a shadow root needs deliberate handling.
Where Shadow DOM is wrong, the fallback is scoped class prefixes on both sides plus a hard rule that neither writes global selectors. In practice the trouble almost always comes from the legacy stylesheet, which will have accumulated bare-element rules over the years. Those get audited before the first screen moves, because a global rule on a bare tag will reach into new markup and produce a layout bug that looks unrelated to anything you changed.
State and authentication
The host owns the session. It holds the token, handles refresh, and exposes the current user through a narrow interface the guest reads. The guest does not keep its own copy.
For shared application state, we move data across the boundary as events, or through a small framework-agnostic store both sides can subscribe to. What we avoid is each side maintaining its own duplicate of the same state and reconciling them. Two sources of truth across a framework boundary is the failure mode that makes teams give up on this approach and go back to planning a big-bang rewrite.
The cost, stated honestly
Two frameworks in one page costs you something, and any page claiming otherwise is selling you something. A React runtime is around 45KB gzipped; an Angular runtime is larger. For as long as both are loaded you carry weight you wouldn't otherwise.
What makes it acceptable is that the cost is bounded and temporary. Most screens load one framework only, because routing decides what mounts. Where a screen genuinely needs both, the second loads lazily. And the legacy bundle shrinks as screens move across, so the overhead curve bends down over the life of the migration rather than up. We measure it per screen and report it, because a migration that quietly doubles time-to-interactive isn't a migration anyone should accept.
There's a second cost worth naming: your team is holding two mental models at once for as long as the migration runs. That's real, it's why we sequence by area rather than scattering, and it's an argument for finishing rather than living in the middle state indefinitely.
How we sequence it
- Audit the legacy stylesheet and the global scripts first. These produce the surprises.
- Stand up the shell and move one low-risk, low-traffic screen. Prove the boundary works in production before committing to a plan.
- Move by feature area rather than by whichever screen is easiest, so the team is only holding two models in one part of the product at a time.
- Keep every step independently deployable and independently revertible.
- Track the legacy bundle size as a first-class metric. It should fall every sprint.
In practice
Placeholder — real figures required before launch.
The situation: An AngularJS front end past end-of-life, two abandoned rewrite attempts, and a roadmap nobody would agree to pause.
What we did: Stood up a React shell alongside the existing application using custom elements, then moved screens in priority order, in production, while their team kept shipping.
Outcome: TO SUPPLY: screens migrated, weeks taken, legacy bundle reduction, days of feature freeze
Questions we get
Won't loading two frameworks kill performance?
It costs you something real, and the cost is above. What makes it acceptable is that it's bounded and temporary, most screens load one framework only, and the legacy bundle shrinks as screens move across. We measure it per screen and report it.
How do the two sides share auth and state?
The host owns the session and the guest reads it through a narrow interface. Shared state crosses the boundary as events or through a small framework-agnostic store. Neither side keeps a duplicate copy of the other's state.
What about CSS collisions?
Shadow DOM where it fits, scoped prefixes plus a no-global-selectors rule where it doesn't. The legacy stylesheet is the usual source of trouble and gets audited before the first screen moves.
Related capabilities
Tell us which screen you'd move first
We'll tell you whether coexistence is the right approach for your codebase, and say so if it isn't.