State is any data your application needs to share or react to — user info, cart items, UI flags. Angular offers signals for local and shared state, plus libraries like NgRx for complex global state.

Signals — Fine-Grained Reactivity

Signals (Angular 16+) hold values and notify consumers when they change:

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubleCount() }}</p>
    <button (click)="increment()">+1</button>
  `
})
export class CounterComponent {
  count = signal(0);
  doubleCount = computed(() => this.count() * 2);

  constructor() {
    effect(() => {
      console.log('Count changed:', this.count());
    });
  }

  increment() {
    this.count.update(c => c + 1);
  }
}
  
API Purpose
signal(initial) Writable signal
computed(() => ...) Derived read-only value
effect(() => ...) Run side effects when dependencies change
.set(value) / .update(fn) Change a writable signal

Call signals as functions in templates: {{ count() }}.

Signal-Based Inputs and Model

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

@Component({
  selector: 'app-rating',
  standalone: true,
  template: `
    @for (star of [1,2,3,4,5]; track star) {
      <button (click)="value.set(star)">{{ star <= value() ? '★' : '☆' }}</button>
    }
  `
})
export class RatingComponent {
  value = model(0);           // Two-way bindable: [(value)]="rating"
  max = input(5);               // Read-only input from parent
}
  

Shared State with a Service

Combine signals with DI for app-wide state:

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

  toggle() {
    this.theme.update(t => t === 'light' ? 'dark' : 'light');
  }
}
  
  @Component({
  template: `
    <div [class.dark]="theme() === 'dark'">
      <button (click)="themeService.toggle()">Toggle theme</button>
    </div>
  `
})
export class HeaderComponent {
  themeService = inject(ThemeService);
  theme = this.themeService.theme;
}
  

When to Use NgRx

For large apps with complex async flows, NgRx applies Redux patterns:

  Action → Reducer → Store → Selector → Component
                ↑
            Effects (side effects)
  
  // Simplified NgRx pattern
export const loadUsers = createAction('[Users] Load');
export const loadUsersSuccess = createAction(
  '[Users] Load Success',
  props<{ users: User[] }>()
);

export const usersReducer = createReducer(
  initialState,
  on(loadUsersSuccess, (state, { users }) => ({ ...state, users }))
);
  

NgRx adds structure and devtools but also boilerplate. Start with services + signals; adopt NgRx when complexity demands it.

State Strategy Guide

Pattern Best for
Component signals Local UI state (tabs, toggles)
Service + signals Shared state across a feature
RxJS BehaviorSubject Legacy code or stream-heavy APIs
NgRx Store Large teams, time-travel debugging, complex workflows

Avoiding Common Pitfalls

  • Do not duplicate server state in multiple places — fetch once, share via service
  • Prefer immutable updates in reducers and signal updaters
  • Use computed() instead of manually syncing derived values
  • Unsubscribe from Observables or use the async pipe

Choose the simplest pattern that fits your app’s complexity — signals cover most modern Angular applications without extra libraries.