How to Migrate an App to a Modern Framework in 5 Steps
To migrate an app to a modern open source framework, don't rewrite it from scratch. Run the new framework next to the old code, move one page or component at a time, and keep shipping while you do it. This is the "strangler fig" approach Martin Fowler describes: the new system grows around the old one until the old one can be deleted.
This guide covers the five steps that work for the common cases in October 2026: jQuery or server-rendered pages moving to React or Vue, Create React App (CRA) moving to Vite or Next.js, AngularJS moving to Angular, and Vue 2 moving to Vue 3.
Why migrate now
Several popular stacks no longer get updates:
- Create React App was deprecated for new apps on February 14, 2025. The React team recommends moving to a framework or to a build tool such as Vite, Parcel or Rsbuild.
- AngularJS (1.x) support ended on December 31, 2021 (angularjs.org).
- Vue 2 reached end of life on December 31, 2023 (v2.vuejs.org/eol).
Unmaintained frameworks stop getting security fixes, and new libraries stop supporting them. "It still works" is true until a dependency you need drops support.
Pick your migration path
| Coming from | Good target | How to run old and new side by side |
|---|---|---|
| jQuery or server-rendered pages | React, Vue, Svelte | Mount new components into existing pages ("islands") |
| Create React App | Vite (SPA) or Next.js | Swap the build tool first, keep the code |
| AngularJS 1.x | Angular | Hybrid app with the @angular/upgrade package |
| Vue 2 | Vue 3 | The migration build (@vue/compat) |
| Any whole site | Next.js, Astro, Nuxt | Proxy unmigrated routes to the legacy app |
Pick the target your team can hire for and support. A framework you already know half of beats a "better" one nobody on the team has used.
Step 1: Audit the app and lock in its behavior
Before you change anything, write down what the app does and make that testable.
- List every route, the data it loads, and who owns it.
- Find the shared pieces: auth, global state, API client, design tokens, analytics.
- Write end-to-end tests for the flows that make money or would cause support tickets: sign in, checkout, the main form. A tool like Playwright or Cypress is fine. These "characterization tests" describe the current behavior, bugs included, so you can prove the migrated version matches.
- Record a baseline: bundle size, Core Web Vitals and error rate. You'll want numbers to show the migration helped.
Step 2: Set up the modern toolchain
Upgrade the tooling before the framework. Often this alone fixes slow builds.
For a CRA app, moving to Vite is usually a one-day job:
- Remove
react-scriptsand install Vite:npm rm react-scripts && npm i -D vite @vitejs/plugin-react. - Add
vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});
- Move
public/index.htmlto the project root, remove the%PUBLIC_URL%placeholders, and add your entry script:<script type="module" src="/src/index.jsx"></script>. - Rename files that contain JSX from
.jsto.jsx(or.tsx). Vite expects JSX in those extensions. - Rename environment variables from
REACT_APP_*toVITE_*and read them withimport.meta.env.VITE_API_URLinstead ofprocess.env. - Update scripts:
"dev": "vite","build": "vite build","preview": "vite preview".
If you want server rendering and file-based routing, follow the official Next.js guide for migrating from CRA. It starts by running your app as a client-side SPA inside Next.js, then adopts Next.js features one route at a time.
Step 3: Run old and new side by side
This is the step that makes the migration safe. There are two ways to do it.
Option A: mount new components into old pages
React and Vue can take over a single DOM node on a page they don't otherwise control. React's docs call this adding React to an existing project. The legacy page keeps a placeholder element:
<div id="cart-widget" data-count="3"></div>
And the new bundle renders into it:
import { createRoot } from 'react-dom/client';
import CartWidget from './CartWidget';
const el = document.getElementById('cart-widget');
if (el) {
createRoot(el).render(<CartWidget initialCount={Number(el.dataset.count)} />);
}
Start with leaf components (a widget, a form, a table), then work up to whole pages.
Option B: route traffic at the edge
Put the new app in front and send anything it doesn't handle yet to the legacy app. In Next.js, a fallback rewrite does this: Next.js checks its own routes first and proxies everything else (rewrites docs):
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async rewrites() {
return {
fallback: [
{ source: '/:path*', destination: 'https://legacy.example.com/:path*' },
],
};
},
};
export default nextConfig;
Each page you rebuild in the new app takes over its URL automatically. Vercel documents the same pattern for incremental migrations. Any reverse proxy (Nginx, Cloudflare, a load balancer) can do the same routing if you're not on Next.js.
Step 4: Migrate feature by feature
Move one feature per pull request and ship each one.
- Share state through a boundary, not globals. Put the API client and auth in a small module both old and new code import. For cross-framework events, a
CustomEventonwindowis simple and works everywhere. - Move styles with the feature. Keep the old global CSS loaded until the last legacy page is gone. Scope new styles (CSS Modules, Tailwind or component styles) so they can't break old pages.
- Rerun the Step 1 tests on every merge. If a characterization test fails, either fix the new code or consciously decide the old behavior was a bug.
- Use the framework's own migration aids. Vue's migration build logs every Vue 2 API you still use. AngularJS-to-Angular hybrids let services and components be "upgraded" or "downgraded" one at a time.
- Track progress with a simple count: routes or components still on the old stack. It keeps the migration from stalling at 80%.
Step 5: Delete the old code and lock the door
A migration isn't done until the legacy framework is out of the bundle.
- Remove the old framework, its polyfills and its build config.
- Remove the proxy fallback or the island mount points.
- Add a lint rule or CI check that blocks imports of the removed library (ESLint's
no-restricted-importsworks). - Compare against your Step 1 baseline: bundle size, Web Vitals and error rate.
- Set up automated dependency updates (Dependabot or Renovate) so you don't end up here again.
Honest trade-offs
- Two frameworks cost bytes. While old and new coexist, users download both. Migrate high-traffic pages first so the overlap period is short where it matters.
- Shared state is the hard part. Most migration bugs come from auth, sessions and global stores, not from components.
- SEO risk on route changes. Keep URLs identical, or add 301 redirects, and check that server-rendered pages return the same titles and canonical tags.
- Big-bang rewrites rarely pay off. They freeze feature work for months and usually ship with regressions the old code had already fixed.
- AI coding tools help with the mechanical parts (converting class components, renaming APIs, writing tests), but review every diff. See our vibe coding guide for where these tools work well and where they don't.
If you're still choosing a target, our post on top open source frameworks compares the main options, and Astro vs Next.js covers content-heavy sites. For the testing side, browse testing tools on DevHunt.
FAQ
Should I rewrite my app from scratch or migrate incrementally?
Migrate incrementally in almost every case. A rewrite only makes sense for small apps (days or a few weeks of work) or when the product itself is changing so much that the old behavior doesn't matter.
How long does a framework migration take?
It depends on the size of the app and how much test coverage you have. Swapping CRA for Vite often takes a day. Moving a large AngularJS app to Angular can take many months. Counting routes and components left on the old stack is the most reliable way to estimate the rest.
Does Create React App still work?
Existing CRA apps still build, but CRA has no active maintainers and was deprecated in February 2025. Move to Vite or a framework like Next.js or React Router before a dependency breaks.
Can I mix React and Vue in the same app during a migration?
Yes. Each framework can mount into its own DOM node on the same page. It works as a transition step, but don't leave it that way: you pay for two runtimes and two sets of tooling.
Which framework should I migrate to?
Choose based on your team and your app: React (with Next.js or Vite) has the largest ecosystem, Vue is the natural step from Vue 2 or jQuery-style code, Angular fits teams coming from AngularJS, and Astro suits content sites that need little JavaScript.