DEV Community

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

Posted 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

React Concept Angular Equivalent
useState Component properties / signals
Context API @Injectable Service (singleton via DI)
useEffect Lifecycle hooks (ngOnInit, ngOnDestroy) / effect()
Props / parent → child data @Input() / signal inputs
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

Global / 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

Class Binding

Let's assume 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

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

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 / 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

Data from parents

React - Props

// Parent Component
function ParentComponent() {
  return <ChildComponent title="Hello" count={5} />;
}

// Child Component
interface ChildProps {
  title: string;
  count: number;
}

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

Angular - Inputs (Angular 16-)

// Parent Component
@Component({
  selector: "app-parent",
  template: '<app-child title="Hello" [count]="5"></app-child>',
})
export class ParentComponent {}
Enter fullscreen mode Exit fullscreen mode
// Child Component
@Component({
  selector: "app-child",
  template: `
    <div>
      <h1>{{ title }}</h1>
      <p>Count: {{ count }}</p>
    </div>
  `,
})
export class ChildComponent {
  @Input() title!: string;
  @Input() count!: number;
}
Enter fullscreen mode Exit fullscreen mode

Angular - Inputs (Angular 17+)

// Child Component with signal inputs
@Component({
  selector: "app-child",
  template: `
    <div>
      <h1>{{ title() }}</h1>
      <p>Count: {{ count() }}</p>
    </div>
  `,
})
Enter fullscreen mode Exit fullscreen mode
export class ChildComponent {
  title = input.required<string>();
  count = input.required<number>();
}
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

Lifecycle and Side Effects

React - useEffect

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

  useEffect(() => {
    // Runs when component mounts or userId changes
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));

    // Runs when component unmounts
    return () => {
      console.log("Cleanup");
    };
    // [userId] defines updates
  }, [userId]);

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

Angular - Lifecycle Hooks (Angular 15-)

export class UserProfileComponent implements OnInit, OnDestroy {
  @Input() userId!: number;
  user: any = null;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    // Runs when component initializes
    this.http
      .get(`/api/users/${this.userId}`)
      .subscribe((data) => (this.user = data));
  }

  ngOnDestroy() {
    // Runs when component is destroyed
    console.log("Cleanup");
  }
}
Enter fullscreen mode Exit fullscreen mode

Angular - effect() with Signals (Angular 16+)

export class UserProfileComponent {
  userId = input.required<number>();
  user = signal<any>(null);

  constructor() {
    // Runs when component initializes
    effect((onCleanup) => {
      const controller = new AbortController();

      // Runs when userId changes
      fetch(`/api/users/${this.userId()}`, { signal: controller.signal })
        .then((res) => res.json())
        .then((data) => this.user.set(data));

      // Runs when component is destroyed
      onCleanup(() => controller.abort());
    });
  }
}
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)