DEV Community

Cover image for React and Angular: Common Patterns Comparison
Guilherme Toledo
Guilherme Toledo

Posted on • Edited on

React and Angular: Common Patterns Comparison

This is by no means intended to be an in-depth guide to the differences between React and Angular. On the contrary, it is a first-glance perspective from a developer who has been working with React and has historically avoided Angular’s OOP boilerplate, but finally decided to take a look at how things are going on the other side of the fence.

Starting with my biggest pain point: boilerplate. I quickly discovered that just looking at Angular code snippets does not do justice to the work its CLI puts into handling that complexity. It defines naming conventions, folder structures, creates files, and manages required imports. Part of what can be considered boilerplate is also related to project structure, and becoming more familiar with MVC helps in dealing with the mental model of OOP-based programming languages. This makes Angular feel well structured and predictable, which is probably why it is often deemed an enterprise framework suited for large organizations.

Over the last few years, Angular has also been shifting toward a more approachable development experience, with its relatively recent move to standalone components as the default, and toward a much more performant framework with the adoption of a signal-based architecture, a path that the React team unfortunately has explicitly chosen not to pursue.

At the end of the day, both React and Angular will remain, at least for a while, the two most popular front-end frameworks, so it is only wise to be familiar with both.

Key Philosophical Differences

Aspect React Angular
Language JavaScript or TypeScript TypeScript
Paradigm Functional, UI/State/Logic coupled Object-oriented, MVC-like
Templates JSX (JavaScript Syntax Extension) HTML templates + directives
State Immutable updates (useState) Mutable class properties
Data Flow Unidirectional (parent to child) Two-way binding
Rendering DOM + Virtual DOM (diff algorithm) DOM with change detection (Zone.js or signals)
Philosophy Minimal, "library" Complete framework

TL;DR

Links React Concept Angular Equivalent
⬇️ useState Component properties / signals
⬇️ useEffect Lifecycle hooks (ngOnInit, ngOnDestroy) / effect()
⬇️ Data fetching (useEffect/fetch()/useState) HttpClient/effect()
⬇️ Props (data / callbacks ) @Input() @Output() / signal input output
⬇️ Context API @Injectable Service (singleton via DI)
⬇️ CSS Modules View Encapsulation
⬇️ Conditional rendering (&&, ?:) *ngIf / @if
⬇️ Lists / keys (.map + key) *ngFor + trackBy / @for
⬇️ Inline style objects [style] / [ngStyle] / computed signal objects
⬇️ Two-way binding (value + onChange) [(ngModel)] / reactive forms

State Management

One of the key aspects of modern frameworks is how they handle state and changes to that state in order to display dynamic, up-to-date content.

In React, state updates are expected to be immutable so React can detect changes via reference comparison. Updating state variables directly does not trigger re-renders, so state is managed through hooks such as useState.

In Angular, component properties can be mutated directly. Change detection is handled by the framework, or, when using signals, by precise reactive updates.

React - useState

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

  const increment = () => setCount((val) => val + 1);
  const decrement = () => setCount((val) => val - 1);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Angular - Class Properties

export class CounterComponent {
  count = 0;

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }
}
Enter fullscreen mode Exit fullscreen mode

Signals (Angular 16+)

export class CounterComponent {
  count = signal(0);

  increment() {
    this.count.update((val) => val + 1);
  }

  decrement() {
    this.count.update((val) => val - 1);
  }
}
Enter fullscreen mode Exit fullscreen mode
<div>
  <p>Count: {{ count() }}</p>
  <button (click)="increment()">+</button>
  <button (click)="decrement()">-</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Lifecycle

Another key aspect of modern frameworks is how they manage a component’s lifecycle, from creation to updates and eventual cleanup.

In React, the lifecycle is driven by rendering and re-rendering. Side effects are handled through hooks such as useEffect, which run in response to render cycles and dependency changes, and cleanup logic is defined within those effects.

In Angular, the lifecycle has traditionally been explicit and structured, with well-defined hooks such as ngOnInit, ngOnChanges, and ngOnDestroy, called by the framework at specific moments. With the introduction of signals and effect(), Angular also supports a more reactive, render-adjacent model, where side effects run automatically in response to state changes rather than fixed lifecycle phases.

React - useEffect

function Component() {
  useEffect(() => {
    // Run on mount
    console.log("Mounted");

    // Cleanup function on return
    return () => {
      console.log("Unmounting");
    };

    // Re-run when dependency changes
  }, [dependency]);
  // No dependecy in array, runs just once
  // No array as second parameter, re-run every time

  return <div>Render</div>;
}
Enter fullscreen mode Exit fullscreen mode

Angular

Lifecycle hooks

@Component({ template: `<div>Render</div>` })
export class Component implements OnInit, OnDestroy, OnChanges {
  ngOnInit() {
    console.log("Mounted");
  }

  ngOnChanges(changes: SimpleChanges) {
    console.log("Updated", changes);
  }

  ngOnDestroy() {
    console.log("Unmounting");
  }
}
Enter fullscreen mode Exit fullscreen mode

Effect (Angular 17+)

@Component({ template: `<div>{{ count() }}</div>` })
export class Component {
  constructor() {
    // Run on mount and on updates
    effect((onCleanup) => {
      console.log("Mounted");

      // Cleanup function on return
      onCleanup(() => console.log("Cleaning up effect"));
    });

    afterNextRender(() => {
      console.log("DOM ready");
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Data Fetching

In React, data fetching is commonly performed using the Fetch API, or some other library, inside useEffect and data is then stored in a state using useState.

In Angular, data fetching is typically handled in services using HttpClient, often triggered from lifecycle hooks such as ngOnInit. With signals and effect(), Angular also supports a reactive model where data updates propagate without relying on lifecycle hooks.

React - useEffect + fetch() + useState

function User() {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    fetch("/api/user")
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, []);

  return <div>{user?.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Angular

HttpClient + lifecycle (ngOnInit)

@Component({
  selector: "app-user",
  template: "{{ user?.name }}",
})
export class UserComponent implements OnInit {
  user: any;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.http.get("/api/user").subscribe((data) => {
      this.user = data;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

HttpClient + signal + effect (Angular 17+)

@Component({
  selector: "app-user",
  template: "{{ user()?.name }}",
})
export class UserComponent {
  user = signal<any>(null);

  constructor(private http: HttpClient) {
    effect(() => {
      this.http.get("/api/user").subscribe((data) => {
        this.user.set(data);
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Global and Shared State

Another key aspect of modern frameworks is how state that must be shared across multiple, unrelated components is handled.

In React, global state is commonly managed via the Context API, often combined with useReducer or external libraries that help optimize and structure state management.

In Angular, shared state is typically handled via services, which are singletons by default and can be injected where needed, or via signals that act as reactive global stores.

React – Context API

import { createContext, useContext, useState } from "react";

const ThemeContext = createContext<"light" | "dark">("light");

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  return (
    <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode
function App() {
  return (
    <ThemeProvider value="light">
      <MyComponent />
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode
function MyComponent() {
  const theme = useContext(ThemeContext);

  return <div>Current theme: {theme}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Angular – Services (Singleton State)

import { Injectable } from "@angular/core";

@Injectable({ providedIn: "root" })
export class ThemeService {
  theme: "light" | "dark" = "light";
}
Enter fullscreen mode Exit fullscreen mode
import { Component } from "@angular/core";
import { ThemeService } from "./theme.service";

@Component({
  selector: "app-my-component",
  template: `<div>Current theme: {{ themeService.theme }}</div>`,
})
export class MyComponent {
  constructor(public themeService: ThemeService) {}
}
Enter fullscreen mode Exit fullscreen mode

Signals (Angular 16+)

import { Injectable, signal } from "@angular/core";

@Injectable({ providedIn: "root" })
export class ThemeService {
  theme = signal<"light" | "dark">("light");

  toggle() {
    this.theme.update((t) => (t === "light" ? "dark" : "light"));
  }
}
Enter fullscreen mode Exit fullscreen mode
@Component({
  selector: "app-my-component",
  template: `<div>Current theme: {{ themeService.theme() }}</div>`,
})
export class MyComponent {
  constructor(public themeService: ThemeService) {}
}
Enter fullscreen mode Exit fullscreen mode

CSS Scoping

Modern frameworks have ways to deal with style encapsulation to avoid conflicting CSS class names and selectors overwriting each other across unrelated components.

React has no built-in CSS scoping but achieves it through its ecosystem. The common approach is creating custom class names to prevent conflicts. CSS cascading is not affected (child components also receive the parent's styles).

Angular creates a custom attribute selector to apply the styles to. CSS cascading is affected (child components do not receive the parent's styles).

React - CSS Modules

function MyComponent() {
  return <div className={styles.container} />;
}
Enter fullscreen mode Exit fullscreen mode
<div class="container_a3f2b"></div>
Enter fullscreen mode Exit fullscreen mode

Angular - View Encapsulation

@Component({
  selector: "app-my-component",
  template: `<div class="container"></div>`,
  styles: [
    `
      .container {
        color: blue;
      }
    `,
  ],
})
export class MyComponent {}
Enter fullscreen mode Exit fullscreen mode
<div class="container" _ngcontent-abc-123></div>
Enter fullscreen mode Exit fullscreen mode
.container[_ngcontent-abc-123] {
  color: blue;
}
Enter fullscreen mode Exit fullscreen mode

Style Binding

fontSize is a static value while width and backgroundColor are dynamic values.

React - Style Objects

function MyButton() {
  const [width, setWidth] = useState(200);
  const [isActive, setIsActive] = useState(true);

  const buttonStyles = {
    fontSize: "16px",
    width: `${width}px`,
    backgroundColor: isActive ? "green" : "teal",
  };

  return <button style={buttonStyles}>Click</button>;
}
Enter fullscreen mode Exit fullscreen mode

Angular - Property Binding

Individual style properties:

export class MyButtonComponent {
  width = 200;
  isActive = true;
}
Enter fullscreen mode Exit fullscreen mode
<button
  style="font-size: 16px;"
  [style.width.px]="width"
  [style.background-color]="isActive ? 'green' : 'teal'"
>
  Click
</button>
Enter fullscreen mode Exit fullscreen mode

Or with ngStyle for multiple styles:

export class MyButtonComponent {
  width = 200;
  isActive = true;

  get buttonStyles(): Partial<CSSStyleDeclaration> {
    return {
      fontSize: "16px",
      width: this.width + "px",
      backgroundColor: this.isActive ? "green" : "teal",
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Signals (Angular 16+)

export class MyButtonComponent {
  width = signal(200);
  isActive = signal(true);

  buttonStyles = computed<Partial<CSSStyleDeclaration>>(() => ({
    fontSize: "16px",
    width: this.width() + "px",
    backgroundColor: this.isActive() ? "green" : "teal",
  }));
}
Enter fullscreen mode Exit fullscreen mode
<button [ngStyle]="buttonStyles">Click</button>
Enter fullscreen mode Exit fullscreen mode

Class Binding

btn is a class statically applied to the button while active and primary are dynamically applied.

React - String Manipulation

function MyButton() {
  const [isActive, setIsActive] = useState(true);
  const [isPrimary, setIsPrimary] = useState(false);

  return (
    <button
      className={`btn ${isActive ? "active" : ""} ${
        isPrimary ? "primary" : ""
      }`}
    >
      Click
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Angular - Boolean Binding

export class MyButtonComponent {
  isActive = true;
  isPrimary = false;
}
Enter fullscreen mode Exit fullscreen mode
<button class="btn" [class.active]="isActive" [class.primary]="isPrimary">
  Click
</button>
Enter fullscreen mode Exit fullscreen mode

Or with ngClass for multiple classes:

export class MyButtonComponent {
  isActive = true;
  isPrimary = false;

  get btnClasses() {
    return {
      btn: true,
      active: this.isActive,
      primary: this.isPrimary,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Signals (Angular 16+)

export class MyButtonComponent {
  isActive = signal(true);
  isPrimary = signal(false);

  btnClasses = computed(() => ({
    btn: true,
    active: this.isActive(),
    primary: this.isPrimary(),
  }));
}
Enter fullscreen mode Exit fullscreen mode
<button [ngClass]="btnClasses">Click</button>
Enter fullscreen mode Exit fullscreen mode

Conditional Rendering

React - Logical Operators (AND and Ternary)

function MyComponent() {
  const [isVisible, setIsVisible] = useState(true);
  const [user, setUser] = useState({ name: "John" });

  return (
    <div>
      {isVisible && <p>This is visible</p>}

      {user ? <p>Hello, {user.name}</p> : <p>Please log in</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Angular - Structural Directives (Angular 16-)

export class MyComponent {
  isVisible = true;
  user = { name: "John" };
}
Enter fullscreen mode Exit fullscreen mode
<div>
  <p *ngIf="isVisible">This is visible</p>

  <p *ngIf="user; else noUser">Hello, {{ user.name }}</p>
  <ng-template #noUser>
    <p>Please log in</p>
  </ng-template>
</div>
Enter fullscreen mode Exit fullscreen mode

Angular - Control Flow (Angular 17+)

<div>
  @if (isVisible) {
  <p>This is visible</p>
  } @if (user) {
  <p>Hello, {{ user.name }}</p>
  } @else {
  <p>Please log in</p>
  }
</div>
Enter fullscreen mode Exit fullscreen mode

Lists and Loops

React uses key to help optimize list updates while Angular uses track.

React - Array.map()

function MyComponent() {
  const [items, setItems] = useState([
    { id: 1, name: "Apple" },
    { id: 2, name: "Banana" },
    { id: 3, name: "Orange" },
  ]);

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Angular - ngFor (Angular 16-)

export class MyComponent {
  items = [
    { id: 1, name: "Apple" },
    { id: 2, name: "Banana" },
    { id: 3, name: "Orange" },
  ];

  trackById(index: number, item: any): number {
    return item.id;
  }
}
Enter fullscreen mode Exit fullscreen mode
<ul>
  <li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Angular - @for (Angular 17+)

<ul>
  @for (item of items; track item.id) {
  <li>{{ item.name }}</li>
  }
</ul>
Enter fullscreen mode Exit fullscreen mode

Event Handling

React - Event Binding

In React, an input is considered uncontrolled when its value is accessed via a ref rather than being bound to component state.

function MyComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };

  const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  return (
    <div>
      <button onClick={handleClick}>Count: {count}</button>
      <input onChange={handleInput} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

React - Controlled Components

function MyComponent() {
  const [inputValue, setInputValue] = useState("");

  return (
    <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
  );
}
Enter fullscreen mode Exit fullscreen mode

Angular - Event Binding

export class MyComponent {
  count = 0;

  handleClick() {
    this.count++;
  }

  handleInput(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    console.log(value);
  }
}
Enter fullscreen mode Exit fullscreen mode
<div>
  <button (click)="handleClick()">Count: {{ count }}</button>
  <input (input)="handleInput($event)" />
</div>
Enter fullscreen mode Exit fullscreen mode

Angular - Two-Way Binding

export class MyComponent {
  inputValue = "";
}
Enter fullscreen mode Exit fullscreen mode
<!-- NgModel (requires FormsModule) -->
<input [(ngModel)]="inputValue" />

<!-- Or manually with property + event binding -->
<input [value]="inputValue" (input)="inputValue = $any($event.target).value" />
Enter fullscreen mode Exit fullscreen mode

Angular - Reactive Forms

Requires ReactiveFormsModule

import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";

@Component({
  selector: "app-login",
  templateUrl: "./login.component.html",
})
export class LoginComponent {
  form = new FormGroup({
    email: new FormControl("", {
      nonNullable: true,
      validators: [Validators.required, Validators.email],
    }),
    password: new FormControl("", {
      nonNullable: true,
      validators: Validators.required,
    }),
  });

  submit() {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
<form [formGroup]="form" (ngSubmit)="submit()">
  <input type="email" formControlName="email" />
  <input type="password" formControlName="password" />

  <button type="submit" [disabled]="form.invalid">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Parent Child Communication

React - Props and Callbacks

// Parent Component
function ParentComponent() {
  const [count, setCount] = useState(5);

  const handleIncrement = () => {
    setCount((prev) => prev + 1);
  };

  return (
    <ChildComponent title="Hello" count={count} onIncrement={handleIncrement} />
  );
}

// Child Component
interface ChildProps {
  title: string;
  count: number;
  onIncrement: () => void;
}

function ChildComponent({ title, count, onIncrement }: ChildProps) {
  return (
    <div>
      <h1>{title}</h1>
      <p>Count: {count}</p>
      <button onClick={onIncrement}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Angular - Inputs and Outputs (Angular 16-)

// Parent Component
@Component({
  selector: "app-parent",
  template:
    '<app-child title="Hello" [count]="count" (increment)="handleIncrement()"></app-child>',
})
export class ParentComponent {
  count = 5;

  handleIncrement() {
    this.count++;
  }
}
Enter fullscreen mode Exit fullscreen mode
// Child Component
@Component({
  selector: "app-child",
  template: `
    <div>
      <h1>{{ title }}</h1>
      <p>Count: {{ count }}</p>
      <button (click)="onIncrement()">Increment</button>
    </div>
  `,
})
export class ChildComponent {
  @Input() title!: string;
  @Input() count!: number;
  @Output() increment = new EventEmitter<void>();

  onIncrement() {
    this.increment.emit();
  }
}
Enter fullscreen mode Exit fullscreen mode

Angular - Signal Inputs and Outputs (Angular 17+)

// Child Component
@Component({
  selector: "app-child",
  template: `
    <div>
      <h1>{{ title() }}</h1>
      <p>Count: {{ count() }}</p>
      <button (click)="onIncrement()">Increment</button>
    </div>
  `,
})
export class ChildComponent {
  title = input.required<string>();
  count = input.required<number>();
  increment = output<void>();

  onIncrement() {
    this.increment.emit();
  }
}
Enter fullscreen mode Exit fullscreen mode

Computed / Derived Values

While React's useMemo and Angular’s computed() can be compared, they are not equivalent.

  • useMemo is a performance optimization and does not provide reactive guarantees.
  • computed() creates a declarative reactive value that automatically updates when its dependencies change.

React - Derived State

function ShoppingCart() {
  const [items, setItems] = useState([
    { name: "Apple", price: 1.5, quantity: 3 },
    { name: "Banana", price: 0.8, quantity: 5 },
  ]);

  const total = useMemo(() => {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }, [items]);

  return <p>Total: ${total.toFixed(2)}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Angular - Getter Methods (Angular 15-)

export class ShoppingCartComponent {
  items = [
    { name: "Apple", price: 1.5, quantity: 3 },
    { name: "Banana", price: 0.8, quantity: 5 },
  ];

  get total(): number {
    return this.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
<p>Total: ${{ total | number:'1.2-2' }}</p>
Enter fullscreen mode Exit fullscreen mode

Angular - Computed Signals (Angular 16+)

export class ShoppingCartComponent {
  items = signal([
    { name: "Apple", price: 1.5, quantity: 3 },
    { name: "Banana", price: 0.8, quantity: 5 },
  ]);

  total = computed(() => {
    return this.items().reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
  });
}
Enter fullscreen mode Exit fullscreen mode
<p>Total: ${{ total() | number:'1.2-2' }}</p>
Enter fullscreen mode Exit fullscreen mode

I must confess, I’m actually enjoying learning Angular after having stayed away from it for so long. I guess the next step will be the loved/hated RxJS.

Leave a comment if this article sparked your interest in Angular or React, or if you think it can be improved.

And you, what side of the fence are you on? React or Angular?

Top comments (0)