On this page
State Management
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
asyncpipe
Choose the simplest pattern that fits your app’s complexity — signals cover most modern Angular applications without extra libraries.