React inside an application you are not allowed to rewrite
Adding modern components to a mature Ember platform, screen by screen, without a migration project - and what that constraint teaches you about component boundaries.
Every developer who joins a mature codebase has the same first thought, and it is usually some version of we should rewrite this in the thing I like.
On a wealth administration platform - the kind of software that runs for a decade, administers real money, and has to meet accessibility and browser standards that are not negotiable - that thought does not survive contact with the estimate. The application worked. It was Ember. Nobody was going to fund replacing it, and nobody should have.
New work, though, was React. So the question was not which framework. It was: how do you add new components to an application you are not replacing, without creating a second application inside the first?
Mount points, not migrations
The approach that worked was unglamorous. Ember owns the page, the routing and the shell. At specific points, it hands a DOM node to a React component along with the data that component needs. React renders inside that node and does not reach outside it.
No shared store. No shared router. Data in through props, changes out through callbacks. When React needs server data it makes its own REST calls - POST, PUT, DELETE, GET - rather than borrowing whatever the host application already had in memory.
That sounds like a compromise. It is actually a discipline, and it is the useful part of the whole exercise.
What the boundary teaches you
I had spent a long time maintaining a shared component library across several products - a hosting dashboard and the tools around it, some Twig, some React. I thought I knew what a reusable component was.
Mounting components inside a host application built on a different framework is a much harsher examination. A component that reaches for global state does not work, because that state does not exist here. A component that assumes the router does not work, because the router belongs to Ember. A component that expects a particular CSS reset breaks visibly.
Everything implicit becomes explicit or it fails immediately. And a component that survives that is genuinely portable - not portable in the sense that you could copy it, portable in the sense that it has no opinions about who is rendering it.
The same test applies to a shared library and almost nobody runs it, because within one application the implicit dependencies keep working and you never notice they are there.
The parts that hurt
Two style systems in one page. Ember styles and component styles both apply. Scoped styles and a shared token layer keep it survivable. Specificity wars are the alternative and they never end.
Bundle size. Adding a framework to an application that already has one is not free. It is only defensible if new work genuinely goes into the new components, rather than one screen using React because someone felt like it.
Two mental models on the team. Data flows differently on each side of the mount point. This needs to be written down and shown to new joiners, or every code review re-litigates it.
Testing across the seam. Unit tests on either side pass happily while the integration is broken. End-to-end tests are what catch that - I have leaned on Cypress for exactly this on every project since.
Would I do it again
Yes, with one caveat: only where the new components have a real reason to exist. A screen being rebuilt, a feature that is genuinely new, a piece of UI that several products need.
Converting a working screen because it is old is how you end up maintaining two versions of the same thing, and shipping nothing anyone asked for.
The goal was never this app should be React. It was new work should not be held back by the age of the application it lives in. Those are different projects, and only one of them ever gets funded.