React vs Angular: Comprehensive Comparison

Introduction

React is a JavaScript library for building user interfaces, developed and maintained by Meta (Facebook). It focuses on the view layer and provides flexibility in choosing other libraries for routing, state management, and other features.

Angular is a complete framework for building web applications, developed and maintained by Google. It's an opinionated, full-featured framework that provides everything needed for enterprise-scale applications out of the box.


1. Philosophy and Architecture

React

Angular


2. Language

React

// React with JSX
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Using the component
<Welcome name="World" />

Angular

// Angular with TypeScript
import { Component } from '@angular/core';

@Component({
  selector: 'app-welcome',
  template: '<h1>Hello, {{name}}!</h1>'
})
export class WelcomeComponent {
  name: string = 'World';
}

// Using the component
<app-welcome></app-welcome>

3. Component Structure

React

// React Functional Component
import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [userId]);

  return (
    <div>
      {user ? <h1>{user.name}</h1> : <p>Loading...</p>}
    </div>
  );
}

Angular

// Angular Component
import { Component, OnInit, Input } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-profile',
  template: `
    <div>
      <h1 *ngIf="user">{{user.name}}</h1>
      <p *ngIf="!user">Loading...</p>
    </div>
  `,
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
  @Input() userId: number;
  user: any = null;

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUser(this.userId).subscribe(
      data => this.user = data
    );
  }
}

4. Data Binding

React

// React - One-way binding
function SearchBox() {
  const [query, setQuery] = useState('');

  const handleChange = (e) => {
    setQuery(e.target.value);
  };

  return (
    <input 
      type="text" 
      value={query} 
      onChange={handleChange}
    />
  );
}

Angular

// Angular - Two-way binding
@Component({
  selector: 'app-search-box',
  template: `
    <input 
      type="text" 
      [(ngModel)]="query"
    />
  `
})
export class SearchBoxComponent {
  query: string = '';
}

5. State Management

React

// React with useState
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

// React Context for global state
const ThemeContext = React.createContext();

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <MainContent />
    </ThemeContext.Provider>
  );
}

Angular

// Angular Service for state
@Injectable({
  providedIn: 'root'
})
export class CounterService {
  private countSubject = new BehaviorSubject<number>(0);
  count$ = this.countSubject.asObservable();

  increment() {
    this.countSubject.next(this.countSubject.value + 1);
  }
}

// Using the service
@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Count: {{count$ | async}}</p>
      <button (click)="increment()">Increment</button>
    </div>
  `
})
export class CounterComponent {
  count$ = this.counterService.count$;

  constructor(private counterService: CounterService) {}

  increment() {
    this.counterService.increment();
  }
}

6. Routing

React

// React Router
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<User />} />
      </Routes>
    </BrowserRouter>
  );
}

Angular

// Angular Router
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'users/:id', component: UserComponent },
  { 
    path: 'admin', 
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    canActivate: [AuthGuard]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

// In template
<nav>
  <a routerLink="/">Home</a>
  <a routerLink="/about">About</a>
  <a routerLink="/users/123">User</a>
</nav>
<router-outlet></router-outlet>

7. Dependency Injection

React

// React - Manual dependency passing
function App() {
  const userService = new UserService();
  const apiService = new ApiService();

  return <UserProfile userService={userService} apiService={apiService} />;
}

// Or with Context
const ServiceContext = React.createContext();

function App() {
  const services = {
    userService: new UserService(),
    apiService: new ApiService()
  };

  return (
    <ServiceContext.Provider value={services}>
      <UserProfile />
    </ServiceContext.Provider>
  );
}

Angular

// Angular - Automatic DI
@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(id: number) {
    return this.http.get(`/api/users/${id}`);
  }
}

@Component({
  selector: 'app-user-profile',
  template: '<div>{{user?.name}}</div>'
})
export class UserProfileComponent {
  user: any;

  // Services automatically injected
  constructor(
    private userService: UserService,
    private apiService: ApiService
  ) {}
}

8. Forms Handling

React

// React - Controlled Form
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});

  const handleSubmit = (e) => {
    e.preventDefault();
    const validationErrors = validate({ email, password });
    
    if (Object.keys(validationErrors).length === 0) {
      // Submit form
      console.log({ email, password });
    } else {
      setErrors(validationErrors);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      {errors.email && <span>{errors.email}</span>}
      
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      {errors.password && <span>{errors.password}</span>}
      
      <button type="submit">Login</button>
    </form>
  );
}

Angular

// Angular - Reactive Form
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-login-form',
  template: `
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
      <input type="email" formControlName="email">
      <div *ngIf="loginForm.get('email')?.errors?.['required']">
        Email is required
      </div>
      
      <input type="password" formControlName="password">
      <div *ngIf="loginForm.get('password')?.errors?.['minlength']">
        Password must be at least 6 characters
      </div>
      
      <button type="submit" [disabled]="!loginForm.valid">
        Login
      </button>
    </form>
  `
})
export class LoginFormComponent {
  loginForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6)]]
    });
  }

  onSubmit() {
    if (this.loginForm.valid) {
      console.log(this.loginForm.value);
    }
  }
}

9. HTTP Requests

React

// React with Fetch
import { useState, useEffect } from 'react';

function Users() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Angular

// Angular with HttpClient
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-users',
  template: `
    <div *ngIf="loading">Loading...</div>
    <div *ngIf="error">Error: {{error}}</div>
    <ul *ngIf="users$ | async as users">
      <li *ngFor="let user of users">{{user.name}}</li>
    </ul>
  `
})
export class UsersComponent implements OnInit {
  users$: Observable<any[]>;
  loading = true;
  error: string | null = null;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.users$ = this.http.get<any[]>('/api/users');
    
    this.users$.subscribe({
      next: () => this.loading = false,
      error: (err) => {
        this.error = err.message;
        this.loading = false;
      }
    });
  }
}

10. Testing

React

// React Testing with Jest and RTL
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('increments counter', () => {
  render(<Counter />);
  
  const button = screen.getByText('Increment');
  fireEvent.click(button);
  
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Angular

// Angular Testing with Jasmine
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ CounterComponent ]
    }).compileComponents();

    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should increment counter', () => {
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    fixture.detectChanges();
    
    expect(component.count).toBe(1);
  });
});

11. Performance

React

// React Performance Optimization
import { memo, useMemo, useCallback } from 'react';

const ExpensiveComponent = memo(({ data, onUpdate }) => {
  const processedData = useMemo(() => {
    return data.map(item => item * 2);
  }, [data]);

  const handleClick = useCallback(() => {
    onUpdate(processedData);
  }, [processedData, onUpdate]);

  return <button onClick={handleClick}>Update</button>;
});

Angular

// Angular Performance Optimization
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-expensive',
  template: '<button (click)="handleClick()">Update</button>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExpensiveComponent {
  @Input() data: number[];

  handleClick() {
    const processedData = this.data.map(item => item * 2);
    this.onUpdate.emit(processedData);
  }
}

12. Tooling and CLI

React

# Create React App
npx create-react-app my-app

# Vite
npm create vite@latest my-app -- --template react

Angular

# Angular CLI
npm install -g @angular/cli
ng new my-app

# Generate components, services, etc.
ng generate component user-profile
ng generate service user
ng generate module admin --routing

13. Learning Curve

React

Angular


14. Community and Ecosystem

React

Angular


15. Bundle Size

React

Angular


16. Mobile Development

React

Angular


17. Enterprise Features

React

Angular


Performance Comparison Table

Feature React Angular
Initial Load Time ⚡ Faster 🐢 Slower
Runtime Performance ⚡ Fast (Virtual DOM) ⚡ Fast (Optimized CD)
Bundle Size ✅ Smaller ⚠️ Larger
Memory Usage ✅ Lower ⚠️ Higher
Update Speed ⚡ Very Fast ⚡ Fast

Learning Curve Comparison

Aspect React Angular
Getting Started ⭐⭐⭐⭐⭐ Easy ⭐⭐⭐ Moderate
TypeScript Required ❌ Optional ✅ Yes
Concepts to Learn 5-7 15+
Time to Productivity 1-2 weeks 3-4 weeks
Mastery Time 2-3 months 6-12 months

When to Use Each

Choose React When:

Choose Angular When:


Pros and Cons Summary

React Pros

React Cons

Angular Pros

Angular Cons


Code Comparison: Todo App

React Todo App

import { useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (input.trim()) {
      setTodos([...todos, { id: Date.now(), text: input, done: false }]);
      setInput('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <h1>Todo List</h1>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <button onClick={addTodo}>Add</button>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ 
              textDecoration: todo.done ? 'line-through' : 'none' 
            }}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Angular Todo App

// todo.component.ts
import { Component } from '@angular/core';

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

@Component({
  selector: 'app-todo',
  template: `
    <div>
      <h1>Todo List</h1>
      <input
        [(ngModel)]="input"
        (keyup.enter)="addTodo()"
      />
      <button (click)="addTodo()">Add</button>
      
      <ul>
        <li *ngFor="let todo of todos">
          <input
            type="checkbox"
            [(ngModel)]="todo.done"
          />
          <span [style.text-decoration]="todo.done ? 'line-through' : 'none'">
            {{todo.text}}
          </span>
          <button (click)="deleteTodo(todo.id)">Delete</button>
        </li>
      </ul>
    </div>
  `
})
export class TodoComponent {
  todos: Todo[] = [];
  input: string = '';

  addTodo() {
    if (this.input.trim()) {
      this.todos.push({
        id: Date.now(),
        text: this.input,
        done: false
      });
      this.input = '';
    }
  }

  deleteTodo(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
  }
}

Migration Considerations

From React to Angular

From Angular to React


Conclusion

Both React and Angular are excellent choices for building modern web applications:

React excels at providing flexibility and simplicity, making it ideal for small to medium projects, teams that value freedom of choice, and developers who want to learn quickly. Its massive ecosystem and community support make it a safe choice for most projects.

Angular shines in enterprise environments where structure, consistency, and comprehensive features are paramount. It's the better choice for large-scale applications with large teams that benefit from opinionated patterns and built-in solutions.

The choice between React and Angular should be based on:

Neither is objectively "better" – they solve different problems and serve different needs. Choose the tool that best fits your specific use case and team strengths.