NgRx Store (Classic)
NgRx implements the Redux pattern for Angular. State is immutable; changes happen through actions dispatched to reducers.
Installation
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools
Core Concepts
| Concept | Role |
|---|---|
| Store | Single source of truth; holds all application state |
| Action | Describes what happened ([Source] Event) |
| Reducer | Pure function: (state, action) → newState |
| Selector | Memoized projection of state |
| Effect | Handles side effects (HTTP, routing) triggered by actions |
Actions
import { createAction, props } from '@ngrx/store';
export const loadAlbums = createAction('[Album] Load');
export const loadAlbumsSuccess = createAction(
'[Album] Load Success',
props<{ albums: Album[] }>()
);
export const loadAlbumsFailure = createAction(
'[Album] Load Failure',
props<{ error: string }>()
);
Reducer
import { createReducer, on } from '@ngrx/store';
export interface AlbumState {
albums: Album[];
loading: boolean;
error: string | null;
}
const initialState: AlbumState = { albums: [], loading: false, error: null };
export const albumReducer = createReducer(
initialState,
on(loadAlbums, state => ({ ...state, loading: true })),
on(loadAlbumsSuccess, (state, { albums }) => ({ ...state, albums, loading: false })),
on(loadAlbumsFailure, (state, { error }) => ({ ...state, error, loading: false }))
);
Selectors
import { createFeatureSelector, createSelector } from '@ngrx/store';
const selectAlbumState = createFeatureSelector<AlbumState>('albums');
export const selectAllAlbums = createSelector(selectAlbumState, s => s.albums);
export const selectLoading = createSelector(selectAlbumState, s => s.loading);
Effects
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, map, catchError, of } from 'rxjs';
@Injectable()
export class AlbumEffects {
load$ = createEffect(() =>
this.actions$.pipe(
ofType(loadAlbums),
switchMap(() =>
this.albumService.getAlbums().pipe(
map(albums => loadAlbumsSuccess({ albums })),
catchError(err => of(loadAlbumsFailure({ error: err.message })))
)
)
)
);
constructor(private actions$: Actions, private albumService: AlbumService) {}
}
Wiring (app.config.ts)
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ albums: albumReducer }),
provideEffects([AlbumEffects]),
provideStoreDevtools({ maxAge: 25 }),
],
};
Using in a Component
export class AlbumListComponent {
private store = inject(Store);
albums$ = this.store.select(selectAllAlbums);
loading$ = this.store.select(selectLoading);
ngOnInit() {
this.store.dispatch(loadAlbums());
}
}
@if (loading$ | async) { <p>Loading…</p> }
@for (album of albums$ | async; track album.id) {
<app-album-card [album]="album" />
}
NgRx vs Signal Store
| Feature | NgRx Classic | Signal Store |
|---|---|---|
| Pattern | Redux | Signals-native |
| Boilerplate | High | Low |
| DevTools | Redux DevTools | NgRx DevTools (Angular 17+) |
| Reactivity | Observables | Signals |
| Best for | Large teams, complex state | Modern Angular apps |