AngularJS to React Migration: A Practical Playbook
A step-by-step AngularJS to React migration playbook: strategies, codemods, and how to find every affected component and migrate across repos at scale.

A step-by-step AngularJS to React migration playbook: strategies, codemods, and how to find every affected component and migrate across repos at scale.
Although your AngularJS app still works, that might not necessarily be for the best. The framework's support officially ended as of January 2022, the repository is archived, and every month it sits there, it accumulates security debt and hiring friction. This playbook is for frontend and platform engineers who own one or more of those apps and need to port them to React without freezing feature work or breaking production. Although these migrations can be difficult, the hardest step in an AngularJS-to-React migration isn't writing the React components. It's finding every place the old code lives and changing it consistently, especially once the work spans more than one repository.
The biggest reason to migrate is risk, not fashion. AngularJS 1.x is archived and will not get new security patches, so any CVE in the framework or its directive ecosystem is now your problem to mitigate. You can blunt that risk with isolation, dependency pinning, application-layer controls, or paid third-party long-term support, but the open-source project itself is no longer a safe long-term foundation. That alone pushes most teams off the fence.
The architectural reasons matter too, and they shape how hard the work will be. AngularJS is built on two-way data binding, where the model and view stay automatically synchronized through the digest cycle, though components since 1.5 favor one-way < bindings, so not every AngularJS codebase is purely $scope-driven. React went the other direction: data flows one way, and props are read-only snapshots a component cannot mutate, so when something needs to change, you set state instead. That single difference, two-way binding versus one-way flow, is the conceptual core of the whole rewrite. Most of the effort goes into translating implicit $scope synchronization into explicit state and props.
There's a practical payoff on the other side. Modern React components are typically JavaScript or TypeScript functions that return JSX, though class components are still supported. The ecosystem is large and actively maintained, and a modern build pipeline usually trims bundle size versus shipping the full AngularJS runtime and its dependencies. A smaller bundle size isn't guaranteed, though. You pay for the win in the migration effort, and the size of that bill depends entirely on how well you can see your own codebase.
There are three ways to run an Angular-to-React migration, and picking the wrong one is how you might end up being the creator of a cautionary tale. The big-bang rewrite is the seductive option and usually the riskiest: you build the React app in parallel, and two years later, the AngularJS version still has all the users and all the edge cases. Incremental and hybrid approaches win because each step is reversible and shippable.
React is designed for the incremental path. The official guidance is to add React to an existing stack and render components anywhere, starting with small interactive pieces and gradually moving upward until the whole page is React. That is the strangler-fig pattern applied to a frontend: the React surface grows while the AngularJS surface shrinks, route by route, until you can delete the last controller.
Here is how the three strategies actually compare in practice:
| Strategy | How it works | Risk | Best when |
|---|---|---|---|
| Big-bang rewrite | Build the full React app separately, cut over once | High. Long no-ship window, edge cases discovered late | The app is tiny, or already slated for a full redesign |
| Incremental (strangler) | Replace one route/component at a time; React grows, AngularJS shrinks | Low. Each slice ships and is reversible | Most production apps, especially revenue-critical ones |
| Hybrid coexistence | Run React and AngularJS on the same page during the transition | Medium. Two runtimes and a shared-state bridge to maintain | You need new React features to be live before the old app is gone |
For most teams, the real choice is incremental versus hybrid, and they are not mutually exclusive: hybrid coexistence is usually the mechanism that makes the incremental strategy possible. Our legacy code modernization guide goes deeper into strangler-fig sequencing if you are deciding how to slice the work.
Mainstream guidance simply says "inventory your application" and then moves on, as if that were a five-minute task. It is the part that quietly sinks migrations. You can't safely retire an AngularJS directive until you know every template and controller that references it, and on a real codebase, that answer isn't in anyone's head.
This is where the migration process stops being about React and starts being about visibility. An AngularJS application is held together by dependency injection, directives, and controllers that augment $scope, and each of those is a distinct thing you have to find and count. You need a complete, deterministic list of every .directive(), .controller(), .service(), and .component() registration, plus every place they are used in templates.
Sourcegraph Code Search is built for exactly this enumeration. It runs literal, keyword, and regex searches across every connected repo and branch in milliseconds, so a query like \.directive\(['\"] with patterntype:regexp returns the directive definitions in the org, and a follow-up search on each directive's HTML attribute name finds its usages in templates. Widen the same approach to .component(), .controller(), .service(), and .factory() registrations, and remember a text search can still miss directives registered through variables, TypeScript wrappers, or template-only usage. Symbol search locates the functions and services by name. The point isn't speed for its own sake; it's that the list is defensible and repeatable, and the degree of completeness depends on indexed repos, branch coverage, and query quality. When you tell leadership "there are 84 directives, and these 12 are unused," that number comes from the source of truth, not a stale architecture diagram.
For the fuzzier questions, Deep Search answers natural-language questions like "how does authentication flow through these controllers?" by running iterative searches over the code and Git history. Map the messy coupling with it first, then drop to deterministic Code Search to enumerate the exact areas in the code you will change. That two-step move, semantic exploration then exact enumeration, is the assessment step that some may skip over, but shouldn't.
Once you have the inventory, set up a hybrid environment so React and AngularJS can run on the same page while you migrate. The bridge most teams reach for is react2angular, which wraps a React component as an AngularJS component, allowing you to drop React into existing templates. It has not seen frequent updates recently, so confirm it works with your AngularJS version, and know the alternatives: route-level coexistence, a microfrontend boundary, or a thin wrapper component of your own around createRoot. Here is what using react2angular looks like, for example:
import { react2angular } from 'react2angular'
import UserBadge from './UserBadge' // a new React component
angular.module('app').component('userBadge', react2angular(UserBadge, ['userId', 'onSelect']))
<!-- now usable inside any existing AngularJS template -->
<user-badge user-id="$ctrl.id" on-select="$ctrl.handleSelect"></user-badge>
Two sharp edges are worth knowing before you commit. First, react2angular converts all React props to AngularJS one-way bindings, and functions must be passed as a function reference, not an invokable expression, or you get infinite digest loops. Second, you inject existing AngularJS services into the React component by passing them as the third argument, which is how you reuse $http or a config constant without rewriting it on day one.
You'll also need a build step you probably didn't have before. Some AngularJS apps load straight from a script tag, but many enterprise apps already run Webpack, Gulp, Grunt, RequireJS, or Bower, and JSX still has to be compiled. Adding a bundler like Vite with the Babel React preset is a good modern choice, though wiring it into a legacy build pipeline can be nontrivial. Stand it up once, and every React component you add from here flows through it.
With the bridge in place, the per-component work consists of mechanical translations. None is conceptually hard; the difficulty is doing each one the same way hundreds of times, which is the problem the next section solves.
Templates and directives. AngularJS directives are markers on DOM elements that tell the $compile service to attach behavior. In React, they become components that return JSX. ng-repeat becomes .map(), ng-if becomes a conditional, and an interpolation like {user.name} becomes {``user.name``}. Here is what that translation looks like between languages for something like a list originally populated by ng-repeat and ng-if:
<!-- AngularJS -->
<li ng-repeat="user in users" ng-if="user.active">{{ user.name }}</li>
// React
{users.filter(u => u.active).map(user => (
<li key={user.id}>{user.name}</li>
))}
The key needs to be stable and unique, so user.id works only if those IDs are guaranteed; fall back to another stable identifier rather than the array index.
Dependency injection. AngularJS resolves services through its injector subsystem. React components have no built-in DI container; you replace it with plain module imports, React context for cross-cutting concerns, and custom hooks for shared logic.
State. This is where two-way binding goes to die, in a good way. Local component state moves to the useState Hook, where state is read-only, and you replace it rather than mutate it. App-wide state management that used to live on $rootScope or shared services moves to a store like Redux, a separate library that works with React. Forms follow the same logic: AngularJS ng-model two-way bindings become controlled inputs whose value comes from state and whose onChange calls the setter.
Routing. React is a library, not a framework, and it does not ship routing; you add React Router or a framework on top. That replaces AngularJS routing, whether the built-in ngRoute module or the widely used UI-Router with its nested states and resolves, which mapped URL paths to controllers and views. Migrating nested UI-Router states to React Router can be one of the harder parts, so migrate routes at the same granularity as you migrate components, keeping each newly converted route end-to-end owned by React.
Although single-app tutorials can help conceptually, in a real organization, the same AngularJS pattern shows up in dozens of files and often across many repositories, and changing it by hand is where migrations stall. A codemod, a script that rewrites code via AST transforms (jscodeshift is the most common), handles repetitive edits across a codebase. The open question is how you run that transform everywhere and actually land the pull requests.
That is what Sourcegraph Batch Changes is for. You define a single declarative YAML batch spec; it runs your codemod across every matched repository, opens pull requests for each, and tracks each as it moves through review to merge. The repositories are selected using a Sourcegraph search query, so the same Code Search you used for the inventory serves as the targeting mechanism for the change. Here is an example of what that configuration might look like, to show how simple it can be:
version: 2
name: ngrepeat-to-react-map
on:
- repositoriesMatchingQuery: lang:HTML ng-repeat patterntype:keyword
steps:
- run: npx jscodeshift -t ./ngrepeat-to-map.js .
container: node:20
changesetTemplate:
title: Migrate ng-repeat templates to React
branch: migrate/ngrepeat
commit:
message: Convert ng-repeat usages to React .map()
published: false
jscodeshift targets JavaScript by default, so a template transform like this needs a codemod that understands AngularJS HTML, plus its dependencies available inside the container. You preview the whole fan-out before anything is published with src batch preview -f migrate.batch.yaml, then publish to create the actual pull requests on each code host. A burndown chart shows the proportion of changesets that have merged over time, turning "Are we done yet?" from a guess into a number. Compared to running a codemod repo-by-repo and chasing PRs in a spreadsheet, this is the difference between a coordinated rollout and 200 manual rollouts.
A migration without tests is a gamble, and the gamble compounds with every converted component. The good news is that the testing stack is one of the cleaner swaps. AngularJS apps were often tested with Karma and Jasmine, though many teams also used Protractor, Mocha, or Cypress. React unit tests move to Jest or Vitest, plus the React Testing Library, which queries the real DOM your component renders rather than its internals. End-to-end and regression coverage matter as much as unit tests.
Order matters here. Before you convert a slice, pin its current behavior with tests so any change in observable output surfaces immediately. Then migrate and confirm the React version meets the same expectations. Roll out behind the hybrid bridge route by route: ship the converted slice, watch error rates and latency, and keep the old path one config flip away where possible, since changes to state, schema, or data contracts can require feature flags or dual writes rather than a clean toggle. Because every changeset went through the same batch spec, the rollout is consistent across repos rather than subtly different in each. Track the shrinking AngularJS surface area with something like Code Insights so leadership can see the curve bending rather than trusting status updates.
The cross-repo problem this playbook keeps circling back to is not hypothetical. When Lyft decomposed its PHP monolith into microservices, the work spanned thousands of repositories. Lyft engineer Aneesh Agrawal said Sourcegraph gave them the ability to "search for and refactor references to deprecated services, libraries, URL patterns, and more across our 2000+ repositories" with the confidence that they were not leaving anyone behind. A multi-repo Angular-to-React migration is the same kind of problem: find every usage, change it consistently, and prove the old pattern is gone.
The ROI shows up as time, not magic. In the code-migration use case, many Sourcegraph users reported reducing the manual effort for large-scale code updates by up to 90%, and Workiva cut the time to make large-scale code changes by 80% once discovery and bulk changes ran through a single platform. This layer doesn't make the migration easy (since nothing ever completely does), but it removes the unknowns that made it unsafe, which lets you commit to a date and hit it.
An AngularJS-to-React migration is really two problems wearing one name. The visible problem is translating two-way binding into one-way React components, which is well understood and largely mechanical. The harder problem is doing it completely and consistently across a codebase that no longer fits in one person's head, and that's exactly where most guides go quiet.
Start with visibility. Map every directive, controller, and usage, drive the repetitive edits across repos, and track the AngularJS surface shrinking to zero. See how Sourcegraph threads search and Batch Changes through a coordinated code migration, then request a demo to walk through your own estate and scope the migration against real numbers instead of guesses.
How do I migrate from AngularJS to React? Pick an incremental strategy, set up a hybrid environment with react2angular so both run in the same page, inventory every directive and controller with code search, convert slices one at a time (templates to JSX, two-way binding to one-way state, ngRoute to React Router), apply repetitive edits with codemods plus Batch Changes, and roll out behind the bridge with tests pinning behavior at each step.
Is there a tool to convert AngularJS to React automatically? Codemods (such as jscodeshift) automate the repetitive, mechanical edits within a codebase, and AI-assisted converters exist for first drafts. Nothing replaces human review: the architectural shift from two-way binding to one-way data flow is judgment work, so treat automation as a force multiplier for engineers who know the app, not a substitute.
Can React and AngularJS run together during the migration? Yes. react2angular wraps a React component as an AngularJS component and converts its props to one-way bindings, letting you render React inside existing AngularJS templates while you migrate route by route.
Should I do a big-bang rewrite or migrate incrementally? Incremental wins for most production apps because each slice ships and is reversible. Reserve the big-bang rewrite for tiny apps or ones already headed for a full redesign.
How long does an AngularJS to React migration take? It depends on how many components you have and how many repositories they span. The variable that most moves the timeline is discovery: teams that can enumerate every affected component and apply changes across repos in coordinated batches finish far faster than teams that convert files one by one.

With Sourcegraph, the code understanding platform for enterprise.
Schedule a demo