State is the data that drives your UI: user input, server data, UI selections, etc. In Angular apps — especially as they grow — managing that state so the UI stays predictable, testable, and fast becomes essential.
This article walks through practical approaches to state management in Angular, shows complete code examples (simple and advanced), and gives recommendations for which approach to use for different scenarios.
Table of contents
-
What is application state and why manage it?
-
Approaches at a glance
-
Local component state (when it's enough)
-
Parent–child communication with
@Input
/@Output
-
Shared services + RxJS (recommended for small–medium apps) — complete todo example
-
NgRx (Redux-style) for large apps — complete todo example (actions, reducer, selectors, effects)
-
Comparison: choose the right approach
-
Best practices & performance tips
-
Testing tips
-
Migration notes & closing
1 — What is application state and why manage it?
State = any data your UI depends on (lists, selected item, user session, forms, loading flags, etc.).
Why manage it explicitly?
-
Prevent inconsistent UI (two components disagreeing on the same data).
-
Make the app predictable and easier to debug/test.
-
Improve performance by avoiding unnecessary re-rendering.
-
Keep code maintainable as the app grows.
2 — Approaches at a glance
-
Local component state: simplest; use when state doesn't leave the component.
-
@Input
/@Output
: parent-child communication for small hierarchies. -
Shared services + RxJS (
BehaviorSubject
/ReplaySubject
): lightweight reactive store — great for many apps. -
NgRx (Redux pattern): single immutable store with actions/reducers/selectors/effects — best for complex enterprise apps.
-
Other libraries: NGXS, Akita, or NgRx Component Store (for local feature stores).
3 — Local component state (when it's enough)
If your state only lives in one component, keep it there — simple and fast.
Use this only when data doesn't need to be shared.
4 — Parent–child via @Input
/ @Output
Good for simple data flow between parent/child components. Avoid using this to pass data across many nesting levels.
5 — Shared services + RxJS — the practical, reactive approach
When to use: medium apps, multiple components need the same data, or you want a reactive model without the complexity of NgRx.
Key idea
Create a singleton service that holds the state as RxJS streams (e.g., BehaviorSubject
) and expose Observable
s to components. Components subscribe (usually via async
pipe) and call service methods to update the state.
Example: Todo app using BehaviorSubject
Model
Service
Component (presentation + binding)
Template
Performance tips:
-
Use
ChangeDetectionStrategy.OnPush
. -
Use
trackBy
in*ngFor
. -
Expose
Observable
s and consume viaasync
pipe — avoids manual subscriptions/unsubscriptions.
This pattern scales well for many apps, is easy to test, and keeps code readable.
6 — NgRx (Redux pattern) — for large, complex apps
When to use: large enterprise apps, many teams, complex UI flows, need time-travel debugging, want single source of truth.
NgRx implements a Redux-style architecture:
-
Actions — describe events (user or server).
-
Reducer — pure function that produces new state from previous state + action.
-
Store — holds the app state tree.
-
Selectors — memoized queries into the store.
-
Effects — handle side effects (HTTP, other async operations).
Below is a compact but full NgRx example for the same Todo functionality.
Actions
State & Reducer
Selectors
Effects (handle async loading)
Module setup
Component using the store
Notes:
-
NgRx encourages immutability and pure reducers.
-
Use selectors to memoize and compute derived data.
-
Effects are the recommended place for network calls and side effects.
-
NgRx has optional helpers like
createEntityAdapter()
for normalized collections.
7 — Comparison: which approach when?
Scenario | Recommended approach |
---|---|
Small component-only state | Local component state |
Parent-child simple sharing | @Input / @Output |
Many components, app-level shared state (small–medium) | Service + RxJS (BehaviorSubject ) |
Complex app, many features, many teams, need strict patterns & devtools | NgRx (or NGXS / Akita) |
Feature-local store inside a component tree | NgRx Component Store or Akita feature stores |
8 — Best practices & performance tips
-
Prefer streams: Expose
Observable
s from services and components; consume withasync
pipe. -
OnPush
change detection: Use for UI components showing Observable data. -
Avoid storing derived state: Compute derived values with selectors or pipes.
-
Normalize big collections: Use entity adapters or normalized shapes (by id) to keep updates cheap.
-
Use
trackBy
with*ngFor
. -
Keep reducers pure and avoid side effects in reducers (use effects/services).
-
Lazy load feature state in NgRx to reduce initial bundle size.
-
Use devtools when using NgRx (
@ngrx/store-devtools
) for time-travel debugging. -
Memoize selectors to avoid unnecessary recomputation.
9 — Testing tips
-
Services + BehaviorSubject: Test the service by subscribing to
todos$
and asserting emissions after callingadd()
/remove()
/toggle()
. No need to mock Angular DI — the service is plain TS with RxJS. -
NgRx:
-
Test reducers as pure functions (send action, assert new state).
-
Test effects with
provideMockActions
and mock HTTP calls. -
Use
provideMockStore
for component unit tests to mock store selects and dispatches.
-
Example reducer test (Jasmine):
10 — Migration notes & closing
-
Start simple: adopt services + RxJS. Move to NgRx only when complexity/teams justify it.
-
Hybrid approach: many apps use a combination — NgRx for global domain state and services/component-store for local UI state.
-
Refactor incrementally: extract a feature into NgRx gradually (actions/reducer/selectors) instead of a big rewrite.
Final checklist for implementing state in Angular
-
Decide scope: local vs shared vs global.
-
Prefer immutable updates (spread operator).
-
Use Observables +
async
pipe. -
Optimize change detection with
OnPush
. -
Add unit tests for services, reducers, and effects.
-
For large apps, use NgRx (and devtools); for smaller apps, prefer services + RxJS.