Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 11: State Management in Angular

Every application needs to manage state — the data that drives what users see on the screen.
Examples of state:

  • A shopping cart’s contents.
  • A logged-in user’s profile.
  • A list of posts fetched from an API.

As apps grow, managing state becomes more complex. Angular provides several options, ranging from simple local state in components to enterprise-scale solutions like NgRx.


11.1 Local State

The simplest way to manage state is inside a component.

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = 0;

  increment() {
    this.count++;
  }
}

✅ Works fine for isolated components.
❌ Breaks down when multiple components need the same state.


11.2 Parent → Child

Use @Input() on the child to accept data from the parent.

Child (profile-card.component.ts)

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-profile-card',
  standalone: true,
  template: `
    <article>
      <h3>{{ name }}</h3>
      <p *ngIf="bio">{{ bio }}</p>
    </article>
  `
})
export class ProfileCardComponent {
  @Input() name = 'Anonymous';
  @Input() bio?: string;
}

Parent (team.component.ts)

import { Component } from '@angular/core';
import { ProfileCardComponent } from './profile-card.component';

@Component({
  selector: 'app-team',
  standalone: true,
  imports: [ProfileCardComponent],
  template: `
    <app-profile-card
      [name]="leadName"
      [bio]="leadBio">
    </app-profile-card>
  `
})
export class TeamComponent {
  leadName = 'Aisha Khan';
  leadBio  = 'Frontend lead focused on design systems.';
}

Key idea: data flows down from parent to child.


11.3 Sibling ↔ Sibling

Components can’t pass inputs directly to siblings. The classic pattern is:

  1. Sibling A emits an event (e.g., a selected value)
  2. Parent handles the event and updates state
  3. Parent passes updated state down to Sibling B via @Input()

Sibling A: selects a color (color-picker.component.ts)

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-color-picker',
  standalone: true,
  template: `
    <label>Pick a color:</label>
    <button (click)="choose('red')">Red</button>
    <button (click)="choose('green')">Green</button>
    <button (click)="choose('blue')">Blue</button>
  `
})
export class ColorPickerComponent {
  @Output() colorChange = new EventEmitter<string>();
  choose(color: string) { this.colorChange.emit(color); }
}

Sibling B: displays the color (color-preview.component.ts)

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-color-preview',
  standalone: true,
  template: `
    <div
      style="width:120px;height:60px;border:1px solid #ccc;"
      [style.background]="color">
    </div>
    <p>Selected: {{ color || 'none' }}</p>
  `
})
export class ColorPreviewComponent {
  @Input() color = '';
}

Parent: mediates between siblings (color-playground.component.ts)

import { Component } from '@angular/core';
import { ColorPickerComponent } from './color-picker.component';
import { ColorPreviewComponent } from './color-preview.component';

@Component({
  selector: 'app-color-playground',
  standalone: true,
  imports: [ColorPickerComponent, ColorPreviewComponent],
  template: `
    <app-color-picker (colorChange)="onColor($event)"></app-color-picker>
    <app-color-preview [color]="selectedColor"></app-color-preview>
  `
})
export class ColorPlaygroundComponent {
  selectedColor = '';
  onColor(color: string) { this.selectedColor = color; }
}

Why this matters: This “lift state up” pattern keeps siblings decoupled and predictable.

Tip: For many cross-sibling interactions across distant branches, prefer a shared service with signals or a global store (NgRx) instead of chaining many parent mediators.


11.5 Using @ViewChild()

@ViewChild() lets a parent query a child (component, directive, or native element) that exists in its own template. Common uses:

  • Call a public method on the child component
  • Focus an input (access native element)
  • Read internal child state (read-only preferred)

A) Call a child component method

Child (modal.component.ts)

import { Component } from '@angular/core';

@Component({
  selector: 'app-modal',
  standalone: true,
  template: `
    <dialog #dlg>
      <h3>Modal</h3>
      <p>Hi there!</p>
      <button (click)="close()">Close</button>
    </dialog>
  `
})
export class ModalComponent {
  private dialogEl!: HTMLDialogElement;

  // Angular calls after view init; link the native element
  ngAfterViewInit() {
    this.dialogEl = (document.querySelector('app-modal dialog') as HTMLDialogElement);
  }

  open()  { this.dialogEl?.showModal(); }
  close() { this.dialogEl?.close(); }
}

Parent uses @ViewChild to drive it (page.component.ts)

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ModalComponent } from './modal.component';

@Component({
  selector: 'app-page',
  standalone: true,
  imports: [ModalComponent],
  template: `
    <button (click)="open()">Open Modal</button>
    <app-modal></app-modal>
  `
})
export class PageComponent implements AfterViewInit {
  @ViewChild(ModalComponent) modal?: ModalComponent;

  ngAfterViewInit() {
    // modal is available here
  }

  open() {
    this.modal?.open();
  }
}

Note: In real apps, it’s cleaner to reference the dialog with a @ViewChild('dlg') template ref rather than querySelector; shown here for clarity. See the native element example below.

B) Focus an input via native element

Template reference + ElementRef

import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input #term type="text" placeholder="Search…" />
    <button (click)="focus()">Focus</button>
  `
})
export class SearchComponent implements AfterViewInit {
  @ViewChild('term') termInput?: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    // Optionally autofocus when ready:
    this.termInput?.nativeElement.focus();
  }

  focus() {
    this.termInput?.nativeElement.focus();
  }
}

Key points

  • @ViewChild() resolves after the view is initialized → use ngAfterViewInit.
  • Prefer calling public methods on a child rather than poking internal fields.
  • For DOM, access via ElementRef and avoid direct DOM manipulation when possible (use Angular bindings).
  • If the element is created conditionally (*ngIf), set @ViewChild(..., { static: false }) (default) and ensure it exists before using it.

When to use which?

  • @Input() (parent → child): pass data down. Simple and explicit.
  • Sibling ↔ Sibling via Parent: raise an event from one sibling, update state in parent, pass down to the other via @Input().
    • For many siblings/features → consider a shared service with signals or NgRx.
  • @ViewChild() (parent → child instance): call public methods (e.g., open modal, reset form) or interact with native elements (focus). Use sparingly to avoid tight coupling.

11.6 Service-Based State (Shared Services)

The most common Angular pattern is to use services with DI to share state.

@Injectable({ providedIn: 'root' })
export class CartService {
  private items: string[] = [];

  addItem(item: string) { this.items.push(item); }
  getItems() { return this.items; }
}

Injected into components:

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <button (click)="add()">Add Item</button>
    <ul><li *ngFor="let item of items">{{ item }}</li></ul>
  `
})
export class CartComponent {
  items: string[] = [];
  constructor(private cart: CartService) {
    this.items = this.cart.getItems();
  }
  add() { this.cart.addItem('Book'); }
}

✅ Simple, effective, easy to test.
❌ State changes don’t automatically notify components (unless combined with Observables or Signals).


11.7 State with RxJS Observables

Before signals, the main reactive pattern was BehaviorSubject or ReplaySubject.

@Injectable({ providedIn: 'root' })
export class UserService {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();

  setUser(user: User) { this.userSubject.next(user); }
}

Component:

@Component({
  selector: 'app-profile',
  standalone: true,
  template: `<p *ngIf="user$ | async as user">Welcome, {{ user.name }}</p>`
})
export class ProfileComponent {
  user$ = this.userService.user$;
  constructor(private userService: UserService) {}
}

✅ Very powerful, integrates with Angular’s async pipe.
❌ More boilerplate and learning curve than signals.


11.8 @Input() with Signals

Normally, @Input() is just a property that Angular assigns when the parent updates it. But with Angular’s signal inputs, you can turn inputs into reactive signals that auto-update when the parent changes.


Example 1: Simple Input Signal

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-greeting',
  standalone: true,
  template: `<h2>Hello, {{ name() }}!</h2>`
})
export class GreetingComponent {
  // input() makes a signal input
  name = input<string>('Guest');
}

Parent component:

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [GreetingComponent],
  template: `
    <app-greeting [name]="userName"></app-greeting>
    <button (click)="userName = 'Alice'">Set Name</button>
  `
})
export class HomeComponent {
  userName = 'Bob';
}
  • Initially renders: Hello, Bob!
  • After clicking button: Hello, Alice!
  • If no name input is provided, defaults to Guest.

Example 2: Computed Signal from an Input

You can derive values from inputs using computed().

import { Component, input, computed } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <h3>{{ displayName() }}</h3>
  `
})
export class UserCardComponent {
  firstName = input<string>();
  lastName = input<string>();

  displayName = computed(() =>
    `${this.firstName() ?? ''} ${this.lastName() ?? ''}`.trim()
  );
}

Parent component:

<app-user-card [firstName]="'Jane'" [lastName]="'Doe'"></app-user-card>

Renders: Jane Doe.


Example 3: Input Signals with Defaults

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Start: {{ start() }}</p>
    <p>Count: {{ count }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  start = input<number>(0); // default value
  count = 0;

  increment() {
    this.count++;
  }
}

If parent provides [start]="10", it shows Start: 10. If not, defaults to 0.


Key Benefits of Input Signals

  • ✅ Reactive by default — no need for ngOnChanges().
  • ✅ Can be combined with computed signals.
  • ✅ Can provide default values.
  • ✅ Cleaner and more ergonomic than traditional @Input().

11.9 NgRx: Enterprise-Scale State Management

For large applications, Angular offers NgRx, inspired by Redux. NgRx uses:

  • Store – a single global state tree.
  • Actions – events that describe state changes.
  • Reducers – pure functions that update state.
  • Effects – handle side effects (API calls).

Example: Counter with NgRx

Action:

export const increment = createAction('[Counter] Increment');

Reducer:

export const counterReducer = createReducer(0,
  on(increment, state => state + 1)
);

Component:

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count$ | async }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count$ = this.store.select('count');
  constructor(private store: Store<{ count: number }>) {}
  increment() { this.store.dispatch(increment()); }
}

✅ Scales to very large apps.
✅ Predictable and testable.
❌ Steeper learning curve and boilerplate-heavy for small apps.


11.7 Choosing the Right Approach

ApproachBest For
Local Component StateSimple, isolated UI state
@Input/@OutputParent-child communication
Service (Plain)Shared logic/data between few components
Service + SignalsMedium apps needing reactivity
RxJS ObservablesComplex async streams, multi-subscriber
NgRxEnterprise-scale apps with global state

11.8 Best Practices

  • Start simple (component or service).
  • Introduce signals for reactivity instead of manually wiring RxJS everywhere.
  • Use RxJS when you truly need streams or complex async handling.
  • Adopt NgRx only when your app grows beyond what services/signals can handle.

11.9 Summary

  • State management ranges from local variables to enterprise libraries.
  • Signals bring fine-grained reactivity to Angular apps.
  • NgRx is still the go-to for large-scale, complex apps.
  • Always choose the simplest tool that meets your needs.