How to Migrate from AngularJS to React

Type: Software Reference Confidence: 0.92 Sources: 8 Verified: 2026-02-22 Freshness: quarterly

TL;DR

Constraints

Quick Reference

AngularJS PatternReact EquivalentExample
$scope / $rootScopeuseState / useContext / Redux storeconst [count, setCount] = useState(0)
ng-model (two-way binding)Controlled component (value + onChange)<input value={val} onChange={e => setVal(e.target.value)} />
ng-repeatArray.map() in JSX{items.map(item => <Item key={item.id} {...item} />)}
ng-if / ng-showConditional rendering / CSS toggle{show && <Component />}
ng-clickonClick handler<button onClick={handleClick}>Go</button>
ng-classclassName with template literal or clsxclassName={clsx('btn', {active: isActive})}
Directive (restrict: 'E')Functional componentfunction MyWidget({ title }) { return <h2>{title}</h2>; }
Service / Factory (DI)Module import / Context / custom hookimport { apiClient } from './services/api'
$http / $resourcefetch / axios / React Query / TanStack Queryconst { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
$watch / $watchCollectionuseEffect with dependency arrayuseEffect(() => { ... }, [value])
$timeout / $intervalsetTimeout / setInterval + useEffect cleanupuseEffect(() => { const id = setInterval(fn, 1000); return () => clearInterval(id); }, [])
resolve (route pre-fetch)Loader function (React Router 6) / useEffectloader: async () => fetch('/api/data')
Filters ({{ val | currency }})Helper functions called in JSX{formatCurrency(val)}
angular.module('app', [deps])ES module imports + <App /> rootimport App from './App'; createRoot(el).render(<App />)
$emit / $broadcast (event bus)Callback props / Context / state management<Child onUpdate={handleUpdate} />

Decision Tree

START
|-- Is your AngularJS app < 5,000 LOC?
|   |-- YES --> Consider a clean rewrite in React (faster than incremental migration)
|   +-- NO |
|-- Does your app use AngularJS 1.5+ component API?
|   |-- YES --> Use react2angular for bridge (see Step 2)
|   +-- NO |
|-- Does your app use only directives (restrict: 'E', 'A')?
|   |-- YES --> Use ngReact or angular2react for bridge (see Step 2, Alternative)
|   +-- NO |
|-- Is your app under active feature development?
|   |-- YES --> Incremental migration: new features in React, bridge existing
|   +-- NO |
|-- Is the app in maintenance mode with security-only updates?
|   |-- YES --> Consider HeroDevs NES while planning migration [src7]
|   +-- NO |
|-- Is PCI DSS 4.0 or FedRAMP compliance required?
|   |-- YES --> Migrate urgently — EOL frameworks banned without verified mitigation [src8]
|   +-- NO |
+-- DEFAULT --> Incremental bottom-up migration with react2angular + shared Redux store

Step-by-Step Guide

1. Set up the React build pipeline alongside AngularJS

Add React and its build tooling to your existing project without removing any AngularJS code. Use your existing bundler (webpack, rollup) or add one if you only use script tags. [src1, src5]

# Install React core + bridge library
npm install react react-dom react2angular prop-types

# Install build tooling (if not already present)
npm install --save-dev @babel/preset-react webpack webpack-cli babel-loader

# For TypeScript projects
npm install --save-dev typescript @types/react @types/react-dom

Add JSX support to your webpack/babel config:

// webpack.config.js — add to module.rules
{
  test: /\.(js|jsx|tsx?)$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env', '@babel/preset-react']
    }
  }
}

Verify: npx webpack --mode development builds without errors, and your existing AngularJS app still works unchanged.

2. Create the React-AngularJS bridge

Register React components as AngularJS directives using react2angular. This is the core mechanism that allows both frameworks to coexist. [src4, src6]

// bridge.js — Register React components as Angular directives
import { react2angular } from 'react2angular';
import angular from 'angular';
import { UserCard } from './components/UserCard';

angular
  .module('myApp')
  .component('userCard', react2angular(UserCard, ['user', 'onEdit']));
// Props are passed as attributes:
// <user-card user="$ctrl.user" on-edit="$ctrl.handleEdit">

For apps using the older directive API instead of .component(), use ngReact:

// Alternative: ngReact bridge for directive-based apps (AngularJS < 1.5)
import 'ngreact';
import { UserCard } from './components/UserCard';

angular
  .module('myApp', ['react'])
  .value('UserCard', UserCard)
  .directive('userCard', function(reactDirective) {
    return reactDirective('UserCard', ['user', 'onEdit']);
  });

Verify: Add <user-card user="$ctrl.someUser"></user-card> to any AngularJS template. The React component should render within the AngularJS page.

3. Establish shared state management with Redux

Set up a Redux store that both AngularJS and React components can access. This prevents state divergence during the migration period. [src4, src5]

// store.js — Shared Redux store
import { configureStore, createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'users',
  initialState: { list: [], loading: false },
  reducers: {
    setUsers: (state, action) => { state.list = action.payload; },
    setLoading: (state, action) => { state.loading = action.payload; }
  }
});

export const { setUsers, setLoading } = userSlice.actions;
export const store = configureStore({ reducer: { users: userSlice.reducer } });
// Connect AngularJS to the shared Redux store using ng-redux
import ngRedux from 'ng-redux';
import { store } from './store';

angular.module('myApp', [ngRedux])
  .config(($ngReduxProvider) => {
    $ngReduxProvider.provideStore(store);
  });

Verify: Dispatch an action from an AngularJS controller and confirm the React component re-renders with updated data.

4. Convert leaf components bottom-up

Start with the simplest, most isolated components at the bottom of your component tree and work upward. This minimizes risk and lets you build confidence. [src2, src4]

// BEFORE: AngularJS directive
angular.module('myApp').directive('statusBadge', function() {
  return {
    restrict: 'E',
    scope: { status: '=' },
    template: '<span class="badge badge-{{status}}">{{status}}</span>'
  };
});

// AFTER: React component
function StatusBadge({ status }) {
  return <span className={`badge badge-${status}`}>{status}</span>;
}

// Register as Angular directive via bridge
angular.module('myApp')
  .component('statusBadge', react2angular(StatusBadge, ['status']));

Conversion priority order: (1) pure display components, (2) form inputs, (3) list items/cards, (4) containers with data fetching, (5) page-level components, (6) router (last).

Verify: Run your existing test suite after each component conversion. Both AngularJS and converted React components should behave identically.

5. Migrate routing last

Once the majority of your components are React, replace the AngularJS router with React Router. This is the most disruptive step and should be done last. [src4, src5]

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Dashboard } from './pages/Dashboard';
import { UserProfile } from './pages/UserProfile';
import { Settings } from './pages/Settings';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/users/:id" element={<UserProfile />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </BrowserRouter>
  );
}

Verify: Navigate to every route and verify all pages render correctly. Check that browser back/forward navigation and deep links work.

6. Remove AngularJS entirely

Once all components and routing are in React, remove the AngularJS dependency and all bridge code. [src2, src5]

# Remove AngularJS and bridge dependencies
npm uninstall angular angular-route angular-resource ngreact react2angular ng-redux
// New entry point: index.jsx
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';

const root = createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

Verify: npm ls angular returns empty (no AngularJS in dependency tree). Run full test suite. Bundle size should be significantly smaller.

Code Examples

JavaScript: Wrapping a React Component for AngularJS with react2angular

Full script: javascript-wrapping-a-react-component-for-angularj.js (46 lines)

// Input:  A React component + AngularJS module
// Output: The React component available as an AngularJS directive

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import angular from 'angular';

// Step 1: Create a standard React component
function SearchBox({ placeholder, onSearch, debounceMs = 300 }) {
  const [query, setQuery] = useState('');

  useEffect(() => {
    const timer = setTimeout(() => {
      if (query.length >= 2) {
        onSearch(query);
      }
    }, debounceMs);
    return () => clearTimeout(timer);
  }, [query, debounceMs, onSearch]);

  return (
    <div className="search-box">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder={placeholder}
      />
      {query && (
        <button onClick={() => { setQuery(''); onSearch(''); }}>
          Clear
        </button>
      )}
    </div>
  );
}

SearchBox.propTypes = {
  placeholder: PropTypes.string,
  onSearch: PropTypes.func.isRequired,
  debounceMs: PropTypes.number
};

// Step 2: Register as AngularJS component via bridge
angular
  .module('myApp')
  .component('searchBox', react2angular(SearchBox, ['placeholder', 'onSearch', 'debounceMs']));

// Step 3: Use in AngularJS template
// <search-box placeholder="'Search users...'" on-search="$ctrl.handleSearch"></search-box>
// NOTE: Attribute names use kebab-case; react2angular maps to camelCase props.

TypeScript: Migrating an AngularJS Service to a React Custom Hook

Full script: typescript-full-migration-of-an-angularjs-service-.ts (81 lines)

// Input:  AngularJS service with $http calls and state management
// Output: React custom hook with identical functionality

// BEFORE: angular.module('myApp').factory('UserService', function($http) { ... });
// AFTER:

import { useState, useCallback } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface UseUsersReturn {
  users: User[];
  loading: boolean;
  error: string | null;
  fetchUsers: () => Promise<void>;
  updateUser: (id: string, data: Partial<User>) => Promise<void>;
  deleteUser: (id: string) => Promise<void>;
}

export function useUsers(): UseUsersReturn {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchUsers = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch('/api/users');
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const data: User[] = await response.json();
      setUsers(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to fetch users');
    } finally {
      setLoading(false);
    }
  }, []);

  const updateUser = useCallback(async (id: string, data: Partial<User>) => {
    setError(null);
    try {
      const response = await fetch(`/api/users/${id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const updated: User = await response.json();
      setUsers(prev => prev.map(u => u.id === id ? updated : u));
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to update');
      throw err;
    }
  }, []);

  const deleteUser = useCallback(async (id: string) => {
    setError(null);
    try {
      const response = await fetch(`/api/users/${id}`, { method: 'DELETE' });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      setUsers(prev => prev.filter(u => u.id !== id));
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to delete');
      throw err;
    }
  }, []);

  return { users, loading, error, fetchUsers, updateUser, deleteUser };
}

JavaScript: Shared Redux Store Bridge Between AngularJS and React

Full script: javascript-shared-redux-store-bridge-between-angul.js (50 lines)

// Input:  Redux store accessed by both AngularJS controllers and React components
// Output: Synchronized state across both frameworks during migration

import { configureStore, createSlice } from '@reduxjs/toolkit';

// 1. Define Redux slice (shared by both frameworks)
const filtersSlice = createSlice({
  name: 'filters',
  initialState: { searchQuery: '', category: 'all', sortBy: 'name' },
  reducers: {
    setSearchQuery: (state, action) => { state.searchQuery = action.payload; },
    setCategory: (state, action) => { state.category = action.payload; },
    setSortBy: (state, action) => { state.sortBy = action.payload; }
  }
});

export const { setSearchQuery, setCategory, setSortBy } = filtersSlice.actions;
export const store = configureStore({ reducer: { filters: filtersSlice.reducer } });

// 2. AngularJS side: connect controller to Redux via ng-redux
function FilterBarCtrl($ngRedux, $scope) {
  const mapState = (state) => ({
    searchQuery: state.filters.searchQuery,
    category: state.filters.category
  });
  const mapDispatch = { setSearchQuery, setCategory };
  const unsubscribe = $ngRedux.connect(mapState, mapDispatch)($scope);
  $scope.$on('$destroy', unsubscribe);

  // Double-write pattern during transition
  $scope.updateSearch = function(query) {
    $scope.setSearchQuery(query);             // Redux (new)
    LegacyFilterService.setSearch(query);      // Angular service (remove later)
  };
}

// 3. React side: read from same Redux store
import { useSelector } from 'react-redux';

function SearchResults() {
  const { searchQuery, category } = useSelector(state => state.filters);
  return <p>Results for "{searchQuery}" in {category}</p>;
}

Anti-Patterns

Wrong: Big-bang rewrite of the entire application

// BAD — Stopping all feature development for months to rewrite everything
// This approach has a high failure rate and massive business risk [src2, src5]

// "Let's just rewrite the whole thing in React over the next 6 months"
// Meanwhile: no new features, no bug fixes, team morale drops,
// stakeholders lose patience, project gets cancelled

Correct: Incremental migration with continuous delivery

// GOOD — Migrate one component at a time while shipping features [src2, src4]
// Week 1: Set up React + bridge library
// Week 2: Convert 3 leaf components (badges, icons, tooltips)
// Week 3: Ship a new feature — write it in React from the start
// Week 4: Convert 2 more existing components
// ...continue until AngularJS is fully removed

Wrong: Duplicating state between AngularJS and React

// BAD — Each framework maintains its own copy of the same data

// AngularJS controller
$scope.users = UserService.getAll();    // AngularJS has its own copy

// React component
function UserList() {
  const [users, setUsers] = useState([]);  // React has its own copy
  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers);
  }, []);
  // BUG: AngularJS and React show different data after updates
}

Correct: Single source of truth via shared Redux store

// GOOD — One Redux store is the single source of truth [src4, src5]

// AngularJS reads from Redux via ng-redux
const unsubscribe = $ngRedux.connect(
  state => ({ users: state.users.list })
)($scope);

// React reads from Redux via react-redux
function UserList() {
  const users = useSelector(state => state.users.list);
  // Both always show the same data
}

Wrong: Converting the router first

// BAD — Replacing the router breaks every route at once [src4]
// This forces you to convert ALL page components simultaneously

// "Let's start by switching to React Router..."
// Result: Every page breaks. You must convert everything before anything works.

Correct: Convert the router last

// GOOD — Convert leaf components and pages first, router last [src4, src5]
// 1. Convert UI components (buttons, cards, forms)
// 2. Convert page-level components
// 3. Only THEN replace the AngularJS router with React Router
// At step 3, all pages are already React — the router swap is straightforward

Wrong: Direct DOM manipulation in React components

// BAD — jQuery or direct DOM manipulation inside React [src1]
function BadComponent() {
  useEffect(() => {
    $('#my-element').addClass('active');
    document.querySelector('.sidebar').innerHTML = '<p>Updated</p>';
  }, []);
  return <div id="my-element">Content</div>;
}

Correct: Let React manage its own DOM, isolate legacy DOM access

// GOOD — Use React state and refs; isolate non-React DOM [src1]
function GoodComponent({ isActive }) {
  return <div className={isActive ? 'active' : ''}>Content</div>;
}

// For legacy jQuery plugins, isolate them:
function LegacyPluginWrapper() {
  const containerRef = useRef(null);
  useEffect(() => {
    $(containerRef.current).datepicker({ format: 'yyyy-mm-dd' });
    return () => $(containerRef.current).datepicker('destroy');
  }, []);
  return <div ref={containerRef}></div>;
}

Common Pitfalls

Diagnostic Commands

# Check AngularJS version in your project
npm ls angular | head -5

# Check if react2angular bridge is installed
npm ls react2angular

# Count remaining AngularJS module declarations (migration progress)
grep -r "angular\.module\(" src/ --include="*.js" --include="*.ts" | wc -l

# Count remaining AngularJS directives
grep -r "\.directive\(" src/ --include="*.js" --include="*.ts" | wc -l

# Count remaining AngularJS controllers
grep -r "\.controller\(" src/ --include="*.js" --include="*.ts" | wc -l

# Count remaining $scope usage (unconverted code indicator)
grep -r "\$scope" src/ --include="*.js" --include="*.ts" | wc -l

# Analyze bundle size to track bloat during migration
npx webpack-bundle-analyzer dist/stats.json

# Run both test suites during migration period
npx karma start karma.conf.js && npx jest --coverage

# Check for known AngularJS CVEs in your dependency tree
npm audit | grep -i angular

Version History & Compatibility

TechnologyVersionStatusMigration Notes
AngularJS1.8.x (final: 1.8.3)EOL since Dec 20218+ unpatched CVEs; HeroDevs offers commercial NES [src7, src8]
AngularJS1.5-1.7EOLComponent API from 1.5+; required for react2angular [src6]
AngularJS1.2-1.4EOLNo component API; must use ngReact with directive bridge
react2angular4.xMaintainedSupports React 16-17; for React 18 use react18-react2angular fork
ngReact---ArchivedNo longer maintained; prefer react2angular [src4]
angular2react---MaintainedWraps AngularJS components for use in React; useful late in migration
React18.xCurrentConcurrent features; use react18-react2angular for bridge
React19.xLatestStable since Dec 2024; test bridge compatibility before upgrading

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
AngularJS 1.x app needs modernization (EOL framework)App is already on Angular 2+ (modern Angular)Upgrade within Angular ecosystem or Angular-to-React migration
Team knows React or wants to adopt itTeam prefers Vue.js or SvelteMigrate AngularJS to Vue (vue-in-angularjs) or Svelte
App is under active development with new featuresApp is being decommissioned within 6 monthsKeep AngularJS; apply security patches via HeroDevs NES
Codebase is > 10K LOC and rewrite is too riskyApp is < 5K LOC with simple UIClean rewrite may be faster than incremental migration
You need the React ecosystem (React Native, Next.js)You only need better TypeScript supportAngular 17+ has excellent TypeScript support
PCI DSS 4.0 or FedRAMP compliance is requiredApp is internal-only with no compliance requirementsHeroDevs NES as interim solution while planning migration [src8]

Important Caveats

Related Units