State Management
Component State (Simple)
@Component({
selector: 'app-counter'
})
export class CounterComponent {
count = 0; // Local state
increment() {
this.count++;
}
}
Signals (Modern)
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-todo-list'
})
export class TodoListComponent {
todos = signal<Todo[]>([]);
filter = signal<'all' | 'active' | 'completed'>('all');
filteredTodos = computed(() => {
const filter = this.filter();
const todos = this.todos();
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
});
addTodo(title: string) {
this.todos.update(todos => [
...todos,
{ id: Date.now(), title, completed: false }
]);
}
toggleTodo(id: number) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
}
Service-based State
@Injectable({ providedIn: 'root' })
export class UserService {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSubject.asObservable();
setUser(user: User) {
this.userSubject.next(user);
}
getUser(): Observable<User | null> {
return this.user$;
}
clearUser() {
this.userSubject.next(null);
}
}
// Component usage
@Component({})
export class ProfileComponent {
userService = inject(UserService);
user$ = this.userService.user$;
}
Signal-based Service
@Injectable({ providedIn: 'root' })
export class TodoStore {
private state = signal<TodoState>({
todos: [],
filter: 'all',
loading: false
});
// Selectors
todos = computed(() => this.state().todos);
filter = computed(() => this.state().filter);
loading = computed(() => this.state().loading);
filteredTodos = computed(() => {
const todos = this.todos();
const filter = this.filter();
// Filter logic...
return todos;
});
// Actions
addTodo(title: string) {
this.state.update(state => ({
...state,
todos: [...state.todos, { id: Date.now(), title, completed: false }]
}));
}
setFilter(filter: 'all' | 'active' | 'completed') {
this.state.update(state => ({ ...state, filter }));
}
async loadTodos() {
this.state.update(state => ({ ...state, loading: true }));
try {
const todos = await this.http.get<Todo[]>('/api/todos').toPromise();
this.state.update(state => ({ ...state, todos, loading: false }));
} catch (error) {
this.state.update(state => ({ ...state, loading: false }));
}
}
}
NgRx (Enterprise)
Store Setup
// app.config.ts
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ todos: todoReducer }),
provideEffects([TodoEffects])
]
};
Actions
import { createAction, props } from '@ngrx/store';
export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction(
'[Todo] Load Todos Success',
props<{ todos: Todo[] }>()
);
export const addTodo = createAction(
'[Todo] Add Todo',
props<{ title: string }>()
);
Reducer
import { createReducer, on } from '@ngrx/store';
export interface TodoState {
todos: Todo[];
loading: boolean;
}
const initialState: TodoState = {
todos: [],
loading: false
};
export const todoReducer = createReducer(
initialState,
on(loadTodos, state => ({ ...state, loading: true })),
on(loadTodosSuccess, (state, { todos }) => ({
...state,
todos,
loading: false
})),
on(addTodo, (state, { title }) => ({
...state,
todos: [...state.todos, { id: Date.now(), title, completed: false }]
}))
);
Selectors
import { createSelector, createFeatureSelector } from '@ngrx/store';
export const selectTodoState = createFeatureSelector<TodoState>('todos');
export const selectAllTodos = createSelector(
selectTodoState,
state => state.todos
);
export const selectActiveTodos = createSelector(
selectAllTodos,
todos => todos.filter(t => !t.completed)
);
export const selectLoading = createSelector(
selectTodoState,
state => state.loading
);
Effects
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { map, mergeMap, catchError } from 'rxjs/operators';
@Injectable()
export class TodoEffects {
actions$ = inject(Actions);
todoService = inject(TodoService);
loadTodos$ = createEffect(() =>
this.actions$.pipe(
ofType(loadTodos),
mergeMap(() =>
this.todoService.getTodos().pipe(
map(todos => loadTodosSuccess({ todos })),
catchError(() => of(loadTodosFailure()))
)
)
)
);
}
Component Usage
import { Store } from '@ngrx/store';
@Component({})
export class TodoListComponent {
store = inject(Store);
todos$ = this.store.select(selectAllTodos);
loading$ = this.store.select(selectLoading);
ngOnInit() {
this.store.dispatch(loadTodos());
}
addTodo(title: string) {
this.store.dispatch(addTodo({ title }));
}
}
Comparison
| Approach | Best For | Complexity | Performance |
|---|---|---|---|
| Component State | Simple, isolated state | Low | Good |
| Signals | Local reactive state | Low | Excellent |
| Service + BehaviorSubject | Shared state | Medium | Good |
| Signal Service | Shared reactive state | Medium | Excellent |
| NgRx | Enterprise, complex apps | High | Excellent |
Best Practices
Signal-based (Recommended for new apps)
@Injectable({ providedIn: 'root' })
export class AppStore {
// Private state
private state = signal<AppState>(initialState);
// Public selectors
user = computed(() => this.state().user);
isLoggedIn = computed(() => !!this.user());
// Actions (mutate state)
login(user: User) {
this.state.update(s => ({ ...s, user }));
}
logout() {
this.state.update(s => ({ ...s, user: null }));
}
}
RxJS-based (Legacy apps)
@Injectable({ providedIn: 'root' })
export class AppStore {
private stateSubject = new BehaviorSubject<AppState>(initialState);
private state$ = this.stateSubject.asObservable();
user$ = this.state$.pipe(map(s => s.user));
isLoggedIn$ = this.user$.pipe(map(u => !!u));
login(user: User) {
const current = this.stateSubject.value;
this.stateSubject.next({ ...current, user });
}
}
When to Use What
- Component state: Simple, isolated features
- Signals: Modern reactive local/shared state
- Service + BehaviorSubject: Legacy shared state
- NgRx: Large apps, complex state, team standards
- Component store (NgRx): Feature-specific state