How to Migrate from Angular to React

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

TL;DR

Constraints

Quick Reference

Angular PatternReact EquivalentExample
@Component({ template })Function component + JSXfunction App() { return <div>Hello</div>; }
@Injectable() class DataServiceCustom hook or plain modulefunction useData() { ... } or export const dataService = { ... }
constructor(private svc: DataService) (DI)useContext() or direct importconst data = useContext(DataContext)
this.http.get<T>(url) (HttpClient)fetch / React Query / SWRconst { data } = useQuery({ queryKey: ['key'], queryFn: () => fetch(url) })
observable$.subscribe(val => ...) (RxJS)useState + useEffect or React Queryconst [val, setVal] = useState(null); useEffect(() => { ... }, [])
signal() (Angular 16+)useState / useSignalconst [count, setCount] = useState(0)
@Input() name: stringPropsfunction Child({ name }: { name: string }) { ... }
@Output() clicked = new EventEmitter()Callback propsfunction Child({ onClick }: { onClick: () => void }) { ... }
*ngIf="condition" / @if (condition)Conditional rendering{condition && <Component />}
*ngFor="let item of items" / @for (item of items).map() in JSX{items.map(item => <Item key={item.id} {...item} />)}
[(ngModel)]="value" (two-way binding)Controlled input<input value={val} onChange={e => setVal(e.target.value)} />
RouterModule.forRoot(routes)React Router <Routes><Routes><Route path="/" element={<Home />} /></Routes>
canActivate: [AuthGuard] (route guard)Wrapper component or loader<ProtectedRoute><Dashboard /></ProtectedRoute>
@Pipe({ name: 'currency' })Plain function or useMemoconst formatted = useMemo(() => formatCurrency(val), [val])
@NgModule({ imports, declarations })No equivalent — just ES importsReact has no module system; use ES modules and React.lazy()
HttpInterceptorFetch wrapper or Axios interceptorconst api = axios.create(); api.interceptors.request.use(...)
Reactive Forms (FormGroup, FormControl)React Hook Form or useActionState (React 19)const { register, handleSubmit } = useForm<FormData>()
| async pipeuse() hook (React 19) or useQueryconst comments = use(commentsPromise)

Decision Tree

START
├── Is the Angular app < 10K LOC with few third-party Angular libraries?
│   ├── YES → Full rewrite in React (faster than incremental for small apps)
│   └── NO ↓
├── Does the team need to ship features during migration?
│   ├── YES → Strangler Fig: use Module Federation or single-spa for coexistence
│   └── NO ↓
├── Is the Angular app on version 17+ with standalone components?
│   ├── YES → Easier migration — standalone components map 1:1 to React. Migrate component-by-component.
│   └── NO ↓
├── Is the app heavily module-based with lazy-loaded Angular modules?
│   ├── YES → Migrate route-by-route: replace each Angular lazy module with a React micro-frontend
│   └── NO ↓
├── Does the app rely heavily on RxJS for complex async orchestration?
│   ├── YES → First refactor RxJS → simpler async patterns (promises, Angular signals), then migrate to React hooks
│   └── NO ↓
├── Is there heavy use of Angular Material or CDK?
│   ├── YES → Choose a React UI library (MUI, Radix, shadcn/ui) first, map components, then migrate
│   └── NO ↓
└── DEFAULT → Route-by-route incremental migration with shared state via a framework-agnostic store

Step-by-Step Guide

1. Audit the Angular codebase

Map all Angular modules, services, routes, and third-party dependencies. Categorize each module by complexity and business criticality. This determines migration order and timeline. [src3, src5]

# Count Angular components, services, pipes, directives, and modules
find src -name "*.component.ts" | wc -l
find src -name "*.service.ts" | wc -l
find src -name "*.pipe.ts" | wc -l
find src -name "*.module.ts" | wc -l

# Count RxJS usage (higher count = more migration effort)
grep -rn "subscribe\|Observable\|BehaviorSubject\|switchMap" --include="*.ts" src/ | wc -l

# Count Angular signals usage (Angular 16+, maps easily to React useState)
grep -rn "signal(\|computed(\|effect(" --include="*.ts" src/ | wc -l

Verify: You have a document listing every Angular module with component count, service count, RxJS complexity score, signals usage count, and third-party dependency list.

2. Set up the coexistence shell

Install Module Federation (recommended) or single-spa to run Angular and React side by side. The Angular app becomes the “host” and React apps mount as remotes. [src5, src7]

# Option A: Module Federation (Webpack 5 — recommended for new migrations)
npm install @angular-architects/module-federation
ng add @angular-architects/module-federation --project main --port 4200

# Option B: single-spa (proven for complex multi-framework setups)
npx create-single-spa --framework react --moduleType app-parcel
npm install single-spa single-spa-react

Verify: Both Angular and React apps load in the same browser window. Navigate between routes without full page reloads.

3. Create a shared state layer

Angular services and React hooks cannot directly share state. Create a framework-agnostic store that both can read/write. [src3, src6]

// shared/store.ts — framework-agnostic reactive store
type Listener = () => void;

export class SharedStore<T extends Record<string, unknown>> {
  private state: T;
  private listeners = new Set<Listener>();

  constructor(initialState: T) { this.state = initialState; }
  getState(): T { return this.state; }

  setState(partial: Partial<T>): void {
    this.state = { ...this.state, ...partial };
    this.listeners.forEach(fn => fn());
  }

  subscribe(listener: Listener): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}

Verify: Setting state from the Angular console triggers re-renders in React components and vice versa.

4. Migrate services to hooks and modules

Convert Angular @Injectable services into React custom hooks (for stateful/lifecycle logic) or plain TypeScript modules (for pure functions). [src1, src2]

// BEFORE: Angular service
// @Injectable({ providedIn: 'root' })
// export class UserService {
//   constructor(private http: HttpClient) {}
//   getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users'); }
// }

// AFTER: React custom hook (with React Query)
import { useQuery } from '@tanstack/react-query';

export function useUsers() {
  return useQuery<User[]>({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('/api/users');
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    },
  });
}

Verify: useUsers() in a React component returns the same data as UserService.getUsers() in Angular.

5. Migrate routes incrementally

Replace one Angular lazy-loaded route at a time with its React equivalent. The Module Federation host or single-spa router decides which framework renders each URL. [src3, src7]

// single-spa root config
import { registerApplication, start } from 'single-spa';

registerApplication({
  name: 'angular-app',
  app: () => import('./angular-main'),
  activeWhen: ['/dashboard', '/settings'],  // Not yet migrated
});

registerApplication({
  name: 'react-app',
  app: () => import('./react-main'),
  activeWhen: ['/users', '/reports'],  // Migrated to React
});

start();

Verify: Navigate to /users — React renders. Navigate to /dashboard — Angular renders. Back button and shared store work correctly.

6. Convert template syntax and patterns

Translate Angular template directives to React JSX patterns. This is the bulk of the migration work. [src1, src4]

// BEFORE: Angular template
// <li *ngFor="let user of users; trackBy: trackById"
//     [class.active]="user.id === selectedId"
//     (click)="selectUser(user)">
//   {{ user.name | uppercase }}
// </li>

// AFTER: React JSX
function UserList() {
  const { data: users } = useUsers();
  const [selectedId, setSelectedId] = useState<string | null>(null);

  return (
    <ul>
      {users?.map(user => (
        <li
          key={user.id}
          className={user.id === selectedId ? 'active' : ''}
          onClick={() => setSelectedId(user.id)}
        >
          {user.name.toUpperCase()}
        </li>
      ))}
    </ul>
  );
}

Verify: Side-by-side comparison — both render identical HTML. Click behavior and styling match.

7. Decommission Angular and clean up

After all routes are migrated and production-tested, remove Angular, the coexistence shell, and the shared store bridge. [src3, src6]

# Remove Angular packages
npm uninstall @angular/core @angular/common @angular/router @angular/forms \
  @angular/platform-browser @angular/compiler @angular/compiler-cli \
  @ngrx/store @ngrx/effects rxjs zone.js single-spa single-spa-angular

# Verify no Angular references remain
grep -rn "@angular\|@Injectable\|@Component\|@NgModule" --include="*.ts" src/ | head -20
npm ls @angular/core

Verify: npm ls @angular/core returns “empty”. App loads, all routes work, bundle size decreased. CI pipeline passes.

Code Examples

TypeScript/React: Converting an Angular service with RxJS to a React hook

Full script: typescript-react-converting-an-angular-service-wit.ts (66 lines)

// Input:  Angular service using RxJS BehaviorSubject for real-time state
// Output: React custom hook with equivalent behavior using useSyncExternalStore

import { useSyncExternalStore, useCallback, useMemo } from 'react';

interface CartItem { id: string; name: string; price: number; quantity: number; }

// Module-level store (replaces @Injectable singleton)
let cartItems: CartItem[] = [];
const listeners = new Set<() => void>();

function emitChange() { listeners.forEach(fn => fn()); }

export const cartStore = {
  subscribe(listener: () => void): () => void {
    listeners.add(listener);
    return () => listeners.delete(listener);
  },
  getSnapshot(): CartItem[] { return cartItems; },
  addItem(item: CartItem): void {
    cartItems = [...cartItems, item];
    emitChange();
  },
  removeItem(id: string): void {
    cartItems = cartItems.filter(i => i.id !== id);
    emitChange();
  },
};

export function useCart() {
  const items = useSyncExternalStore(cartStore.subscribe, cartStore.getSnapshot);
  const total = useMemo(() => items.reduce((sum, i) => sum + i.price * i.quantity, 0), [items]);
  const addItem = useCallback((item: CartItem) => cartStore.addItem(item), []);
  const removeItem = useCallback((id: string) => cartStore.removeItem(id), []);
  return { items, total, addItem, removeItem };
}

TypeScript/React: Converting Angular Reactive Forms to React Hook Form

Full script: typescript-react-converting-angular-reactive-forms.ts (87 lines)

// Input:  Angular reactive form with validators and error messages
// Output: React Hook Form equivalent with identical validation

import { useForm } from 'react-hook-form';

interface SignupForm { email: string; password: string; confirmPassword: string; }

function SignupPage() {
  const { register, handleSubmit, watch, formState: { errors, isSubmitting } } = useForm<SignupForm>();
  const password = watch('password');

  const onSubmit = async (data: SignupForm) => {
    const res = await fetch('/api/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: data.email, password: data.password }),
    });
    if (!res.ok) throw new Error('Signup failed');
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', {
        required: 'Email is required',
        pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Invalid email' },
      })} />
      {errors.email && <span className="error">{errors.email.message}</span>}
      <button type="submit" disabled={isSubmitting}>Sign Up</button>
    </form>
  );
}

TypeScript: Framework-agnostic bridge for Angular-React coexistence

Full script: typescript-framework-agnostic-bridge-for-angular-r.ts (51 lines)

// Input:  Angular app that needs to share auth state with React micro-frontends
// Output: CustomEvent-based bridge that works across any framework boundary

interface AuthPayload { userId: string; token: string; roles: string[]; }
type EventMap = {
  'auth:login': AuthPayload;
  'auth:logout': undefined;
  'theme:change': { theme: 'light' | 'dark' };
};

export function emit<K extends keyof EventMap>(event: K, detail: EventMap[K]): void {
  window.dispatchEvent(new CustomEvent(event, { detail }));
}

export function on<K extends keyof EventMap>(
  event: K, handler: (detail: EventMap[K]) => void
): () => void {
  const wrapper = (e: Event) => handler((e as CustomEvent).detail);
  window.addEventListener(event, wrapper);
  return () => window.removeEventListener(event, wrapper);
}

TypeScript/React: Converting Angular route guards to React protected routes

// Input:  Angular canActivate guard
// Output: React wrapper component with React Router v6

import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

function ProtectedRoute() {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) return <div>Loading...</div>;
  if (!isAuthenticated) return <Navigate to="/login" replace />;
  return <Outlet />;
}

// Usage in router:
// <Route element={<ProtectedRoute />}>
//   <Route path="/dashboard" element={<Dashboard />} />
//   <Route path="/settings" element={<Settings />} />
// </Route>

Anti-Patterns

Wrong: Porting Angular DI system into React with a custom DI container

// ❌ BAD — Rebuilding Angular's DI in React
class DIContainer {
  private registry = new Map<string, any>();
  register(token: string, instance: any) { this.registry.set(token, instance); }
  resolve<T>(token: string): T { return this.registry.get(token); }
}

const container = new DIContainer();
container.register('UserService', new UserService());

function UserList() {
  const userService = container.resolve<UserService>('UserService');
  // ...
}

Correct: Use React's built-in patterns — hooks for state, imports for logic

// ✅ GOOD — React-idiomatic approach: custom hooks and plain imports
export function useUsers() {
  return useQuery({ queryKey: ['users'], queryFn: fetchUsers });
}

export function formatUserName(user: User): string {
  return `${user.firstName} ${user.lastName}`;
}

function UserList() {
  const { data: users } = useUsers();
  return <ul>{users?.map(u => <li key={u.id}>{formatUserName(u)}</li>)}</ul>;
}

Wrong: Converting RxJS observables by subscribing inside useEffect

// ❌ BAD — Manual RxJS subscription in React (memory leaks, no Suspense support)
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const sub = userService.getUser(userId).pipe(
      switchMap(user => userService.getActivity(user.id)),
      catchError(err => of(null))
    ).subscribe(data => setUser(data));
    return () => sub.unsubscribe();
  }, [userId]);
}

Correct: Replace RxJS with React Query or native async patterns

// ✅ GOOD — React Query handles caching, refetching, error states automatically
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {(error as Error).message}</div>;
  return <div>{user?.name}</div>;
}

Wrong: Migrating NgModules to React “module” wrappers (provider hell)

// ❌ BAD — Recreating Angular's NgModule as deeply nested providers
function SharedModule({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <HttpProvider>
          <ToastProvider>
            {children}
          </ToastProvider>
        </HttpProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

Correct: Use flat composition with co-located providers

// ✅ GOOD — Only wrap what needs wrapping, keep providers flat
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}
// Auth context only wraps routes that need it via layout routes

Wrong: Keeping two-way binding mentality with uncontrolled inputs

// ❌ BAD — Using refs to imperatively read form values (Angular [(ngModel)] muscle memory)
function EditProfile() {
  const nameRef = useRef<HTMLInputElement>(null);
  const emailRef = useRef<HTMLInputElement>(null);

  const handleSubmit = () => {
    const name = nameRef.current!.value;
    const email = emailRef.current!.value;
    fetch('/api/profile', { method: 'PUT', body: JSON.stringify({ name, email }) });
  };
  // No validation, no re-render on input change
}

Correct: Use controlled components with state

// ✅ GOOD — Controlled inputs: React state is the single source of truth
function EditProfile() {
  const [name, setName] = useState('John');
  const [email, setEmail] = useState('[email protected]');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await fetch('/api/profile', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, email }),
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <button type="submit">Save</button>
    </form>
  );
}

Wrong: Big-bang rewrite of the entire Angular app at once

// ❌ BAD — Halting all feature development for 6+ months to rewrite
// "We'll rewrite the entire app in React over Q2-Q3, then launch."
// Result: scope creep, feature freeze, missed deadlines, regressions at go-live.

Correct: Strangler Fig — migrate route by route while shipping features

// ✅ GOOD — Incremental migration with continuous delivery
// Month 1: Set up single-spa shell, migrate /login to React
// Month 2: Migrate /users, keep shipping Angular features on /dashboard
// Month 3: Migrate /dashboard
// Month 4: Migrate /settings, remove Angular
// Each migration is tested and deployed to production independently.

Common Pitfalls

Diagnostic Commands

# Count Angular-specific patterns remaining in codebase
grep -rn "@Component\|@Injectable\|@NgModule\|@Pipe\|@Directive" --include="*.ts" src/ | wc -l

# Count RxJS usage (should trend toward zero)
grep -rn "subscribe(\|\.pipe(\|BehaviorSubject\|switchMap\|mergeMap" --include="*.ts" src/ | wc -l

# Count Angular signals (useful if pre-migrating RxJS to signals first)
grep -rn "signal(\|computed(\|effect(" --include="*.ts" src/ | wc -l

# Check total bundle size during coexistence
npx webpack-bundle-analyzer dist/stats.json

# Verify no zone.js interference in React components
grep -rn "zone.js\|NgZone" --include="*.ts" src/ | head -10

# List all Angular packages still installed
npm ls 2>/dev/null | grep @angular

# Check for duplicate dependencies
npm ls --all 2>/dev/null | grep -E "rxjs|zone.js|@angular" | sort -u

Version History & Compatibility

VersionStatusBreaking ChangesMigration Notes
Angular 19 (Nov 2024)CurrentStandalone components default, signals stable, @let syntaxEasiest to migrate — no NgModule boilerplate, signals map to useState
Angular 18 (May 2024)ActiveZoneless change detection (experimental), stable signalsGood migration candidate — modern APIs reduce migration surface
Angular 17 (Nov 2023)LTSNew control flow (@if, @for, @switch), deferrable viewsControl flow closer to JSX, simplifies template migration
Angular 16 (May 2023)MaintenanceSignals (developer preview), required inputsSignals map more naturally to React state
Angular 14–15 (2022)EOL/MaintenanceStandalone components (opt-in), typed formsStandalone components can be migrated individually
React 19 (Dec 2024)Currentuse() hook, ref as prop, Actions, useActionState, useOptimisticUse modern API — no forwardRef needed, better form handling
React 18 (Mar 2022)LTScreateRoot, Concurrent features, useSyncExternalStoreMinimum recommended React version for new migrations

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Team is hiring React devs and Angular talent is scarceAngular team is productive, no hiring issuesStay on Angular, upgrade to latest version
App needs React ecosystem (Next.js, React Native, Remix)SSR is the only needAngular 17+ SSR or Angular Universal
Codebase has accumulated tech debt, rewrite is justifiedApp is well-maintained, meeting business needsUpgrade Angular version instead
Company is standardizing on React across productsAngular app is isolated and self-containedMaintain independently
Complex RxJS chains cause bugs and confusionTeam is proficient with RxJSSimplify RxJS with Angular signals instead
UI library is limiting design flexibilityCurrent UI library meets design requirementsSwap UI library within Angular
Need to share code between web and mobile (React Native)Only need responsive web designKeep Angular, use responsive CSS

Important Caveats

Related Units