Skip to main content

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

ConceptRole
StoreSingle source of truth; holds all application state
ActionDescribes what happened ([Source] Event)
ReducerPure function: (state, action) → newState
SelectorMemoized projection of state
EffectHandles 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

FeatureNgRx ClassicSignal Store
PatternReduxSignals-native
BoilerplateHighLow
DevToolsRedux DevToolsNgRx DevTools (Angular 17+)
ReactivityObservablesSignals
Best forLarge teams, complex stateModern Angular apps