<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CobuildX AI</title>
    <description>The latest articles on DEV Community by CobuildX AI (cobuildx-ai).</description>
    <link>https://dev.to/cobuildx-ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15088%2Fd94010e3-279b-40ed-b4bc-4f6158184522.jpeg</url>
      <title>DEV Community: CobuildX AI</title>
      <link>https://dev.to/cobuildx-ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cobuildx-ai"/>
    <language>en</language>
    <item>
      <title>The Framework Nobody Wrote Down: Taking a Backbone App to React</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:49:28 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/the-framework-nobody-wrote-down-taking-a-backbone-app-to-react-39hi</link>
      <guid>https://dev.to/cobuildx-ai/the-framework-nobody-wrote-down-taking-a-backbone-app-to-react-39hi</guid>
      <description>&lt;p&gt;Backbone gave you models, views, a router and events, and left the rest to you. So most Backbone apps carry a homegrown framework on top: a global singleton, an event bus, a navigation controller, view cleanup code, a template build step and a drawer of jQuery plugins. Moving to React is mostly about finding that framework and deciding what replaces it. This guide covers how to run Backbone and React side by side, let React read Backbone models, turn views into components, test an app that never had tests, and delete the code that only existed to keep Backbone tidy.&lt;/p&gt;

&lt;p&gt;Backbone never tried to be a full framework. It gave you models, collections, views, a router and events, and then it left the rest to you. How views get cleaned up, how one part of the app talks to another, where navigation lives, how templates get built: your team decided all of that. Or, quite often, someone who left the company years ago did.&lt;/p&gt;

&lt;p&gt;That's why leaving Backbone feels different from leaving other frameworks. You aren't really moving off Backbone. You're moving off the framework your team built on top of it, and nobody ever wrote that one down. The good news is that a lot of it can simply be deleted, because React already does those jobs for you. The real work is finding it all first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; the examples in this post come from a small GitHub viewer we built with Backbone 1.0, RequireJS, jQuery Mobile and Handlebars. On a desktop its views sit side by side as panels, and on a phone each one becomes its own page. We rebuilt it in React 18 with the same screens, element ids and API calls. You can find both versions at &lt;a href="https://github.com/cobuild-tech/migratex-examples/tree/main/backbone-react" rel="noopener noreferrer"&gt;cobuild-tech/migratex-examples&lt;/a&gt;. The app was small, so we simply rewrote it. For bigger apps, we move step by step, and that's the approach this post describes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before You Start
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcls7k5oewl50k5fnng8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcls7k5oewl50k5fnng8p.png" alt="Before you start: Backbone itself provides models, collections, views, a router and events; on top of it most teams built their own framework, including a global singleton, an event bus, a navigation controller, view cleanup code, a template build step, jQuery plugins and global AJAX hooks. Count them before estimating" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why leave, and when not to
&lt;/h3&gt;

&lt;p&gt;Nobody we talk to is upset with &lt;a href="https://backbonejs.org/" rel="noopener noreferrer"&gt;Backbone&lt;/a&gt;. It did exactly what it promised. Teams leave because of everything that grew up around it. RequireJS, Handlebars 1.x and old jQuery plugins have gone quiet, and in our example, jQuery Mobile was &lt;a href="https://blog.jquery.com/2021/10/07/jquery-maintainers-continue-modernization-initiative-with-deprecation-of-jquery-mobile/" rel="noopener noreferrer"&gt;officially deprecated by its own maintainers&lt;/a&gt; in 2021. Every Backbone app also has its own house rules, so new developers have to learn your particular setup before they can change a button. And almost every Backbone team has a story about a "zombie" view that was removed from the page but kept listening to events.&lt;/p&gt;

&lt;p&gt;That said, be honest with yourself before you commit. If the app is stable, rarely changes and the people who look after it are happy, leaving it alone is a perfectly fair choice. Move when changing the app keeps getting more expensive, not just because the code looks old.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inventory the framework you built
&lt;/h3&gt;

&lt;p&gt;Before anyone gives an estimate, list every piece of setup your team added around Backbone. Even our small example had a surprising amount. For each item, you'll decide whether React already covers it or whether it can simply go:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the team built&lt;/th&gt;
&lt;th&gt;What it was for&lt;/th&gt;
&lt;th&gt;In our example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A global singleton&lt;/td&gt;
&lt;td&gt;Lets any module reach the controller, the event bus or the current user&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Globals&lt;/code&gt;, created to break a circular RequireJS dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An event bus&lt;/td&gt;
&lt;td&gt;Lets distant parts of the app talk without imports&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Globals.events.trigger('page:destroy', id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A navigation controller&lt;/td&gt;
&lt;td&gt;Decides which view goes where, often instead of &lt;code&gt;Backbone.Router&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;controller.js&lt;/code&gt; with &lt;code&gt;goToHomePage&lt;/code&gt;, &lt;code&gt;goToRepoPage&lt;/code&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;View cleanup code&lt;/td&gt;
&lt;td&gt;Unbinds events and removes DOM so views don't leak&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BaseView.close()&lt;/code&gt; plus a page stack in &lt;code&gt;events.js&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A template build step&lt;/td&gt;
&lt;td&gt;Precompiles templates into JavaScript&lt;/td&gt;
&lt;td&gt;The Handlebars CLI, run by hand from a README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global AJAX hooks&lt;/td&gt;
&lt;td&gt;Loading spinners, auth headers, error handling for every request&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$.ajaxSetup&lt;/code&gt; showing "Connecting To Github"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jQuery plugins&lt;/td&gt;
&lt;td&gt;Widgets, animation, layout&lt;/td&gt;
&lt;td&gt;jQuery Mobile pages and transitions, EnquireJS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's a quick way to get a rough size. Search the code for &lt;code&gt;.extend(&lt;/code&gt;, &lt;code&gt;listenTo(&lt;/code&gt;, &lt;code&gt;.on(&lt;/code&gt;, &lt;code&gt;trigger(&lt;/code&gt;, &lt;code&gt;$(&lt;/code&gt; and the name of your global object, and count the hits. The &lt;code&gt;extend&lt;/code&gt; count tells you how many models and views you have. The event and jQuery counts show how much hidden wiring sits between them, and that's where most of the time goes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get onto a modern bundler first
&lt;/h3&gt;

&lt;p&gt;If the app still loads through RequireJS or a long list of &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, change that first, before any React code arrives. The nice part is that you don't need to rewrite your modules to do it. &lt;a href="https://webpack.js.org/api/module-methods/#amd" rel="noopener noreferrer"&gt;webpack understands AMD &lt;code&gt;define()&lt;/code&gt; calls&lt;/a&gt;, so the existing code can be bundled as it is and moved to ES modules file by file later. &lt;a href="https://vitejs.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt; works really well for the new React code. While you're at it, move any hand-run template build step into the bundler, so it runs on every build.&lt;/p&gt;

&lt;p&gt;Then upgrade Backbone to 1.4 or later. It's a small step that brings old patterns to the surface early. Our example still read &lt;code&gt;this.options&lt;/code&gt; inside views, which &lt;a href="https://backbonejs.org/#changelog" rel="noopener noreferrer"&gt;Backbone removed in 1.1&lt;/a&gt;. It's also a good time to swap &lt;code&gt;model.on(...)&lt;/code&gt; for &lt;code&gt;this.listenTo(model, ...)&lt;/code&gt;, so views clean up their own listeners when they're removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Backbone and React Together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm2absjzc7g49d9q96sb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm2absjzc7g49d9q96sb.png" alt="Running Backbone and React together: a Backbone view hosts a React root, creating it in render and unmounting it in remove; a Backbone model is read by React through useSyncExternalStore, so both frameworks see the same data; then views are replaced one at a time until React owns routing" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rewriting everything at once is tempting, but it's risky. The safer path is Martin Fowler's &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;strangler fig&lt;/a&gt; approach: you add new code around the old app, piece by piece, until the old app isn't needed anymore. Backbone makes this easier than almost any other framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mounting React inside a Backbone view
&lt;/h3&gt;

&lt;p&gt;A Backbone view is really just an object that holds a DOM element, so you can hand that element straight to React with &lt;a href="https://react.dev/reference/react-dom/client/createRoot" rel="noopener noreferrer"&gt;&lt;code&gt;createRoot&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// views/cartView.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Cart&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../react/Cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CartView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;View&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;unmount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;View&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the Backbone app doesn't notice anything changed. It still creates a &lt;code&gt;CartView&lt;/code&gt; and calls &lt;code&gt;render()&lt;/code&gt; and &lt;code&gt;remove()&lt;/code&gt; as before. Calling &lt;code&gt;render()&lt;/code&gt; again simply updates the React component with new props. Just remember to override &lt;code&gt;remove()&lt;/code&gt; as shown, so the React root is cleaned up along with the view.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing models and the URL
&lt;/h3&gt;

&lt;p&gt;For a while, your React components will need data that still lives in Backbone models. There's no need to copy it into React state and keep two versions in sync. Backbone models already fire &lt;code&gt;change&lt;/code&gt; events, and that's all React's &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;&lt;code&gt;useSyncExternalStore&lt;/code&gt;&lt;/a&gt; hook needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useModelValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`change:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`change:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a Backbone view can call &lt;code&gt;cart.set('count', 3)&lt;/code&gt; and the React component updates on its own. One simple rule keeps this smooth: when an attribute holds an object or an array, always &lt;code&gt;set&lt;/code&gt; a new one instead of changing it in place. React compares values by reference, so a new value is how it knows something changed.&lt;/p&gt;

&lt;p&gt;The URL needs a single owner too. &lt;code&gt;Backbone.history&lt;/code&gt; and &lt;a href="https://reactrouter.com/" rel="noopener noreferrer"&gt;React Router&lt;/a&gt; both listen to the URL, so pick one at a time. Early on, Backbone keeps the router and React lives in small islands. Once most screens are in React, switch it around: React Router takes over, and the remaining Backbone screens are mounted by small React wrapper components. From there, you can move routes across one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing Models and Collections
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbx9679as2nfyjbzo76qq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbx9679as2nfyjbzo76qq.png" alt="A Backbone model or collection does five jobs: fetching and caching become plain fetch functions called through TanStack Query, parse() becomes a tested function ported first, defaults and validate() become form state and a validation schema, change and reset events become query cache updates and re-rendering, and Underscore filters become array methods during render" width="799" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Backbone model looks like one thing, but it does several jobs at once. Split them up and each one has a simple React replacement:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What a Backbone model does&lt;/th&gt;
&lt;th&gt;A good choice in React&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;url&lt;/code&gt;, &lt;code&gt;fetch()&lt;/code&gt;, &lt;code&gt;save()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Plain &lt;code&gt;fetch&lt;/code&gt; functions, called through &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;TanStack Query&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parse()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A tested function that shapes the API response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;defaults&lt;/code&gt;, &lt;code&gt;validate()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Form state with a validation function or schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;change&lt;/code&gt; events&lt;/td&gt;
&lt;td&gt;Query cache updates, with React re-rendering for you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collection &lt;code&gt;reset&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt; events&lt;/td&gt;
&lt;td&gt;Refetching or updating the cached list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;_.where&lt;/code&gt;, &lt;code&gt;_.filter&lt;/code&gt; on collections&lt;/td&gt;
&lt;td&gt;Everyday array methods, used during render&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;parse()&lt;/code&gt; methods are worth more than they look. They hold years of knowledge about your API, like which field was renamed or which endpoint wraps its results. Move them into plain functions with tests before anything else, so both apps can use them. Ours was a one-liner, which is pretty typical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Backbone&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;urlRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.github.com/legacy/user/search/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// React&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LEGACY_USER_SEARCH_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might spot two small improvements in the React version: it encodes the username for the URL, and it returns an empty object instead of &lt;code&gt;undefined&lt;/code&gt; when nothing matches. Porting is a great time to fix little bugs like these, as long as you write each one down.&lt;/p&gt;

&lt;p&gt;One more thing to keep in mind. Backbone views often created a new collection every time they were shown, so every click sent a fresh request. TanStack Query caches results, which is usually a nice speed boost. Where users expect a button to really refresh, add a request id to the query key so it fetches again, just like before. That's what we did in our port.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Views to Components
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff908vrga5gkj6hg758e5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff908vrga5gkj6hg758e5.png" alt="From views to components: a Backbone view goes through initialize, render, an events hash, listenTo, manual DOM updates and a hand-written close; a React component renders from state, handles events in JSX, and cleans up automatically when it unmounts, so the global event bus and page stack disappear" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the biggest change in how you think, so let's look at a real example. Here's our Backbone home view reacting to a user search. It finds DOM elements, changes them and runs a jQuery animation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;showAdditionalButtons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;gravatarUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gravatar_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatarContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slideDown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatarContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slideUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No User Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In React, the handler just updates state. The markup describes what each state looks like, and the slide becomes a CSS transition on a class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;gravatarUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gravatar_id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;hideAvatar&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No User Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;SLIDE_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`avatar-container&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;avatarOpen&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; is-open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the team gets used to this, a whole group of bugs goes away. The page can't fall out of step with the data anymore, because the page is always drawn from the data.&lt;/p&gt;

&lt;p&gt;The rest of a view carries over easily. The &lt;code&gt;events&lt;/code&gt; hash becomes &lt;code&gt;onClick&lt;/code&gt; and similar props on the elements themselves. Our views even had two versions of every event map, one for touch and one for click, and React only needs one. Handlebars templates become JSX, and the &lt;code&gt;{{#if isPhone}}&lt;/code&gt; blocks that wrapped every template became a single &lt;code&gt;&amp;lt;PhonePage&amp;gt;&lt;/code&gt; component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete the cleanup code
&lt;/h3&gt;

&lt;p&gt;This is the most satisfying part. Our example had a page stack, a &lt;code&gt;page:destroy&lt;/code&gt; event, a &lt;code&gt;close()&lt;/code&gt; method on a base view, and careful calls to &lt;code&gt;undelegateEvents()&lt;/code&gt; and &lt;code&gt;removeData()&lt;/code&gt;, all to stop views from leaking. In React, when a component is no longer rendered, it's gone, along with its event handlers. So we deleted all of that code instead of porting it. It even fixed a bug: clicking "Get User Activity" twice on desktop used to leave the first view attached to the page.&lt;/p&gt;

&lt;p&gt;The global event bus usually goes the same way. For each &lt;code&gt;trigger&lt;/code&gt;, ask who actually listens to it. Most of the time it's just one place, and the event becomes a prop, a callback or a shared piece of state. In our app, &lt;code&gt;page:destroy&lt;/code&gt; didn't need a replacement at all, because it only existed to clean up views, and React handles that for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Responsive layouts without jQuery Mobile
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqujmnummrel8o5wbuws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqujmnummrel8o5wbuws.png" alt="One set of components, two layouts: a useIsPhone hook built on matchMedia and useSyncExternalStore picks the layout live; on desktop the shared components render as panels on the home page, on a phone each is its own route with a Back header, replacing the EnquireJS startup check and controller-appended jQuery Mobile pages" width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our example had one more job to move. The same views showed up as panels on a desktop and as separate pages on a phone. The Backbone controller used EnquireJS to check the screen size once, at startup. In React, the screen size is just another value to subscribe to, using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia" rel="noopener noreferrer"&gt;&lt;code&gt;matchMedia&lt;/code&gt;&lt;/a&gt; and the same &lt;code&gt;useSyncExternalStore&lt;/code&gt; hook we used for models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PHONE_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all and (max-width: 599px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PHONE_QUERY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;mql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useIsPhone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PHONE_QUERY&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the panels and the pages share the same components. On a desktop, the home page shows them side by side. On a phone, each one is its own route with a Back header, so the browser's Back button works too, which the original never managed. Because the hook follows the window size, resizing switches the layout straight away. The jQuery Mobile look, with its header bars, rounded buttons, slide transitions and loading overlay, came across as a few hundred lines of plain CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Migration
&lt;/h2&gt;

&lt;p&gt;Many Backbone apps have few tests, and ours had none. So before changing anything, we wrote down what the app actually does, odd parts included. Michael Feathers calls these characterisation tests in &lt;a href="https://www.oreilly.com/library/view/working-effectively-with/0131177052/" rel="noopener noreferrer"&gt;&lt;em&gt;Working Effectively with Legacy Code&lt;/em&gt;&lt;/a&gt;: they record what the code does today, not what anyone thinks it should do. Reading our app closely turned up things nobody would have put in a spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user search fails, nothing happens at all, because the listener only ran on success.&lt;/li&gt;
&lt;li&gt;"No User Found" waits until the avatar has finished sliding away before it appears.&lt;/li&gt;
&lt;li&gt;Clicking "Get User Repositories" again clears the repo table as well as reloading the categories.&lt;/li&gt;
&lt;li&gt;The phone or desktop layout was decided once and never changed when the window was resized.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one became a test, or a written decision to change it. Without that list, the first three would have quietly changed during the port and come back months later as bug reports.&lt;/p&gt;

&lt;p&gt;For bigger apps, write the important user journeys as &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; tests and get them passing against the Backbone app first. Then run the same tests against React. A journey counts as migrated when it passes on both. Keeping the original element ids in the React markup gives both apps the same test handles, and mocking the API with &lt;a href="https://mswjs.io/" rel="noopener noreferrer"&gt;MSW&lt;/a&gt; lets one fake server work for both apps. Inside the React test suite, wait for what the user would see with Testing Library's &lt;a href="https://testing-library.com/docs/dom-testing-library/api-async/" rel="noopener noreferrer"&gt;&lt;code&gt;findBy&lt;/code&gt; queries&lt;/a&gt;. Our 23 tests cover the desktop panels, the phone pages and resizing across the breakpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yuuqk4ncatakcsy133u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yuuqk4ncatakcsy133u.png" alt="Characterisation tests record what the Backbone app does today, including the odd parts; the same Playwright journeys and MSW mocks then run against the Backbone app and the React app, and a journey counts as migrated only when it passes on both" width="799" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finishing the Migration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Helping the team settle in
&lt;/h3&gt;

&lt;p&gt;Code moves faster than habits, and Backbone habits run deep. These patterns show up in almost every team's first React pull requests, and each one has a simple React-friendly alternative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reaching for jQuery inside an effect.&lt;/strong&gt; If something on the page should look different, update state and let the component describe the new look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuilding the event bus.&lt;/strong&gt; Passing a callback, or sharing the state both components care about, keeps things easy to follow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing objects in place and forcing a render.&lt;/strong&gt; Create new values instead, and React will pick up the change on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using effects to keep two values in sync.&lt;/strong&gt; Usually the second value can just be calculated during render, as the &lt;a href="https://react.dev/learn/you-might-not-need-an-effect" rel="noopener noreferrer"&gt;React docs on effects&lt;/a&gt; explain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A short internal page that shows one of your own Backbone views next to its React version helps more than any general tutorial. Reviewing early React pull requests in pairs, with one person who knows the old app and one who knows React, spreads both kinds of knowledge quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The removal checklist
&lt;/h3&gt;

&lt;p&gt;You'll know you're finished when &lt;code&gt;backbone&lt;/code&gt;, &lt;code&gt;underscore&lt;/code&gt;, &lt;code&gt;jquery&lt;/code&gt; and the template runtime are removed from &lt;code&gt;package.json&lt;/code&gt;, along with their plugins. Before that commit, run through a few quick checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every screen is rendered by React, and &lt;code&gt;Backbone.history&lt;/code&gt; is no longer started.&lt;/li&gt;
&lt;li&gt;Everything the global singleton used to hold has a new home, or was removed on purpose.&lt;/li&gt;
&lt;li&gt;Auth headers, loading indicators and error handling from &lt;code&gt;$.ajaxSetup&lt;/code&gt; and &lt;code&gt;ajaxPrefilter&lt;/code&gt; now live in your fetch client.&lt;/li&gt;
&lt;li&gt;Plugin CSS, like jQuery Mobile's, has been replaced, and nothing depends on its class names.&lt;/li&gt;
&lt;li&gt;The bridge code, like React roots inside Backbone views and the model hook, has been cleaned up.&lt;/li&gt;
&lt;li&gt;Old URLs, including hash URLs like &lt;code&gt;#users/42&lt;/code&gt;, still work or redirect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5kzj58344yswjj7zzo8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5kzj58344yswjj7zzo8.png" alt="The removal checklist: no Backbone views or Backbone.history, the global singleton emptied, AJAX hooks moved to the fetch client, plugin CSS replaced, bridge code gone, old hash URLs redirected; then backbone, underscore, jquery and the template runtime are removed from package.json, with the last Backbone build kept behind a flag until traffic stays quiet" width="799" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last point is easy to miss. Many Backbone apps used hash URLs (&lt;code&gt;/#users/42&lt;/code&gt;), and those links live on in bookmarks, emails and help pages. The part after the &lt;code&gt;#&lt;/code&gt; never reaches the server, so a server redirect can't catch it. Add a small bit of code on the React side that reads the old hash and sends users to the new path, and keep it around for a long time. Also keep the last Backbone build ready to deploy behind a flag for a short while. Once real traffic has been quiet long enough, delete it and enjoy watching jQuery leave the bundle.&lt;/p&gt;

&lt;p&gt;Looking back, a Backbone migration is mostly about finding everything your team built because Backbone didn't, and noticing how much of it React makes unnecessary. In our example, the page stack, the event bus, the view cleanup and the global singleton were all deleted rather than ported. In short: list the homegrown framework before you estimate, get onto a modern bundler first, mount React inside Backbone views, and write down what the app really does before you test both versions against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" alt="MigrateX" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MigrateX is our software and services platform for moving software, data and infrastructure from one technology to another. It brings automation and experienced engineers together, so the repetitive work gets done quickly and the tricky decisions get the human attention they need.&lt;/p&gt;

&lt;p&gt;Everything in this guide comes from how we run Backbone to React projects with MigrateX. Here's what that looks like in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We scan your Backbone app and map the framework your team built around it: globals, event buses, controllers, cleanup code, template builds, AJAX hooks and jQuery plugins. You get a clear picture of where the effort really sits before anyone commits to a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration plan.&lt;/strong&gt; Together with your team, we decide what each piece becomes in React, what can simply be deleted, which screens move first, and who owns the URL along the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step-by-step migration.&lt;/strong&gt; Automation handles the repetitive parts, like moving the build to a modern bundler, converting templates to JSX and porting &lt;code&gt;parse()&lt;/code&gt; methods. Our engineers handle the parts that need judgement, like the data layer, routing and the views that bridge the two apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; We write down how your app behaves today, oddities included, and run the same Playwright tests against both apps. A screen only counts as migrated when it passes on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover and clean-up.&lt;/strong&gt; We remove the bridge code, jQuery and the last Backbone packages, keep a way back until traffic is quiet, and leave your team with a React codebase they know and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What you get along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No big-bang release.&lt;/strong&gt; Your app keeps running and shipping features while the migration happens in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less risk.&lt;/strong&gt; Shared tests, careful URL handling and a way back mean users don't notice the move, and you can pause at any point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A team that's ready.&lt;/strong&gt; We pair with your developers throughout, so they're comfortable with React long before the last Backbone view is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More than front ends.&lt;/strong&gt; The same approach covers other frameworks, like Angular and Ember, as well as data and infrastructure migrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;To see the code behind this guide, visit &lt;strong&gt;&lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;github.com/cobuild-tech/migratex-examples&lt;/a&gt;&lt;/strong&gt;. Still running a Backbone app that's been quietly doing its job for years? We'd love to hear about it. Tell us a little about your app through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free conversation about where the effort is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;migratex-examples&lt;/em&gt;: a responsive GitHub viewer built with Backbone 1.0, RequireJS, jQuery Mobile and Handlebars, and its React 18 rewrite, with a README describing the migration. &lt;a href="https://github.com/cobuild-tech/migratex-examples/tree/main/backbone-react" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/migratex-examples/tree/main/backbone-react&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Migration strategy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;M. Fowler. &lt;em&gt;StranglerFigApplication.&lt;/em&gt; martinfowler.com, 29 June 2004. &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/StranglerFigApplication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;M. Feathers. &lt;em&gt;Working Effectively with Legacy Code.&lt;/em&gt; Prentice Hall, 2004.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Backbone and its stack&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backbone.js documentation. &lt;a href="https://backbonejs.org/" rel="noopener noreferrer"&gt;https://backbonejs.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Backbone.js. &lt;em&gt;Change log&lt;/em&gt; (1.1.0 removed automatic &lt;code&gt;this.options&lt;/code&gt;). &lt;a href="https://backbonejs.org/#changelog" rel="noopener noreferrer"&gt;https://backbonejs.org/#changelog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;webpack. &lt;em&gt;Module methods: AMD.&lt;/em&gt; &lt;a href="https://webpack.js.org/api/module-methods/#amd" rel="noopener noreferrer"&gt;https://webpack.js.org/api/module-methods/#amd&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;jQuery. &lt;em&gt;jQuery maintainers continue modernization initiative with deprecation of jQuery Mobile.&lt;/em&gt; 7 October 2021. &lt;a href="https://blog.jquery.com/2021/10/07/jquery-maintainers-continue-modernization-initiative-with-deprecation-of-jquery-mobile/" rel="noopener noreferrer"&gt;https://blog.jquery.com/2021/10/07/jquery-maintainers-continue-modernization-initiative-with-deprecation-of-jquery-mobile/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;React and testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React. &lt;em&gt;createRoot.&lt;/em&gt; &lt;a href="https://react.dev/reference/react-dom/client/createRoot" rel="noopener noreferrer"&gt;https://react.dev/reference/react-dom/client/createRoot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;useSyncExternalStore.&lt;/em&gt; &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;https://react.dev/reference/react/useSyncExternalStore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;You Might Not Need an Effect.&lt;/em&gt; &lt;a href="https://react.dev/learn/you-might-not-need-an-effect" rel="noopener noreferrer"&gt;https://react.dev/learn/you-might-not-need-an-effect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React Router. &lt;a href="https://reactrouter.com/" rel="noopener noreferrer"&gt;https://reactrouter.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Playwright. &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;https://playwright.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Testing Library. &lt;em&gt;Async methods&lt;/em&gt; (&lt;code&gt;findBy&lt;/code&gt; queries). &lt;a href="https://testing-library.com/docs/dom-testing-library/api-async/" rel="noopener noreferrer"&gt;https://testing-library.com/docs/dom-testing-library/api-async/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MDN. &lt;em&gt;Window: matchMedia() method.&lt;/em&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Libraries&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TanStack Query. &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;https://tanstack.com/query/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MSW (Mock Service Worker). &lt;a href="https://mswjs.io/" rel="noopener noreferrer"&gt;https://mswjs.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vite. &lt;a href="https://vitejs.dev/" rel="noopener noreferrer"&gt;https://vitejs.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/backbone-to-react-migration-guide" rel="noopener noreferrer"&gt;CobuildX AI blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backbone</category>
      <category>react</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Leaving Ember for React: What Your Conventions Were Hiding</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:48:08 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/leaving-ember-for-react-what-your-conventions-were-hiding-1pjb</link>
      <guid>https://dev.to/cobuildx-ai/leaving-ember-for-react-what-your-conventions-were-hiding-1pjb</guid>
      <description>&lt;p&gt;Ember's great strength was that you didn't have to decide: the router loaded your data, Ember Data cached your records, the run loop handled timing and services found their way into components. When you move to React, every one of those becomes a decision your team owns. This guide is about finding those hidden conventions and choosing what replaces each one, from Ember Data and ember-concurrency to addons, acceptance tests and the React islands that let both frameworks share a page.&lt;/p&gt;

&lt;p&gt;One thing we always liked about Ember is that you could open someone else's project and find your way around in minutes. Ember had already decided most things for you: where data gets loaded, how records are stored, how services reach your components. That works really well while you stay. The trouble starts when you try to leave, because you realise a big part of your app's behaviour doesn't live in your code at all. It lives in Ember.&lt;/p&gt;

&lt;p&gt;Changing Handlebars templates into JSX isn't the hard part. Most developers get comfortable with it in a day or two. What takes time is finding all the small things Ember was doing in the background, and then deciding what should do that job in React. &lt;a href="https://www.hyrumslaw.com/" rel="noopener noreferrer"&gt;Hyrum's Law&lt;/a&gt; sums it up nicely: "all observable behaviors of your system will be depended on by somebody." In other words, your app relies on Ember in ways nobody ever wrote down.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; all the examples in this post come from Inkwell, a small blogging app we first built in Ember 3.24 and then rebuilt in React 18. We kept the same routes, HTML, &lt;code&gt;data-test-*&lt;/code&gt; selectors and acceptance tests. You can find both versions at &lt;a href="https://github.com/cobuild-tech/migratex-examples/tree/main/ember-react" rel="noopener noreferrer"&gt;cobuild-tech/migratex-examples&lt;/a&gt;. Inkwell was small, so we simply rewrote it. For bigger apps, we move step by step, and that's the approach this post describes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before You Start
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff19qx6sp1widy9t323ep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff19qx6sp1widy9t323ep.png" alt="Before you start: why teams leave Ember, when staying is the better call, and the hidden conventions to inventory before sizing the migration" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why leave, and when not to
&lt;/h3&gt;

&lt;p&gt;When we ask teams why they're moving away from Ember, they almost never say Ember is bad. What they say is that everything around it has become harder. New hires usually know React and have to learn Ember from scratch. There's often an old addon in &lt;code&gt;package.json&lt;/code&gt; that nobody has touched since 2019, and it's the reason the app is stuck two versions behind. And the tools they want to use, like design systems, chart libraries and payment widgets, all come with React examples first.&lt;/p&gt;

&lt;p&gt;That said, it's worth being honest with yourself before you commit. A modern Ember app on Octane and Embroider, looked after by people who know it well, is a perfectly good place to be. If your real problem is a handful of outdated addons, upgrading them will cost far less than a migration. Move because the ecosystem keeps slowing you down, not just because the code feels old.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inventory the hidden conventions
&lt;/h3&gt;

&lt;p&gt;Before anyone gives an estimate, sit down and list everything Ember handles for your app by convention. Each item on that list is a place where React lets you pick the approach that suits you best. In our experience, this list tells you much more about how long the migration will take than counting components ever does.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Convention&lt;/th&gt;
&lt;th&gt;What it does in Ember&lt;/th&gt;
&lt;th&gt;Where to look&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The resolver&lt;/td&gt;
&lt;td&gt;Finds components, services and helpers by their file names&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;app/&lt;/code&gt; folder layout, &lt;code&gt;@service foo&lt;/code&gt; with no import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route hooks&lt;/td&gt;
&lt;td&gt;Loads data before the page shows, handles redirects, loading and error screens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;model()&lt;/code&gt;, &lt;code&gt;beforeModel()&lt;/code&gt;, &lt;code&gt;loading.hbs&lt;/code&gt;, &lt;code&gt;error.hbs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ember Data&lt;/td&gt;
&lt;td&gt;Fetches and caches records, keeps one copy of each, links related records&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;this.store&lt;/code&gt;, models, adapters, serializers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The run loop&lt;/td&gt;
&lt;td&gt;Groups updates together and schedules async work&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;later&lt;/code&gt;, &lt;code&gt;debounce&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;schedule('afterRender')&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initializers&lt;/td&gt;
&lt;td&gt;Runs setup code when the app starts, before the first page&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app/initializers&lt;/code&gt;, &lt;code&gt;app/instance-initializers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Addons&lt;/td&gt;
&lt;td&gt;Auth, translations, forms, tasks, mock APIs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;package.json&lt;/code&gt;, often with their own rules on top&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's a simple way to get a rough size. Search the code for &lt;code&gt;@service&lt;/code&gt;, &lt;code&gt;this.store&lt;/code&gt;, &lt;code&gt;model(&lt;/code&gt;, &lt;code&gt;later(&lt;/code&gt;, &lt;code&gt;debounce(&lt;/code&gt; and &lt;code&gt;task(&lt;/code&gt;, and count how many times each one shows up. It takes about ten minutes and shows you where the heavy work is.&lt;/p&gt;

&lt;p&gt;Pay extra attention to initializers, because they're easy to overlook. Ember runs them automatically when the app starts, so they quietly read the login token, load feature flags and start analytics without anyone calling them. In React, startup code is explicit: it lives in the entry file or in a provider at the top of the app, where everyone can see it. That's a nice improvement, but it means each initializer needs to be moved on purpose. List them all early and give each one a clear home, so nothing like analytics gets left behind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get to Octane and Embroider first
&lt;/h3&gt;

&lt;p&gt;If some parts of your app still use Classic Ember (&lt;code&gt;Ember.Component&lt;/code&gt;, mixins, observers, or &lt;code&gt;computed()&lt;/code&gt; with long lists of keys), upgrade them to Octane first, while they're still in Ember. It feels like extra work, but it usually saves time. Octane already works a lot like React: native classes, &lt;code&gt;@tracked&lt;/code&gt;, clear &lt;code&gt;@args&lt;/code&gt; and data that flows one way. That makes Octane components fairly easy to port. Classic components, with observers firing in the background, have to be fully understood before you can move them, and that's slow.&lt;/p&gt;

&lt;p&gt;You don't have to do it all by hand. &lt;a href="https://github.com/ember-codemods/ember-native-class-codemod" rel="noopener noreferrer"&gt;ember-native-class-codemod&lt;/a&gt; converts a lot of the old classes for you, and the &lt;a href="https://guides.emberjs.com/release/upgrading/current-edition/" rel="noopener noreferrer"&gt;Ember upgrade guide&lt;/a&gt; covers the rest. Do the same for the build and move to &lt;a href="https://github.com/embroider-build/embroider" rel="noopener noreferrer"&gt;Embroider&lt;/a&gt;. It puts Ember on standard bundler tools, which makes it much easier to share packages, and later React components, between the two apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Ember and React Together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F859ide3kgxg3zpt10d8c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F859ide3kgxg3zpt10d8c.png" alt="Running Ember and React together: React islands mounted inside an Ember page through a react modifier, a shared cart store read by both the Ember service and the React island, and the progression from a few islands to whole routes moving to React" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you decide to leave Ember, it's tempting to stop everything and rewrite the whole app in one go. Most people who've tried that will tell you not to. Joel Spolsky famously called Netscape's full rewrite &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;"the single worst strategic mistake that any software company can make"&lt;/a&gt;, and that advice has held up for more than twenty years.&lt;/p&gt;

&lt;p&gt;The safer path is what Martin Fowler calls the &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;strangler fig&lt;/a&gt;. You add new code around the old app, piece by piece, until the old app is no longer needed. Slack rebuilt its desktop client this way. They set clear rules for how &lt;a href="https://slack.engineering/rebuilding-slack-on-the-desktop/" rel="noopener noreferrer"&gt;old and new code could talk to each other&lt;/a&gt;, and the first thing they shipped was the emoji picker.&lt;/p&gt;

&lt;p&gt;You might think the natural unit is one route at a time. In practice, Ember routes are often large and nested, and a single one can take weeks. So we start even smaller. We drop React components into Ember templates as small "islands", while Ember keeps running the rest of the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mounting React with a modifier
&lt;/h3&gt;

&lt;p&gt;This is simpler than it sounds. An Ember &lt;a href="https://github.com/ember-modifier/ember-modifier" rel="noopener noreferrer"&gt;modifier&lt;/a&gt; is given a DOM element, so it can start a React root inside it, and clean it up again when Ember removes that element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/modifiers/react.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-modifier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unmount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;react&lt;/span&gt; &lt;span class="nv"&gt;TodoList&lt;/span&gt; &lt;span class="nv"&gt;todos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="na"&gt;@todos&lt;/span&gt; &lt;span class="nv"&gt;onComplete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;complete&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This short version creates a fresh React root each time an argument changes, which is perfectly fine for your first island. For busier components, switch to a class-based modifier that keeps the same root and simply calls &lt;code&gt;root.render&lt;/code&gt; with the new props. React then updates only what changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing state across the boundary
&lt;/h3&gt;

&lt;p&gt;Once a few islands are in place, they'll need to share data with the Ember side. Say your React island needs to show the cart, but the cart lives in an Ember service. Each framework tracks changes in its own way, so the cleanest fix is a tiny store that sits outside both of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shared/cartStore.js&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;l&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React has a hook made for exactly this: &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;&lt;code&gt;useSyncExternalStore&lt;/code&gt;&lt;/a&gt;. Calling &lt;code&gt;useSyncExternalStore(cartStore.subscribe, cartStore.get)&lt;/code&gt; keeps the island in sync with the store. On the Ember side, the cart service subscribes once and copies the value into a &lt;code&gt;@tracked&lt;/code&gt; field, so the existing templates keep working as before. When the last Ember screen that uses the cart is gone, you delete the service, and the store is already in place for React.&lt;/p&gt;

&lt;p&gt;As more islands join up, you can start moving whole routes. One option is a small &lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;shell in front of both apps that decides which one handles each URL&lt;/a&gt;. Another is to let React take over routing and treat the remaining Ember screens as the islands. Either way, plan for the shared setup to be temporary. Set a rough end date for it, so the team keeps moving towards a single React app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing Ember Data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pull the store apart
&lt;/h3&gt;

&lt;p&gt;If there's one part of an Ember migration that tends to take longer than planned, it's &lt;a href="https://guides.emberjs.com/release/models/" rel="noopener noreferrer"&gt;Ember Data&lt;/a&gt;. A single line like &lt;code&gt;this.store.findRecord('todo', 1)&lt;/code&gt; does a lot of work. It fetches the record, cleans up its shape, caches it, connects it to related records, and makes sure every screen sees the same copy.&lt;/p&gt;

&lt;p&gt;React keeps data fetching separate from the view layer, so you get to choose the right tool for each of those jobs. The trick is to stop treating the store as one big thing. Break it into pieces and pick a replacement for each one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What Ember Data does&lt;/th&gt;
&lt;th&gt;A good choice in React&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fetch and cache&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;TanStack Query&lt;/a&gt;, which handles caching, refetching and keeping data fresh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translate the API shape&lt;/td&gt;
&lt;td&gt;Your existing adapters and serializers, moved into plain, tested functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One instance per record&lt;/td&gt;
&lt;td&gt;Refreshing the right queries after a change, or a normalised cache like RTK Query if you need it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationships&lt;/td&gt;
&lt;td&gt;Clear, separate requests, or letting the API return related data (&lt;code&gt;include&lt;/code&gt;, GraphQL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dirty tracking and rollback&lt;/td&gt;
&lt;td&gt;Form state with &lt;a href="https://react-hook-form.com/" rel="noopener noreferrer"&gt;React Hook Form&lt;/a&gt;, kept close to the form&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Serializers first, identity map maybe
&lt;/h3&gt;

&lt;p&gt;We always start with the serializers. They're easy to overlook, but they hold years of knowledge about your API: renamed fields, unusual date formats, and that one endpoint that sends back a different shape for reasons nobody remembers. Move them into a plain module, add a few tests, and both the Ember and React sides can use them while the migration is in progress.&lt;/p&gt;

&lt;p&gt;Next, ask yourself whether every record really needs to be one shared object. Many teams assume it does, simply because Ember always worked that way. In most apps, refreshing the right queries after a change keeps every screen up to date, and it's easier to follow when you're debugging.&lt;/p&gt;

&lt;p&gt;If your app relies heavily on the store, there's one more option worth a look. &lt;a href="https://github.com/emberjs/data" rel="noopener noreferrer"&gt;WarpDrive&lt;/a&gt;, the next version of Ember Data, is being built to work outside Ember too. Spend an afternoon checking where its React support stands before you decide.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsr1mrub3ws5iwesiunl2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsr1mrub3ws5iwesiunl2.png" alt="Ember Data's identity map, where every screen points at one shared record, compared with React query invalidation, where a mutation refreshes each screen's query; plus a guide: TanStack Query for most apps, a normalised cache when records must be shared, and WarpDrive when the app is deeply invested in the store" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Without the Run Loop
&lt;/h2&gt;

&lt;p&gt;If you ask an Ember developer about race conditions, they might not have much to say. That's because the run loop and &lt;a href="https://ember-concurrency.com/" rel="noopener noreferrer"&gt;ember-concurrency&lt;/a&gt; have been handling timing for them for years. For example, a search box that ignores old requests only takes a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;searchTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;restartableTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;restartableTask&lt;/code&gt; cancels the previous search when a new one starts, &lt;code&gt;timeout&lt;/code&gt; adds a short delay while the user types, and the task stops on its own if the user leaves the page.&lt;/p&gt;

&lt;p&gt;In React, you get the same result by choosing the right tool for each kind of async work. For anything that fetches data, TanStack Query does this job really well. You use the search term as the query key, and it makes sure only the latest result is shown. It even gives you an &lt;code&gt;AbortSignal&lt;/code&gt; to pass to &lt;code&gt;fetch&lt;/code&gt;, so old requests can be cancelled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debounced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebouncedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For things like polling, timers and animations, React keeps the setup and the cleanup together in the same effect, so it's easy to see what starts and what stops. The React team's guide &lt;a href="https://react.dev/learn/you-might-not-need-an-effect" rel="noopener noreferrer"&gt;You Might Not Need an Effect&lt;/a&gt; is a great read before you begin.&lt;/p&gt;

&lt;p&gt;Then go through your ember-concurrency tasks one by one and think about what each one was really for. A &lt;code&gt;restartable&lt;/code&gt; task usually becomes a query with the right key. A &lt;code&gt;drop&lt;/code&gt; task becomes a button that's disabled while the request is running. An &lt;code&gt;enqueue&lt;/code&gt; task becomes a small queue. And here's a nice surprise: many of those &lt;code&gt;later()&lt;/code&gt; and &lt;code&gt;schedule('afterRender')&lt;/code&gt; calls were only there to fix Ember timing issues. In React you often don't need them at all, so you can simply delete them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Addons and Tests
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Replacing the addons
&lt;/h3&gt;

&lt;p&gt;It helps to think of each addon as its own small migration. The good news is that React has a strong library for almost every one of them, and in a few cases you can bring your existing work straight across:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ember addon&lt;/th&gt;
&lt;th&gt;React option&lt;/th&gt;
&lt;th&gt;What you can keep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ember-intl&lt;/td&gt;
&lt;td&gt;&lt;a href="https://formatjs.github.io/docs/react-intl/" rel="noopener noreferrer"&gt;react-intl (FormatJS)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Your translation files, since both use the same ICU message format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ember-cli-mirage&lt;/td&gt;
&lt;td&gt;&lt;a href="https://mswjs.io/" rel="noopener noreferrer"&gt;MSW (Mock Service Worker)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Your fixtures; the handlers are written once and shared by both apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ember-simple-auth&lt;/td&gt;
&lt;td&gt;Your auth provider's SDK, or a small auth context&lt;/td&gt;
&lt;td&gt;The session storage format, as long as you keep it the same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ember-concurrency&lt;/td&gt;
&lt;td&gt;TanStack Query plus effects&lt;/td&gt;
&lt;td&gt;Your cancellation rules, once they're written down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ember-power-select&lt;/td&gt;
&lt;td&gt;Downshift, React Select or your design system&lt;/td&gt;
&lt;td&gt;A fresh start; set aside some time for a design review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ember-page-title&lt;/td&gt;
&lt;td&gt;&lt;a href="https://react.dev/reference/react-dom/components/title" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; in components (React 19)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Your page title text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We'd suggest moving translations and mocks early, before most of the components. If both apps read from one shared set of ICU translation files, users see the same wording on every screen from day one. And once your mocks are in MSW, the same fake API works in React tests, in Ember tests and on your laptop.&lt;/p&gt;

&lt;p&gt;Give authentication a little extra care. If both apps read the same session cookie or storage key, in the same format, users can move between Ember and React screens without ever noticing. That smooth hand-off is what makes a step-by-step migration feel invisible to your users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Acceptance tests are the contract
&lt;/h3&gt;

&lt;p&gt;One of the most valuable things an Ember team has is its acceptance tests. Years of them describe, in detail, what users can actually do. Treat them as the checklist the React version needs to pass.&lt;/p&gt;

&lt;p&gt;The tests will look a little different on the React side. Ember tests usually call &lt;code&gt;await settled()&lt;/code&gt;, which waits for the run loop, network requests and timers to finish. React Testing Library takes a more user-focused approach. It waits for what the user would actually see on screen, using &lt;a href="https://testing-library.com/docs/dom-testing-library/api-async/" rel="noopener noreferrer"&gt;&lt;code&gt;findBy...&lt;/code&gt; queries&lt;/a&gt;. Here's the same test in both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ember&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test-add]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test-todo]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// React Testing Library&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Todos&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/add/i&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAllByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listitem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what has worked well for us. Pick the user journeys that matter most and write them as &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; tests. Get them passing against the Ember app first, then run the exact same tests against React. When a journey passes on both, you know it's migrated. If it only passes on Ember, it still needs a bit more work, however finished the code looks. Keep your &lt;code&gt;data-test-*&lt;/code&gt; attributes as they are, so both apps share the same test handles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5geovczil7cr1ezpkjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5geovczil7cr1ezpkjx.png" alt="One Playwright test suite with shared data-test handles runs against the Ember app first, then the React app; a journey counts as migrated only when it passes on both, so one that passes only on Ember is not done yet" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finishing the Migration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Helping the team settle in
&lt;/h3&gt;

&lt;p&gt;Code moves faster than habits, and that's completely normal. When Ember developers write their first React pull requests, we usually see the same three patterns. Each one has a simple React-friendly alternative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turning every service into a context.&lt;/strong&gt; It's a natural first step, but most services don't need to be global. A plain module of functions, or state kept close to the components that use it, is often simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using effects the way observers were used.&lt;/strong&gt; In React, a value that depends on other values can usually be calculated right during render, or updated in the event handler that changed it. The &lt;a href="https://react.dev/learn/you-might-not-need-an-effect" rel="noopener noreferrer"&gt;React docs on effects&lt;/a&gt; explain this really well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetching data inside effects.&lt;/strong&gt; People miss Ember's &lt;code&gt;model()&lt;/code&gt; hook, and React has great options that feel just as familiar: &lt;a href="https://reactrouter.com/" rel="noopener noreferrer"&gt;router loaders&lt;/a&gt; or a query library like TanStack Query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A little support goes a long way here. A short internal guide that shows your app's common patterns in both Ember and React pays for itself within weeks. Pairing an Ember expert with a React expert on code reviews helps even more, and both of them usually learn something new.&lt;/p&gt;

&lt;h3&gt;
  
  
  The removal checklist
&lt;/h3&gt;

&lt;p&gt;You'll know the migration is finished on the day &lt;code&gt;ember-source&lt;/code&gt;, &lt;code&gt;ember-data&lt;/code&gt; and &lt;code&gt;ember-cli&lt;/code&gt; are removed from &lt;code&gt;package.json&lt;/code&gt;. Before you make that commit, run through a few quick checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every route, including error, loading and "not found" pages, is now served by React.&lt;/li&gt;
&lt;li&gt;Every initializer has a new home in React, or has been removed on purpose.&lt;/li&gt;
&lt;li&gt;Session, feature flags and analytics all start from the React entry file.&lt;/li&gt;
&lt;li&gt;The shared store, the island modifier and any other bridge code have been cleaned up.&lt;/li&gt;
&lt;li&gt;Old URLs still work, with redirects in place for anything that changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx71hh8k231vdc3c6svyh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx71hh8k231vdc3c6svyh.png" alt="The removal checklist: no route served by Ember, every initializer has a home, boot code starts in React, bridge code is gone, old URLs still resolve; then ember-source, ember-data and ember-cli are removed from package.json, with the last Ember build kept behind a flag until traffic stays quiet" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even after that, keep the last Ember build ready to deploy behind a flag for a short while. Real users have a way of finding the one thing nobody tested. TSB Bank is a well-known example. In 2018, a platform migration disrupted service for a large share of its 5.2 million customers, and UK regulators later fined the bank &lt;a href="https://www.bankofengland.co.uk/news/2022/december/tsb-fined-for-operational-resilience-failings" rel="noopener noreferrer"&gt;£48.65m&lt;/a&gt;. A front-end migration is much lower risk, but the lesson still applies: keep a way back until you're sure you won't need it. Once things have been quiet for long enough, delete it and enjoy the smaller bundle.&lt;/p&gt;

&lt;p&gt;Looking back, moving from Ember to React is mostly about making clear choices for the things Ember used to handle behind the scenes: loading data, caching, async timing and startup code. Your team now decides how each of these works, which means a bit more effort at the start and a cleaner, easier-to-follow app at the end.&lt;/p&gt;

&lt;p&gt;If we had to sum it up in a few lines: list the conventions before you estimate, get to Octane before you port, start with small islands, and give the data layer, addons and tests the same care as the components. The JSX part will take care of itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" alt="MigrateX" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MigrateX is our software and services platform for moving software, data and infrastructure from one technology to another. It brings automation and experienced engineers together, so the repetitive work gets done quickly and the tricky decisions get the human attention they need.&lt;/p&gt;

&lt;p&gt;Everything in this guide comes from how we run Ember to React projects with MigrateX. Here's what that looks like in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We scan your Ember app and build the inventory described above: services, store usage, route hooks, run loop calls, initializers and addons. You get a clear picture of where the effort really sits before anyone commits to a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration plan.&lt;/strong&gt; Together with your team, we decide what each convention becomes in React, which screens move first, and how Ember and React will share state, sessions and translations along the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step-by-step migration.&lt;/strong&gt; Automation handles the repetitive parts, like converting templates, porting serializers and setting up MSW mocks. Our engineers handle the parts that need judgement, like data layer design, async behaviour and the islands that bridge the two apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; Your most important user journeys run as Playwright tests against both apps. A screen only counts as migrated when it passes on both, so progress is something you can see, not just something you're told.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover and clean-up.&lt;/strong&gt; We remove the bridge code and the last Ember packages, keep a way back until traffic is quiet, and leave your team with a React codebase they know and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What you get along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No big-bang release.&lt;/strong&gt; Your app keeps running and shipping features while the migration happens in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less risk.&lt;/strong&gt; Shared tests, a shared session and a way back mean users don't notice the move, and you can pause at any point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A team that's ready.&lt;/strong&gt; We pair with your developers throughout, so they're comfortable with React long before the last Ember screen is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More than front ends.&lt;/strong&gt; The same approach covers other frameworks, like Angular and Backbone, as well as data and infrastructure migrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;To see the code behind this guide, visit &lt;strong&gt;&lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;github.com/cobuild-tech/migratex-examples&lt;/a&gt;&lt;/strong&gt;. Planning a move from Ember, or already partway through? We'd love to hear about it. Tell us a little about your app through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free conversation about where the effort is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;migratex-examples&lt;/em&gt;: Inkwell, an Ember 3.24 RealWorld blogging app, and its React 18 rewrite. &lt;a href="https://github.com/cobuild-tech/migratex-examples/tree/main/ember-react" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/migratex-examples/tree/main/ember-react&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Migration strategy and case studies&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;H. Wright. &lt;em&gt;Hyrum's Law.&lt;/em&gt; &lt;a href="https://www.hyrumslaw.com/" rel="noopener noreferrer"&gt;https://www.hyrumslaw.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;J. Spolsky. &lt;em&gt;Things You Should Never Do, Part I.&lt;/em&gt; Joel on Software, 6 April 2000. &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;M. Fowler. &lt;em&gt;Strangler Fig Application.&lt;/em&gt; martinfowler.com. &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/StranglerFigApplication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;C. Jackson. &lt;em&gt;Micro Frontends.&lt;/em&gt; martinfowler.com, 19 June 2019. &lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/micro-frontends.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Slack Engineering. &lt;em&gt;When a rewrite isn't: rebuilding Slack on the desktop.&lt;/em&gt; 22 July 2019. &lt;a href="https://slack.engineering/rebuilding-slack-on-the-desktop/" rel="noopener noreferrer"&gt;https://slack.engineering/rebuilding-slack-on-the-desktop/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bank of England. &lt;em&gt;TSB fined for operational resilience failings.&lt;/em&gt; December 2022. &lt;a href="https://www.bankofengland.co.uk/news/2022/december/tsb-fined-for-operational-resilience-failings" rel="noopener noreferrer"&gt;https://www.bankofengland.co.uk/news/2022/december/tsb-fined-for-operational-resilience-failings&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ember&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ember.js. &lt;em&gt;Upgrading to Octane.&lt;/em&gt; &lt;a href="https://guides.emberjs.com/release/upgrading/current-edition/" rel="noopener noreferrer"&gt;https://guides.emberjs.com/release/upgrading/current-edition/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ember-native-class-codemod. &lt;a href="https://github.com/ember-codemods/ember-native-class-codemod" rel="noopener noreferrer"&gt;https://github.com/ember-codemods/ember-native-class-codemod&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Embroider: modern build tooling for Ember. &lt;a href="https://github.com/embroider-build/embroider" rel="noopener noreferrer"&gt;https://github.com/embroider-build/embroider&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ember-modifier. &lt;a href="https://github.com/ember-modifier/ember-modifier" rel="noopener noreferrer"&gt;https://github.com/ember-modifier/ember-modifier&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ember.js. &lt;em&gt;Ember Data.&lt;/em&gt; &lt;a href="https://guides.emberjs.com/release/models/" rel="noopener noreferrer"&gt;https://guides.emberjs.com/release/models/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;WarpDrive (the successor to Ember Data). &lt;a href="https://github.com/emberjs/data" rel="noopener noreferrer"&gt;https://github.com/emberjs/data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ember-concurrency. &lt;a href="https://ember-concurrency.com/" rel="noopener noreferrer"&gt;https://ember-concurrency.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;React and testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React. &lt;em&gt;useSyncExternalStore.&lt;/em&gt; &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;https://react.dev/reference/react/useSyncExternalStore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;You Might Not Need an Effect.&lt;/em&gt; &lt;a href="https://react.dev/learn/you-might-not-need-an-effect" rel="noopener noreferrer"&gt;https://react.dev/learn/you-might-not-need-an-effect&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;.&amp;lt;/em&amp;gt; &amp;lt;a href="https://react.dev/reference/react-dom/components/title"&amp;gt;https://react.dev/reference/react-dom/components/title&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;React Router. &amp;lt;em&amp;gt;Data loading.&amp;lt;/em&amp;gt; &amp;lt;a href="https://reactrouter.com/"&amp;gt;https://reactrouter.com/&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Testing Library. &amp;lt;em&amp;gt;Async methods&amp;lt;/em&amp;gt; (&amp;lt;code&amp;gt;findBy&amp;lt;/code&amp;gt; queries). &amp;lt;a href="https://testing-library.com/docs/dom-testing-library/api-async/"&amp;gt;https://testing-library.com/docs/dom-testing-library/api-async/&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Playwright. &amp;lt;a href="https://playwright.dev/"&amp;gt;https://playwright.dev/&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Libraries&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;TanStack Query. &amp;lt;a href="https://tanstack.com/query/latest"&amp;gt;https://tanstack.com/query/latest&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;React Hook Form. &amp;lt;a href="https://react-hook-form.com/"&amp;gt;https://react-hook-form.com/&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;FormatJS. &amp;lt;em&amp;gt;react-intl.&amp;lt;/em&amp;gt; &amp;lt;a href="https://formatjs.github.io/docs/react-intl/"&amp;gt;https://formatjs.github.io/docs/react-intl/&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;MSW (Mock Service Worker). &amp;lt;a href="https://mswjs.io/"&amp;gt;https://mswjs.io/&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;Originally published on the &amp;lt;a href="https://cobuildx.ai/blog/ember-to-react-migration-guide"&amp;gt;CobuildX AI blog&amp;lt;/a&amp;gt;.&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;
&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ember</category>
      <category>react</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Moving from Angular to React: A Practical Guide</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:37:31 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/moving-from-angular-to-react-a-practical-guide-4iki</link>
      <guid>https://dev.to/cobuildx-ai/moving-from-angular-to-react-a-practical-guide-4iki</guid>
      <description>&lt;p&gt;If you've spent time in Angular, you're used to a framework that makes most of the decisions for you. Templates, dependency injection, routing and forms all come in the box. React gives you much less: it's a library for building components, and routing, shared state and how you structure the app are left to you. That difference is why moving between them takes more than rewriting syntax. We ported large applications from Angular to React, and this is what we learned along the way: how to pick a strategy, how Angular's ideas map onto React's, and how to make the move without breaking things for your users.&lt;/p&gt;

&lt;p&gt;Most Angular-to-React migrations don't fail on a tricky hook or a missing library. They fail on a decision made in the first week: how the move should happen at all.&lt;/p&gt;

&lt;p&gt;So before we touched a single component, we had to answer a risk question rather than a technical one. Rewrite the whole thing in one go, or move it across in pieces while both versions keep running? Most of what follows depends on that answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; the examples here come from a small todo app we built twice, once in Angular 14 and once in React 19, with the same features, styling and tests. Both versions are on GitHub at &lt;a href="https://github.com/cobuild-tech/migratex-examples/tree/main/angular-react" rel="noopener noreferrer"&gt;cobuild-tech/migratex-examples&lt;/a&gt; [1]. The app is small enough that we did it as a full rewrite. The incremental advice below is what we follow for bigger applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Choosing the Strategic Approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The full rewrite
&lt;/h3&gt;

&lt;p&gt;In a full rewrite you leave the old application alone and build a new one next to it, feature by feature. On a chosen day, traffic switches over and the old app is retired.&lt;/p&gt;

&lt;p&gt;For a small, low-stakes app, that's fine. It's what we did with the todo app: two components, one service, no real users depending on it. For anything larger it hides a nasty risk. The team can spend months writing code that no real user ever touches, and the feedback only arrives at the end, which is the most expensive moment to find out you misunderstood something. Joel Spolsky made the classic case against big rewrites back in 2000 [3], and most of it still holds.&lt;/p&gt;

&lt;p&gt;The business doesn't pause while you do this, either. Feature requests keep coming, so each one gets built twice (once in the old app to keep it alive, once in the new one), or it waits until after the cutover. Nobody enjoys being the one to explain that to a product manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  The incremental approach
&lt;/h3&gt;

&lt;p&gt;An incremental migration takes the opposite view. The old app is never frozen and never swapped out in one go. It gets replaced piece by piece, with both versions serving real users at the same time.&lt;/p&gt;

&lt;p&gt;The industry name for this is the &lt;strong&gt;Strangler Fig&lt;/strong&gt; pattern [2], after a vine that starts small on a host tree and slowly grows around it, until the tree isn't needed any more and only the fig is left standing.&lt;/p&gt;

&lt;p&gt;In software, the vine is a thin routing layer. For each route (or even each request) it decides whether the old system or the new one answers. More routes move across over time, until the old system has nothing left to serve and you can switch it off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd6656ou1eeo1gpyd2qj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd6656ou1eeo1gpyd2qj.png" alt="Strangler Fig stages: Angular serves 100%, then 50%, then 0% of routes while React grows from 0% to 50% to 100%" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For almost anything with real users, this is the approach we'd pick, because every step is small and you can undo it. A route that moves gets tried by real people within days, not months. If it breaks, the router sends that route back to the old app while you fix it. The old app doesn't vanish overnight. It hands over one route at a time, until there's nothing left for it to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to choose
&lt;/h3&gt;

&lt;p&gt;The questions that decide it turn out to be about the organisation more than the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do real users depend on the app today?&lt;/strong&gt; If they do, a rewrite puts them at risk for the whole length of the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How big is it, and how many teams touch it?&lt;/strong&gt; One team and a handful of screens can survive a rewrite. Several teams, dozens of screens and a steady stream of feature requests make incremental far more practical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can the business live with a feature freeze?&lt;/strong&gt; A rewrite almost always needs one, because anything added to the old app mid-rewrite has to be built again in the new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can both frameworks run side by side?&lt;/strong&gt; Incremental depends on it, usually through a micro frontend shell [4].&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our rule of thumb: rewrite if the app is small, low-risk and owned by one team. Otherwise go incremental, and let the old system keep serving users while the new one gradually takes over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping What We Already Knew
&lt;/h2&gt;

&lt;p&gt;What surprised us once we started was how much Angular and React agree on. They solve mostly the same problems. They just use different mechanisms to do it.&lt;/p&gt;

&lt;p&gt;So the work wasn't learning React from scratch. It was matching each Angular mechanism to its React counterpart, and resisting the urge to rebuild Angular's machinery inside React. Here's the mapping we ended up with. The official Angular [7] and React [13] docs cover each mechanism in more depth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztenmzlc7tpgqbze7z98.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fztenmzlc7tpgqbze7z98.png" alt="Migration workflow in six steps: understand, choose a path, map concepts, move bottom-up, verify, deprecate" width="799" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Templates
&lt;/h3&gt;

&lt;p&gt;Open an Angular component next to a React one and the first thing you notice is the file count: two files against one. It's tempting to call that a formatting difference and move on. It's more than that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same screen, two ways of writing it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To keep the comparison honest, we built the same todo app twice (&lt;a href="https://github.com/cobuild-tech/migratex-examples/tree/main/angular-react" rel="noopener noreferrer"&gt;source on GitHub&lt;/a&gt; [1]), once in each framework, with identical behaviour and identical styling. Here's the single todo item from each.&lt;/p&gt;

&lt;p&gt;In Angular, the logic lives in a TypeScript class, and a decorator points it at a separate HTML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todo-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./todo-item.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TodoItemComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;onComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"todo-item {{ type }} {{ isDone ? 'done' : '' }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"handleClickCheck()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ title }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In React, it's one function that returns the markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onComplete&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`todo-item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClickCheck&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both render exactly the same &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;, and both are answering the same question: &lt;strong&gt;given this data, what should appear on the screen?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dgwjbradbus3j7sz28f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dgwjbradbus3j7sz28f.png" alt="Angular joins a class file and a template file with the @Component decorator and compiles the template; React's TodoItem is one function whose JSX is plain JavaScript. Both answer: given this data, what should appear on the screen?" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A template is a language. JSX is a value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the difference that explains nearly everything else. An Angular template looks like HTML, but it's really Angular's own small language [8], which the compiler reads and turns into code. It has its own vocabulary for everything a screen needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you need&lt;/th&gt;
&lt;th&gt;Angular template&lt;/th&gt;
&lt;th&gt;React JSX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Show a value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ title }}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{title}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass data to a child&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[title]="item.title"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;title={item.title}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React to an event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(onDelete)="handleItemDelete($event)"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;onDelete={handleItemDelete}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop over a list&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*ngFor="let item of todoList"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;todoList.map(item =&amp;gt; ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show conditionally&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*ngIf="completeList.length"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;completeList.length &amp;gt; 0 &amp;amp;&amp;amp; ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep an input in sync&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[(ngModel)]="title"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;value={title}&lt;/code&gt; + &lt;code&gt;onChange&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at the right-hand column. React has no special syntax for loops or conditions, because JSX is just JavaScript [14]. &lt;code&gt;&amp;lt;p&amp;gt;{title}&amp;lt;/p&amp;gt;&lt;/code&gt; compiles to an ordinary function call that returns an object describing the UI. A loop is &lt;code&gt;.map()&lt;/code&gt;, a condition is &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, and a variable is a variable.&lt;/p&gt;

&lt;p&gt;A few practical things fall out of that. Scope works differently: an Angular template sees the fields of its class, linked by name across two files, while JSX sees whatever is in scope a few lines up, so TypeScript flags a misspelled name straight away.&lt;/p&gt;

&lt;p&gt;Components are found differently, too. &lt;code&gt;&amp;lt;todo-item&amp;gt;&lt;/code&gt; only works in Angular because &lt;code&gt;TodoItemComponent&lt;/code&gt; is listed in the module's &lt;code&gt;declarations&lt;/code&gt;. &lt;code&gt;&amp;lt;TodoItem&amp;gt;&lt;/code&gt; works in React because it was imported at the top of the file. There's no registry, just imports.&lt;/p&gt;

&lt;p&gt;Talking back to the parent gets simpler. Angular needs &lt;code&gt;@Output()&lt;/code&gt;, an &lt;code&gt;EventEmitter&lt;/code&gt; and &lt;code&gt;.emit()&lt;/code&gt;. In React, &lt;code&gt;onComplete&lt;/code&gt; is just a prop that happens to be a function, and the child calls it.&lt;/p&gt;

&lt;p&gt;And because JSX is a value, you can keep markup in a variable, return it early or pass it to another component, which you can't do with a template file. The smaller renames follow from the same idea: &lt;code&gt;class&lt;/code&gt; becomes &lt;code&gt;className&lt;/code&gt;, image paths become imports, and &lt;code&gt;trackBy&lt;/code&gt; [11] becomes &lt;code&gt;key&lt;/code&gt; [15]. Our Angular &lt;code&gt;trackById&lt;/code&gt; method simply disappeared. In React it's &lt;code&gt;key={item.id}&lt;/code&gt; on the list item.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which do we prefer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For this kind of work, we lean towards JSX: one file, one language, and TypeScript checking the markup and the logic together. But Angular's constraint deserves credit. Its templates make it hard for business logic to creep into the view, and nothing in JSX stops a render function from filling up with logic. In React, keeping components clean is a team habit, not a framework rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means for a migration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Porting a template isn't a line-by-line translation of directives. It's closer to rewriting a small program in another language: &lt;code&gt;*ngFor&lt;/code&gt; becomes a map, &lt;code&gt;*ngIf&lt;/code&gt; becomes an expression, &lt;code&gt;@Output&lt;/code&gt; becomes a callback. The screen that comes out should be identical, and in our todo app it is.&lt;/p&gt;

&lt;p&gt;The more useful question isn't &lt;em&gt;"what's React's version of this directive?"&lt;/em&gt; It's &lt;em&gt;"what is this template trying to show, and how would I say that in plain JavaScript?"&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Two-way binding
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxkjg3vi2ijh5nnvov1cl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxkjg3vi2ijh5nnvov1cl.png" alt="Angular two-way binding, where ngModel lets the input and the class field both write title, compared with React one-way flow from state to input to setTitle" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Angular, &lt;code&gt;[(ngModel)]&lt;/code&gt; [9] keeps a form field and a piece of data in sync in both directions. Type in the input and the class field updates; change the field in code and the input updates. In the todo app, the title box is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt; &lt;span class="na"&gt;[(ngModel)]=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "banana in a box" syntax is really two bindings folded together. &lt;code&gt;[ngModel]&lt;/code&gt; pushes the value down into the input, and &lt;code&gt;(ngModelChange)&lt;/code&gt; pushes every change back up into the class.&lt;/p&gt;

&lt;p&gt;React only gives you the first half. Data flows one way, from state down to the screen, and you write the way back yourself with a controlled input [16]:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it's more typing. What you get for it is traceability. In Angular, &lt;code&gt;title&lt;/code&gt; can change from the template, from the class, or from anything else bound to that field, and none of those paths are visible from the input. In React, the only way &lt;code&gt;title&lt;/code&gt; changes is through &lt;code&gt;setTitle&lt;/code&gt;. Search for it and you've found every place the data can change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is allowed to change the data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's really the whole difference. With two-way binding, both sides can: the class writes &lt;code&gt;title&lt;/code&gt;, the input writes &lt;code&gt;title&lt;/code&gt;, and the framework quietly keeps them in sync. It's like a shared document that two people are editing at once. Convenient, until something looks wrong and you have to work out who changed it.&lt;/p&gt;

&lt;p&gt;With one-way data flow, only the state can. The screen is just a picture of that state. The input never changes &lt;code&gt;title&lt;/code&gt; itself; it reports what the user typed through &lt;code&gt;onChange&lt;/code&gt;, and the state decides what to do with it. One person owns the document, and everyone else sends suggestions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Two-way binding (Angular)&lt;/th&gt;
&lt;th&gt;One-way data flow (React)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who owns the data&lt;/td&gt;
&lt;td&gt;Shared by the view and the class&lt;/td&gt;
&lt;td&gt;The state, and only the state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How the view changes data&lt;/td&gt;
&lt;td&gt;Writes to it directly&lt;/td&gt;
&lt;td&gt;Asks, through an event handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where to look when a value is wrong&lt;/td&gt;
&lt;td&gt;The template, the class, and anything bound to them&lt;/td&gt;
&lt;td&gt;Every call to the setter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Simple forms with a few fields&lt;/td&gt;
&lt;td&gt;Screens where inputs depend on each other&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a simple form with a few fields, two-way binding is genuinely nicer to write. Once inputs start depending on each other, though, we'd take one-way flow every time. React makes that choice for you, and a migration is a good moment to get used to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency injection
&lt;/h3&gt;

&lt;p&gt;Angular hands services to components through the constructor [10]. The DI container creates the service, decides how long it lives, and gives the same instance to everyone who asks for it. In the todo app, the component just declares what it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;localStorageService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LocalStorageService&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ToDo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React has no container like that, so what happens to the service?&lt;/p&gt;

&lt;p&gt;When we opened &lt;code&gt;LocalStorageService&lt;/code&gt;, the answer was a bit of an anticlimax. It was a generic class with an empty constructor and two methods, &lt;code&gt;getItem&lt;/code&gt; and &lt;code&gt;setItem&lt;/code&gt;. The class existed mainly so the DI system had something to inject. In React, those became two exported functions in a nine-line file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../services/localStorageService&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing to register, no lifetime to manage. Where logic also needs to hold state or run side effects, it becomes a custom hook instead [19]. That's where the todo state went: everything &lt;code&gt;AppComponent&lt;/code&gt; used to hold now lives in &lt;code&gt;useTodos()&lt;/code&gt;, which &lt;code&gt;App&lt;/code&gt; calls like any other function.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4am0q2flvx43jhhhw472.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4am0q2flvx43jhhhw472.png" alt="Angular's injector creates one shared LocalStorageService and injects it through constructors; in React, localStorageService.ts exports functions that the useTodos() hook imports and App calls" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't rebuild Angular's architecture in React just because the old app had it. Work out what the service was actually doing, and move only that.&lt;/p&gt;

&lt;h3&gt;
  
  
  State management
&lt;/h3&gt;

&lt;p&gt;In Angular, state usually lives in one of two places: inside a component, or in a shared service that DI hands to whoever needs it. Because DI is built in, sharing state is almost free, and a lot of Angular apps put most of their state into services from day one.&lt;/p&gt;

&lt;p&gt;React starts from the opposite default. State begins local, inside the component, with &lt;code&gt;useState&lt;/code&gt;. It only moves when other components need it: first up to the nearest parent they share [17], then into React Context [18] if they're far apart in the tree. Only when that stops being enough do you reach for a library such as Redux [24] or Zustand [25].&lt;/p&gt;

&lt;p&gt;The progression looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ohynu2bbv1kx6z4tgco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ohynu2bbv1kx6z4tgco.png" alt="React state progression: local useState, lifted to a shared parent, Context, then a library such as Redux or Zustand" width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Angular reaches for services&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Angular was built around dependency injection from the start, and a service marked &lt;code&gt;providedIn: 'root'&lt;/code&gt; is a single shared instance for the whole app. That makes it a natural home for shared state. Our todo app keeps its state in &lt;code&gt;AppComponent&lt;/code&gt;, but a small shared store in Angular 14 typically looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TodoStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ToDo&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;todos$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asObservable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToDo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any component that asks for &lt;code&gt;TodoStore&lt;/code&gt; in its constructor gets the same instance, and so the same list. Sharing takes one line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why React didn't follow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React started from a different idea: &lt;strong&gt;the UI is a function of state.&lt;/strong&gt; A component is just a function that runs again whenever its state or props change. There's no container holding shared instances, so state lives where it's rendered, and sharing it is something you do on purpose.&lt;/p&gt;

&lt;p&gt;In our React version, all the todo state lives in the &lt;code&gt;useTodos()&lt;/code&gt; hook. &lt;code&gt;App&lt;/code&gt; calls it once, and each &lt;code&gt;TodoItem&lt;/code&gt; gets only what it needs through props. If two components far apart in the tree needed the same data, Context would let them share it without passing props through every level. The todo app is too small to need it, but it would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TodosContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTodos&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TodosContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;TodosContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CompletedCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;completeList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TodosContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;completeList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; done&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything bigger than that, React leaves to libraries such as Redux or Zustand. That's deliberate: the core stays small, and each team picks the tool that fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for in-place updates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One porting detail is easy to miss. Angular's &lt;code&gt;handleSubmit&lt;/code&gt; adds a todo with &lt;code&gt;this.todoList.push(...)&lt;/code&gt; and then saves &lt;code&gt;this.todoList&lt;/code&gt; to localStorage. Angular's change detection is happy with an array being changed in place. React isn't. Push onto an array held in state and nothing re-renders [20], and reading a state variable straight after calling its setter still gives you the old value [21].&lt;/p&gt;

&lt;p&gt;So every handler in &lt;code&gt;useTodos()&lt;/code&gt; builds the next list first (&lt;code&gt;[...todoList, newTodo]&lt;/code&gt; rather than &lt;code&gt;push&lt;/code&gt;), passes it to the setter, and saves that same list to localStorage. It's a small change, but it's the kind that passes a quick review and then shows up as a list that doesn't update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros and cons&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;th&gt;Angular: shared services&lt;/th&gt;
&lt;th&gt;React: local-first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pros&lt;/td&gt;
&lt;td&gt;Sharing is built in and takes one line. There is one obvious place for shared data. Every team follows the same pattern.&lt;/td&gt;
&lt;td&gt;State sits next to the code that uses it. Data flow is easy to trace. Nothing gets shared by accident.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cons&lt;/td&gt;
&lt;td&gt;It is easy to share state that never needed sharing. Any component that injects the service can change it. It is hard to see which components depend on a service.&lt;/td&gt;
&lt;td&gt;Passing props through many levels ("prop drilling") gets tedious. You have to choose a tool for app-wide state. Different teams may choose differently.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Coming from Angular, the temptation is to reach for the biggest tool first and set up a global store on day one. Resist it. Use the smallest tool that solves today's problem, and move up only when you actually feel the pain of not having something bigger. The todo app never needed more than &lt;code&gt;useState&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing
&lt;/h3&gt;

&lt;p&gt;Angular ships its router as part of the framework [12]. React doesn't ship one at all. You add a library such as React Router [22], or pick a framework like Next.js [23] if you also want server rendering.&lt;/p&gt;

&lt;p&gt;It sounds like a bigger difference than it is. In both, &lt;strong&gt;a URL should resolve to a screen&lt;/strong&gt;; what changes is the configuration and the tooling around it, not how you think about navigation. (The todo app has no routes at all, so this was the one part of the mapping we didn't need.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyi01y5bfaecg7sss87a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feyi01y5bfaecg7sss87a.png" alt="Routing in both frameworks: a URL such as /dashboard goes to a router that matches it to a screen. Angular builds the router in; React adds React Router or Next.js" width="798" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Migration Should Actually Happen, Step by Step
&lt;/h2&gt;

&lt;p&gt;With the strategy chosen and the concepts mapped, the migration itself followed a fixed order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fne8an3c3up2twjk85vc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fne8an3c3up2twjk85vc0.png" alt="Four migration steps in order: set up the tooling, let old and new coexist, migrate bottom-up, verify constantly" width="800" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The order matters more than it looks. Most of the risk in a migration comes from skipping a step, or doing them out of sequence.&lt;/p&gt;

&lt;p&gt;If the steps look familiar, it's because they're the &lt;strong&gt;Strangler Fig&lt;/strong&gt; pattern in practice. Step 2 builds the routing layer, the "vine" that decides who answers each route. Step 3 moves routes across one at a time. Step 4, plus the rollback plan further down, makes sure every move can be undone. That combination is where the reliability comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a mistake affects one route, not the whole application&lt;/li&gt;
&lt;li&gt;every migrated piece meets real users within days, not at the end of a months-long rewrite&lt;/li&gt;
&lt;li&gt;the router can send a route back to the Angular version in minutes if something goes wrong&lt;/li&gt;
&lt;li&gt;new work carries on in whichever framework currently owns that route, so there's no feature freeze&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Set up the tooling
&lt;/h3&gt;

&lt;p&gt;Before a single feature moves, the new React project should build, test and deploy, even if all it contains is one empty page. For the todo app, that meant Vite [26], React 19 and TypeScript, with three scripts in &lt;code&gt;package.json&lt;/code&gt; that every change goes through: &lt;code&gt;npm run lint&lt;/code&gt;, &lt;code&gt;npm test&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt;. Not much, but it meant the React side was checked the same way from the first commit.&lt;/p&gt;

&lt;p&gt;For a production app, you'd want the full set in place before any real code lands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linting and formatting.&lt;/strong&gt; A linter (we used oxlint [29]; ESLint is the common alternative) plus a formatter like Prettier, so style never has to come up in review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type checking.&lt;/strong&gt; &lt;code&gt;tsc&lt;/code&gt; runs as part of the build, so a wrong prop or a missing field fails straight away instead of in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests.&lt;/strong&gt; Vitest [27] and Testing Library [28] for unit and component tests. For the side-by-side check in step 4, an end-to-end tool such as Playwright [30] can run the same user journey against both versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI.&lt;/strong&gt; Lint, type checks, tests and a build on every pull request, deployed somewhere real, ideally with a preview URL for each branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability.&lt;/strong&gt; Error tracking (Sentry, for example), Core Web Vitals and logs, all tagged by framework, so you can tell which side a problem came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature flags.&lt;/strong&gt; The switch [6] that later lets you send a route back to Angular. Far easier to add now than in the middle of an incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pipeline is the safety net everything later depends on. Retrofitting it once the real code exists is harder, and riskier.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Decide how old and new will live together
&lt;/h3&gt;

&lt;p&gt;In an incremental migration, Angular pages and React pages have to live on the same site at the same time, sometimes for months. So how do users move between them without noticing?&lt;/p&gt;

&lt;p&gt;This is where a &lt;strong&gt;micro frontend architecture&lt;/strong&gt; earns its keep. A thin shell application owns the routing decision and hands each route to whichever framework currently owns it [4]. Frameworks such as single-spa [5] exist to do exactly this wiring.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feli9zlzqbjaclct843ki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feli9zlzqbjaclct843ki.png" alt="A shell app owns the URL and delegates each route to an Angular remote (/invoices, /settings) or a React remote (/dashboard, /reports); routes move from Angular to React over time" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not every project needs this. A small app can do without it, and a full rewrite (like our todo app) skips this step entirely. It pays off in larger applications, with many teams and many screens, where the migration has to happen without stopping feature work.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Migrate from the bottom up
&lt;/h3&gt;

&lt;p&gt;Start with the smallest, most isolated pieces (a button, a form field, a single list item) and only move up once they're proven. In a real codebase, "smallest" isn't always obvious, and that's where a dependency graph helps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a dependency graph (a DAG) first&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before moving anything, map which pieces depend on which. Every component and service is a node, with an arrow from A to B whenever A uses B. Code shouldn't depend on itself in a loop, so the result is a &lt;strong&gt;directed acyclic graph&lt;/strong&gt;, or DAG, and the migration order falls straight out of it: start with the nodes that depend on nothing, and only move a node once everything it depends on has moved.&lt;/p&gt;

&lt;p&gt;Ours was small enough to draw by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AppComponent
 ├── uses → TodoItemComponent     (depends on nothing)
 └── uses → LocalStorageService   (depends on nothing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TodoItemComponent&lt;/code&gt; and &lt;code&gt;LocalStorageService&lt;/code&gt; depend on nothing, so they went first and became &lt;code&gt;TodoItem&lt;/code&gt; and &lt;code&gt;localStorageService.ts&lt;/code&gt;. &lt;code&gt;AppComponent&lt;/code&gt; depends on both, so it went last, once there was nothing left underneath it.&lt;/p&gt;

&lt;p&gt;For a larger app you won't draw this by hand. Tools such as madge [31] or dependency-cruiser [32] generate the graph from source, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx madge &lt;span class="nt"&gt;--image&lt;/span&gt; graph.svg &lt;span class="nt"&gt;--extensions&lt;/span&gt; ts src/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to watch for. If the tool finds a cycle, where A uses B and B also uses A, break it before migrating, usually by pulling the shared part into its own small module. And keep the graph up to date as you go. It doubles as a progress chart, with migrated nodes marked as done.&lt;/p&gt;

&lt;p&gt;Going top-down usually means touching everything at once, which throws away most of the safety you chose an incremental migration for.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Verify constantly, not only at the end
&lt;/h3&gt;

&lt;p&gt;After each piece moved, we asked two questions: does it behave the same, and does it look the same?&lt;/p&gt;

&lt;p&gt;Tests answer the first. We ported every Angular spec (Jasmine and Karma) to Vitest and React Testing Library, scenario for scenario, so coverage never dropped, and the React version finishes with all 13 tests passing. The second question needs a person. We ran both apps side by side in a browser, added, prioritised, completed and deleted todos in each, and compared screenshots.&lt;/p&gt;

&lt;p&gt;One small decision made that easier: both versions use the same localStorage keys (&lt;code&gt;todoList&lt;/code&gt; and &lt;code&gt;completeList&lt;/code&gt;) and the same data shape, so either app can read the other's todos in the same browser. It's a cheap trick, and worth copying.&lt;/p&gt;

&lt;p&gt;Don't save either check for the end. A problem found one piece later is cheap to fix. Fifty pieces later, it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deprecation and Rollback
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ferx3bgv9mrm3mpks57cp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ferx3bgv9mrm3mpks57cp.png" alt="Life of each piece: migrated to React, verified in production, old code kept but unused, old code removed, with a rollback path back to Angular until removal" width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When can the old code go? Not when the new code compiles, and not when the tests pass. For a production app, only after the React version has run under real traffic for a while without surprises.&lt;/p&gt;

&lt;p&gt;Each piece goes through the same four stages: migrated to React, verified in production, old code kept but unused, and finally old code removed. Until that last stage, keep a way back, whether that's a feature flag [6] or a route-level toggle that sends users to the Angular version if something slips past the tests.&lt;/p&gt;

&lt;p&gt;Keeping that path costs effort, and once everything looks fine it's tempting to rip it out early. Don't. The point of a rollback is that you don't know when you'll need it, and the one time you do, it pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Back
&lt;/h2&gt;

&lt;p&gt;The migration was never really about turning Angular syntax into React syntax. The harder and more useful part was understanding what the existing app actually did, finding the idea behind each Angular mechanism, and deciding how that idea should live in React.&lt;/p&gt;

&lt;p&gt;It got much easier once we stopped thinking of it as one big rewrite and started treating it as a series of small decisions. Understand what you have before changing anything. Map the concepts deliberately instead of copying the old architecture. Move in small pieces, and check each one before the next. And let go slowly: keep a way back until the new system has earned the trust to stand on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxywolgtbstuveflre5yv.png" alt="MigrateX" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MigrateX is our software and services platform for moving software, data and infrastructure from one technology to another. It brings automation and experienced engineers together, so the repetitive work gets done quickly and the tricky decisions get the human attention they need.&lt;/p&gt;

&lt;p&gt;Everything in this guide comes from how we run Angular to React projects with MigrateX. Here's what that looks like in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We scan your Angular app and map what it really relies on: components, services and the DI tree, two-way bindings, NgRx or service-based state, routes and guards, and third-party modules. You get a clear picture of where the effort really sits before anyone commits to a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration plan.&lt;/strong&gt; Together with your team, we choose between a full rewrite and an incremental move, decide what each Angular mechanism becomes in React, and build the dependency graph that sets the order screens move in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step-by-step migration.&lt;/strong&gt; Automation handles the repetitive parts, like converting templates to JSX, porting Jasmine specs to Vitest and setting up the tooling. Our engineers handle the parts that need judgement, like state design, replacing services and the shell that lets Angular and React share one site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; Every ported test passes, and your most important user journeys run as Playwright tests against both apps. A screen only counts as migrated when it passes on both, so progress is something you can see, not just something you're told.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover and clean-up.&lt;/strong&gt; We remove the shell routing and the last Angular packages, keep a way back until traffic is quiet, and leave your team with a React codebase they know and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What you get along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No big-bang release.&lt;/strong&gt; Your app keeps running and shipping features while the migration happens in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less risk.&lt;/strong&gt; Shared tests, feature flags and a way back mean users don't notice the move, and you can pause at any point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A team that's ready.&lt;/strong&gt; We pair with your developers throughout, so they're comfortable with React long before the last Angular screen is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More than front ends.&lt;/strong&gt; The same approach covers other frameworks, like Ember and Backbone, as well as data and infrastructure migrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;To see the code behind this guide, visit &lt;strong&gt;&lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;github.com/cobuild-tech/migratex-examples&lt;/a&gt;&lt;/strong&gt;. Planning a move from Angular, or already partway through? We'd love to hear about it. Tell us a little about your app through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free conversation about where the effort is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;migratex-examples&lt;/em&gt;: an Angular 14 todo app and its React 19 rewrite, with a README describing the migration. &lt;a href="https://github.com/cobuild-tech/migratex-examples/tree/main/angular-react" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/migratex-examples/tree/main/angular-react&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Migration strategy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;M. Fowler. &lt;em&gt;Strangler Fig.&lt;/em&gt; martinfowler.com, 22 August 2024 (first published 2004). &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/StranglerFigApplication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;J. Spolsky. &lt;em&gt;Things You Should Never Do, Part I.&lt;/em&gt; Joel on Software, 6 April 2000. &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;C. Jackson. &lt;em&gt;Micro Frontends.&lt;/em&gt; martinfowler.com, 19 June 2019. &lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/micro-frontends.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;single-spa. &lt;em&gt;Getting started overview&lt;/em&gt;: a framework for running several front-end frameworks in one app. &lt;a href="https://single-spa.js.org/docs/getting-started-overview" rel="noopener noreferrer"&gt;https://single-spa.js.org/docs/getting-started-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;P. Hodgson. &lt;em&gt;Feature Toggles (aka Feature Flags).&lt;/em&gt; martinfowler.com, 9 October 2017. &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/feature-toggles.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Angular documentation (v17)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Angular. &lt;em&gt;Introduction to the Angular docs.&lt;/em&gt; &lt;a href="https://v17.angular.io/docs" rel="noopener noreferrer"&gt;https://v17.angular.io/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Angular. &lt;em&gt;Template syntax.&lt;/em&gt; &lt;a href="https://v17.angular.io/guide/template-syntax" rel="noopener noreferrer"&gt;https://v17.angular.io/guide/template-syntax&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Angular. &lt;em&gt;Two-way binding&lt;/em&gt; (&lt;code&gt;[(ngModel)]&lt;/code&gt;). &lt;a href="https://v17.angular.io/guide/two-way-binding" rel="noopener noreferrer"&gt;https://v17.angular.io/guide/two-way-binding&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Angular. &lt;em&gt;Understanding dependency injection&lt;/em&gt; (including &lt;code&gt;providedIn: 'root'&lt;/code&gt;). &lt;a href="https://v17.angular.io/guide/dependency-injection" rel="noopener noreferrer"&gt;https://v17.angular.io/guide/dependency-injection&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Angular. &lt;em&gt;Built-in directives&lt;/em&gt; (&lt;code&gt;*ngFor&lt;/code&gt; and &lt;code&gt;trackBy&lt;/code&gt;). &lt;a href="https://v17.angular.io/guide/built-in-directives" rel="noopener noreferrer"&gt;https://v17.angular.io/guide/built-in-directives&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Angular. &lt;em&gt;Common Routing Tasks&lt;/em&gt; (the Angular router). &lt;a href="https://v17.angular.io/guide/router" rel="noopener noreferrer"&gt;https://v17.angular.io/guide/router&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;React documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React. &lt;em&gt;Getting Started&lt;/em&gt; (legacy docs). &lt;a href="https://legacy.reactjs.org/docs/getting-started.html" rel="noopener noreferrer"&gt;https://legacy.reactjs.org/docs/getting-started.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;Writing Markup with JSX.&lt;/em&gt; &lt;a href="https://react.dev/learn/writing-markup-with-jsx" rel="noopener noreferrer"&gt;https://react.dev/learn/writing-markup-with-jsx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;Rendering Lists&lt;/em&gt; (the &lt;code&gt;key&lt;/code&gt; prop). &lt;a href="https://react.dev/learn/rendering-lists" rel="noopener noreferrer"&gt;https://react.dev/learn/rendering-lists&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;&lt;/em&gt;: controlling an input with a state variable. &lt;a href="https://react.dev/reference/react-dom/components/input" rel="noopener noreferrer"&gt;https://react.dev/reference/react-dom/components/input&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;Sharing State Between Components&lt;/em&gt; (lifting state up). &lt;a href="https://react.dev/learn/sharing-state-between-components" rel="noopener noreferrer"&gt;https://react.dev/learn/sharing-state-between-components&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;Passing Data Deeply with Context.&lt;/em&gt; &lt;a href="https://react.dev/learn/passing-data-deeply-with-context" rel="noopener noreferrer"&gt;https://react.dev/learn/passing-data-deeply-with-context&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;Reusing Logic with Custom Hooks.&lt;/em&gt; &lt;a href="https://react.dev/learn/reusing-logic-with-custom-hooks" rel="noopener noreferrer"&gt;https://react.dev/learn/reusing-logic-with-custom-hooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;Updating Arrays in State.&lt;/em&gt; &lt;a href="https://react.dev/learn/updating-arrays-in-state" rel="noopener noreferrer"&gt;https://react.dev/learn/updating-arrays-in-state&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React. &lt;em&gt;State as a Snapshot.&lt;/em&gt; &lt;a href="https://react.dev/learn/state-as-a-snapshot" rel="noopener noreferrer"&gt;https://react.dev/learn/state-as-a-snapshot&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Libraries and tooling&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Router. &lt;a href="https://reactrouter.com/" rel="noopener noreferrer"&gt;https://reactrouter.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next.js documentation. &lt;a href="https://nextjs.org/docs" rel="noopener noreferrer"&gt;https://nextjs.org/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Redux. &lt;a href="https://redux.js.org/" rel="noopener noreferrer"&gt;https://redux.js.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zustand. &lt;a href="https://zustand.docs.pmnd.rs/" rel="noopener noreferrer"&gt;https://zustand.docs.pmnd.rs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vite. &lt;em&gt;Getting Started.&lt;/em&gt; &lt;a href="https://vite.dev/guide/" rel="noopener noreferrer"&gt;https://vite.dev/guide/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vitest. &lt;a href="https://vitest.dev/" rel="noopener noreferrer"&gt;https://vitest.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Testing Library. &lt;em&gt;React Testing Library.&lt;/em&gt; &lt;a href="https://testing-library.com/docs/react-testing-library/intro/" rel="noopener noreferrer"&gt;https://testing-library.com/docs/react-testing-library/intro/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Oxc. &lt;em&gt;Oxlint.&lt;/em&gt; &lt;a href="https://oxc.rs/docs/guide/usage/linter" rel="noopener noreferrer"&gt;https://oxc.rs/docs/guide/usage/linter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Playwright. &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;https://playwright.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;madge: dependency graphs for JavaScript and TypeScript. &lt;a href="https://github.com/pahen/madge" rel="noopener noreferrer"&gt;https://github.com/pahen/madge&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;dependency-cruiser: validate and visualise dependencies. &lt;a href="https://github.com/sverweij/dependency-cruiser" rel="noopener noreferrer"&gt;https://github.com/sverweij/dependency-cruiser&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;CobuildX AI blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>react</category>
      <category>ai</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
