AngularJS (the original 1.x framework, released in 2010) reached end of life on 31 December 2021. Since January 2022 it has received no security patches, no bug fixes, and no browser-compatibility updates from Google. If your app still runs on angular.module(...), $scope, and ng-controller, every unpatched CVE in AngularJS or its transitive dependencies is now permanent. This guide shows how to migrate to React incrementally — shipping the whole time — instead of freezing for a multi-quarter rewrite.
Why this can't wait
AngularJS's Long Term Support window closed at the end of 2021. There will never be another release. That matters for three concrete reasons:
- Security. Known issues — including sanitisation and expression-sandbox bypasses that lead to XSS — will not be fixed. You are also stuck on old, unaudited versions of jQuery/jqLite behaviours and build tooling.
- Compliance. Running EOL software fails most SOC 2, ISO 27001, and PCI controls that require supported, patched dependencies. Auditors flag it directly.
- Hiring and velocity. The AngularJS talent pool is shrinking every year, and its
$digest/dirty-checking model makes large apps slow and hard to reason about.
Not sure which of your stacks are already unsupported? Run them through the EOL checker first — it covers AngularJS alongside PHP, .NET, Python, Node and Laravel.
First: confirm it's AngularJS, not Angular
This trips up a lot of teams and it changes your entire plan. AngularJS (1.x) and Angular (2+) are different frameworks that happen to share a name.
- AngularJS 1.x — JavaScript,
angular.module(),$scope, controllers,ng-*directives in HTML, dirty-checking. This is the EOL framework this guide is about. - Angular 2 / 4 / … / 17+ — TypeScript, components and decorators, RxJS, its own CLI. Actively supported.
ngUpgrade does not apply to you. Google's @angular/upgrade (ngUpgrade / UpgradeModule) lets AngularJS and Angular 2+ run in the same page during an AngularJS → Angular migration. It is Angular-specific and has no React equivalent. If your target is React, ignore every "use ngUpgrade" tutorial — you'll build your own bridge instead (covered below).
The strategy: strangler-fig, not big-bang
The single biggest predictor of a failed migration is the decision to stop feature work and rewrite the app in one shot. Requirements drift, the new app never reaches parity, and the business loses patience. Use the strangler-fig pattern instead: stand React up alongside the running AngularJS app, migrate one feature at a time, and let React gradually "strangle" the old code until nothing AngularJS remains. Both frameworks run in production together for weeks or months — and that's fine.
Rules that keep this honest:
- Never freeze the codebase. Business-critical fixes still ship on AngularJS while migration proceeds around them.
- Migrate leaves before branches. Start with self-contained UI (a badge, a date picker, a table cell), not the shell or the router.
- Every merge leaves the app fully working. No long-lived migration branch.
- Delete AngularJS last. The old dependency comes out only when the final directive is gone.
Pick how the two frameworks co-run
There is no single right bridge — choose per app based on size, routing, and team structure.
| Approach | How it works | Best when | Trade-off |
|---|---|---|---|
| Wrapper directive (React-in-AngularJS) | An AngularJS directive mounts a React component into its element with ReactDOM.createRoot, passing $scope values as props. | One existing app you want to convert component-by-component in place. The default choice. | You hand-write the bridge and manage two render loops at the seam. |
| single-spa micro-frontends | A root config registers an AngularJS app and a React app as separate bundles mounted by route/DOM region (single-spa-angularjs, single-spa-react). | Larger apps, multiple teams, or you want independent deploys per framework. | More upfront orchestration, shared-dependency and build config. |
| Route split (nginx / proxy) | A reverse proxy sends some URL paths to the legacy AngularJS app and others to a new React SPA. No in-page interop. | Clear route boundaries (e.g. /admin vs /app) and you can tolerate a full page load at the seam. | Duplicated app shell/auth; no shared in-memory state across the boundary. |
Many teams combine them: route-split the big sections, and use a wrapper directive to migrate the interior of each section.
How AngularJS concepts map to React
Keep this table next to you while porting. It's the mental model that makes the code changes mechanical.
| AngularJS | React |
|---|---|
Controller + $scope | Function component + useState |
| Directive | Component |
| Service / factory (singleton) | Module export, custom hook, or Context provider |
$http | fetch / axios (in useEffect or a data library) |
ng-repeat | array.map() in JSX (with a key) |
ng-if / ng-show | Conditional JSX ({cond && …} / ternary) |
$routeProvider / ui-router | React Router |
Two-way binding (ng-model) | Controlled input (value + onChange) |
$scope.$watch | useEffect with a dependency array |
$scope.$on('$destroy') | useEffect cleanup function |
Step by step
1. Add a React build (Vite) alongside AngularJS
Don't touch the AngularJS build. Add a second, modern build with Vite that emits a bundle the existing page can load. Scaffold it, then during the transition expose your React components on window so AngularJS templates can reach them.
npm create vite@latest react-app -- --template react
cd react-app && npm install
Load the compiled bundle from the AngularJS index.html (or inject it via your existing bundler). Configure Vite to output a predictable filename so the legacy page can reference it.
2. Establish a bridge
For the default wrapper-directive approach, write one reusable directive that mounts any React component. This is the piece ngUpgrade would have given you for Angular — for React you own it, and it's about 20 lines.
// react-component.directive.js (AngularJS side)
angular.module('app').directive('reactComponent', function () {
return {
restrict: 'E',
scope: { component: '<', props: '<' },
link: function (scope, element) {
const root = ReactDOM.createRoot(element[0]);
function render() {
root.render(React.createElement(scope.component, scope.props || {}));
}
scope.$watch('props', render, true); // re-render when props change
scope.$on('$destroy', function () { root.unmount(); }); // avoid leaks
}
};
});
Now any AngularJS template can host React: <react-component component="UserBadge" props="{userId: user.id}"></react-component>. Going the other direction (AngularJS inside React) is rarely worth it — migrate leaf-up so React is always the child.
3. Migrate leaf components first, then containers
Port the smallest, most self-contained pieces first. A controller becomes a component; $scope fields become useState. Here's the canonical shape of the change:
// AngularJS: controller + template
angular.module('app').controller('CounterCtrl', function ($scope) {
$scope.count = 0;
$scope.increment = function () { $scope.count++; };
});
// template: <button ng-click="increment()">+</button> <span>{{count}}</span>
// React: one function component
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>+</button>
<span>{count}</span>
</>
);
}
Template directives translate directly. Lists and conditionals stop being magic attributes and become plain JavaScript:
<li ng-repeat="u in users" ng-if="u.active">{{u.name}}</li>
{users.filter(u => u.active).map(u => (
<li key={u.id}>{u.name}</li>
))}
Two-way binding becomes an explicit controlled input — more code, but the data flow is finally one-directional and debuggable:
<input ng-model="name">
<input value={name} onChange={e => setName(e.target.value)} />
4. Share state at the seam during transition
While both frameworks are live, they need to agree on data. Convert AngularJS singletons (services/factories) into a plain module or React Context, and expose read/write access to both worlds. The cleanest bridge is a framework-agnostic store (a tiny event emitter, Zustand, or Redux) that AngularJS services write to and React components subscribe to via a hook. Also replace $http with fetch/axios as you port — and note the semantics differ:
$http.get('/api/users').then(function (res) {
$scope.users = res.data; // Angular auto-runs a digest
});
useEffect(() => {
fetch('/api/users')
.then(r => r.json()) // fetch does NOT reject on 4xx/5xx — check r.ok
.then(setUsers);
}, []);
5. Move routes one by one
Once enough of a screen is React, flip its route. Introduce React Router for migrated sections and let the AngularJS router keep the rest, or use your reverse proxy to hand specific paths to the React SPA. Migrate route-by-route so the URL contract to users never changes.
6. Delete AngularJS dependencies last
When the final directive, service, and route are gone, remove angular, angular-route/ui-router, and the wrapper directive from the build. Only now do you drop the second build step and collapse everything into the Vite pipeline. Removing AngularJS earlier just to "feel done" is how apps end up half-migrated forever.
7. Test continuously
The seam is where regressions hide — every mount/unmount boundary and every shared-state write is a risk. Keep (or add) end-to-end tests (Playwright/Cypress) that exercise whole user journeys across the boundary, and add React Testing Library tests for each ported component as you go. Green E2E after every merge is what makes "never freeze" safe.
Watch the two render loops. While a React component lives inside an AngularJS directive, AngularJS's $digest and React's render cycle are independent. Mutating $scope from a React callback needs a $scope.$apply() (or $timeout) to be picked up, and forgetting root.unmount() on $destroy leaks memory and detached DOM. Centralising both in the single wrapper directive from step 2 keeps this in one auditable place.
How Mira does this for you
Everything above is the manual path. Mira, Codsy's AI modernization pipeline, automates the mechanical bulk of it — inventorying directives/controllers/services, generating equivalent React components with the concept mapping applied, wiring the wrapper/bridge, and producing tests at the seam — while you keep shipping. It's the same strangler-fig approach, just faster. If you'd rather not hand-port hundreds of controllers, that's what it's for.
Frequently asked questions
Is AngularJS end of life?
Yes. AngularJS 1.x reached end of life on 31 December 2021; its Long Term Support ended then and no further releases — including security patches — are made. As of 2026 it has been unsupported for over four years. Continuing to run it in production is a known, growing security and compliance risk.
What's the difference between AngularJS and Angular?
They're different frameworks with a confusingly similar name. AngularJS (1.x) is the original JavaScript framework built on $scope, controllers, and ng-* directives — and it's EOL. Angular (2 and above) is a complete, separate rewrite in TypeScript using components, decorators and RxJS, and it is actively supported. Migrating "off Angular" means very different work depending on which one you actually have.
Can I use ngUpgrade to migrate to React?
No. ngUpgrade (@angular/upgrade) is Google's official bridge for running AngularJS and Angular 2+ together during an Angular migration. It has no knowledge of React and cannot mount React components. To co-run AngularJS and React you build your own bridge — typically the ~20-line wrapper directive shown in step 2, or a micro-frontend framework like single-spa.
Should I rewrite from scratch or migrate incrementally?
Migrate incrementally in almost every real-world case. A big-bang rewrite means freezing feature work, chasing a moving parity target, and betting the business on a single cutover — the classic way these projects die. The strangler-fig approach keeps the app shipping the entire time and lets you stop, reprioritise, or ship value at any point. A full rewrite is only defensible for a very small app, or one whose architecture is so broken that porting it forward would just carry the rot into React.
How long does an AngularJS-to-React migration take?
It scales with the number of directives, controllers and services rather than lines of code, and with how much shared global state exists at the seams. Because the strangler-fig approach ships continuously, the more useful question isn't "when is it 100% done" but "how fast are we retiring AngularJS surface area each sprint" — which you can measure and forecast from week one.
Which co-running approach should I choose?
Default to the wrapper directive for a single app you're converting in place. Reach for single-spa when multiple teams or independent deploys are involved, and for a route split (nginx/proxy) when your app already has clean URL boundaries and a full page load at the seam is acceptable. They combine well — route-split the big sections and use the wrapper directive inside each.