Skip to content
Angular ng state 4 min read

State Management Overview

Every non-trivial Angular application has to answer one question: where does the data live, and who is allowed to change it? State management is the set of patterns that answer that question consistently. Get it wrong and you end up with components silently overwriting each other, stale views, and bugs that only appear in one corner of the app. This page surveys the strategies available in modern Angular — from a single signal in a service to a full Redux-style store — so you can pick the smallest tool that solves your problem.

What counts as state

State is any data that influences what the user sees or how the app behaves. It comes in a few flavours, and they often want different tools:

Kind of stateExamplesTypical home
Local UI statea toggled accordion, a form draftthe component itself (signal)
Shared app statethe logged-in user, theme, cartan injectable service
Server cache statea list fetched from an APIa service + HTTP, or a query library
Router statethe active route, query paramsAngular Router

Tip: A surprising amount of “state” is really server cache. Don’t reach for a global store just to hold the response of one HTTP call — a service usually does the job.

The spectrum of solutions

Angular gives you a sliding scale of power and ceremony. Going right adds structure and tooling at the cost of more boilerplate.

component signal  →  signal service  →  RxJS data service  →  NgRx SignalStore  →  NgRx Store + Effects
   (simplest)                                                                              (most structured)

Component-local signals

For state that no other component cares about, keep it in the component. Signals make this reactive and zoneless-friendly.

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="increment()">Clicked {{ count() }} times</button>
    @if (count() > 4) {
      <p>That's a lot of clicks ({{ doubled() }} doubled).</p>
    }
  `,
})
export class CounterComponent {
  protected readonly count = signal(0);
  protected readonly doubled = computed(() => this.count() * 2);

  increment(): void {
    this.count.update((n) => n + 1);
  }
}

Signal-based services

When two or more components need the same data, lift it into an injectable service and expose signals. This is the modern default for shared state in small-to-medium apps.

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

export interface CartItem {
  id: string;
  name: string;
  price: number;
  qty: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly items = signal<CartItem[]>([]);

  readonly lines = this.items.asReadonly();
  readonly total = computed(() =>
    this.items().reduce((sum, i) => sum + i.price * i.qty, 0),
  );

  add(item: CartItem): void {
    this.items.update((list) => [...list, item]);
  }

  clear(): void {
    this.items.set([]);
  }
}

Any component can inject(CartService) and read cart.total() in its template. The signal is the single source of truth, and mutations only happen through the service’s methods.

RxJS data services

When state is driven by streams — debounced search, WebSocket feeds, combining several async sources — RxJS still shines. A BehaviorSubject (or a signal bridged with toObservable) holds the value, and operators compose the pipeline.

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, switchMap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ProductSearchService {
  private readonly http = inject(HttpClient);
  private readonly query$ = new BehaviorSubject<string>('');

  readonly results$ = this.query$.pipe(
    switchMap((q) => this.http.get<string[]>(`/api/products?q=${q}`)),
  );

  search(term: string): void {
    this.query$.next(term);
  }
}

NgRx for large apps

Once state is touched from many features, has complex transitions, or you need time-travel debugging and strict auditability, a Redux-style store earns its keep. NgRx offers two flavours:

  • NgRx Store + Effects — the classic Redux model: actions, reducers, selectors, and effects for side effects. Maximum structure and tooling.
  • NgRx SignalStore — a lighter, signals-first store with less boilerplate that fits the modern Angular grain.
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';

export const CounterStore = signalStore(
  { providedIn: 'root' },
  withState({ count: 0 }),
  withMethods((store) => ({
    increment: () => patchState(store, (s) => ({ count: s.count + 1 })),
    reset: () => patchState(store, { count: 0 }),
  })),
);

How to choose

The right answer is almost always “the simplest thing that works.” Use this rough guide:

SituationReach for
State used by one componentA component signal
Shared across a few componentsA signal service
Stream-heavy / async orchestrationAn RxJS data service
Many features, complex transitionsNgRx SignalStore
Large team, strict patterns, devtoolsNgRx Store + Effects

Warning: Adding NgRx to a small app is a common over-engineering trap. The boilerplate slows you down without paying off until the state graph is genuinely large and shared.

Best Practices

  • Start with signals in components and services; only escalate to a store when sharing and complexity demand it.
  • Keep state mutations behind methods on a service or store — never let components write to shared state directly.
  • Treat server responses as cache, not application state; cache invalidation is a separate concern from UI state.
  • Expose state as readonly signals (asReadonly()) so consumers can read but not mutate.
  • Derive, don’t duplicate: use computed() and selectors instead of storing values you can calculate.
  • Pick one primary approach per feature; mixing many patterns in the same area makes the data flow hard to follow.
Last updated June 14, 2026
Was this helpful?