react2angular to embed React components inside your AngularJS app, converting leaf components first and working up the component tree until AngularJS can be fully removed. [src2, src4]npm install react2angular react react-dom prop-typesreact2angular bridge (component API required); for 1.2-1.4, use ngReact with directive bridge instead [src6]| AngularJS Pattern | React Equivalent | Example |
|---|---|---|
$scope / $rootScope | useState / useContext / Redux store | const [count, setCount] = useState(0) |
ng-model (two-way binding) | Controlled component (value + onChange) | <input value={val} onChange={e => setVal(e.target.value)} /> |
ng-repeat | Array.map() in JSX | {items.map(item => <Item key={item.id} {...item} />)} |
ng-if / ng-show | Conditional rendering / CSS toggle | {show && <Component />} |
ng-click | onClick handler | <button onClick={handleClick}>Go</button> |
ng-class | className with template literal or clsx | className={clsx('btn', {active: isActive})} |
| Directive (restrict: 'E') | Functional component | function MyWidget({ title }) { return <h2>{title}</h2>; } |
| Service / Factory (DI) | Module import / Context / custom hook | import { apiClient } from './services/api' |
$http / $resource | fetch / axios / React Query / TanStack Query | const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) |
$watch / $watchCollection | useEffect with dependency array | useEffect(() => { ... }, [value]) |
$timeout / $interval | setTimeout / setInterval + useEffect cleanup | useEffect(() => { const id = setInterval(fn, 1000); return () => clearInterval(id); }, []) |
resolve (route pre-fetch) | Loader function (React Router 6) / useEffect | loader: async () => fetch('/api/data') |
Filters ({{ val | currency }}) | Helper functions called in JSX | {formatCurrency(val)} |
angular.module('app', [deps]) | ES module imports + <App /> root | import App from './App'; createRoot(el).render(<App />) |
$emit / $broadcast (event bus) | Callback props / Context / state management | <Child onUpdate={handleUpdate} /> |
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
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.
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.
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.
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.
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.
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.
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.
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 };
}
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>;
}
// 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
// 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
// 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
}
// 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
}
// 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.
// 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
// 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>;
}
// 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>;
}
on-search), React uses camelCase (onSearch). When using ngReact's reactDirective, you must explicitly list camelCase prop names. Fix: reactDirective('MyComponent', ['onSearch', 'userName']). [src4, src6]$scope.$apply() or use $ngRedux which handles digest integration automatically. [src4]$ngRedux.connect() causes ghost updates. Fix: $scope.$on('$destroy', unsubscribe). [src4]React.lazy + dynamic import()) and tree-shake unused AngularJS modules as you migrate. [src2, src5]fetch wrapper replicating the same behavior. [src3, src5]ng-scope, ng-isolate-scope classes that React doesn't generate. Fix: use CSS modules or CSS-in-JS for React components to scope styles. [src2]MutationEvent with MutationObserver wrapper before migrating. [src8]# 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
| Technology | Version | Status | Migration Notes |
|---|---|---|---|
| AngularJS | 1.8.x (final: 1.8.3) | EOL since Dec 2021 | 8+ unpatched CVEs; HeroDevs offers commercial NES [src7, src8] |
| AngularJS | 1.5-1.7 | EOL | Component API from 1.5+; required for react2angular [src6] |
| AngularJS | 1.2-1.4 | EOL | No component API; must use ngReact with directive bridge |
| react2angular | 4.x | Maintained | Supports React 16-17; for React 18 use react18-react2angular fork |
| ngReact | --- | Archived | No longer maintained; prefer react2angular [src4] |
| angular2react | --- | Maintained | Wraps AngularJS components for use in React; useful late in migration |
| React | 18.x | Current | Concurrent features; use react18-react2angular for bridge |
| React | 19.x | Latest | Stable since Dec 2024; test bridge compatibility before upgrading |
| Use When | Don't Use When | Use 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 it | Team prefers Vue.js or Svelte | Migrate AngularJS to Vue (vue-in-angularjs) or Svelte |
| App is under active development with new features | App is being decommissioned within 6 months | Keep AngularJS; apply security patches via HeroDevs NES |
| Codebase is > 10K LOC and rewrite is too risky | App is < 5K LOC with simple UI | Clean rewrite may be faster than incremental migration |
| You need the React ecosystem (React Native, Next.js) | You only need better TypeScript support | Angular 17+ has excellent TypeScript support |
| PCI DSS 4.0 or FedRAMP compliance is required | App is internal-only with no compliance requirements | HeroDevs NES as interim solution while planning migration [src8] |
react2angular bridge adds overhead per bridged component. For performance-critical paths with hundreds of bridged components, batch conversions to reduce bridge overhead.$inject) has no direct React equivalent. Services must be refactored to ES module imports, React Context, or a DI library like inversify.ng-model) is fundamentally different from React's one-way data flow. Components relying heavily on two-way binding require more conversion effort. [src1]$compile for dynamic template rendering, replacing it is one of the hardest migration challenges — use React's dynamic component rendering patterns instead.