NgRx Store Fundamentals
NgRx is a Redux-inspired state management library for Angular built on RxJS. It centralizes your application state in a single, immutable store and enforces a strict, one-way data flow: components dispatch actions, pure reducers compute the next state, and selectors read slices back out. This predictability makes large apps easier to reason about, debug, and test — at the cost of more boilerplate than signals or a plain service.
The unidirectional data flow
NgRx revolves around four pieces that form a loop. A component dispatches an action describing what happened. A reducer receives the current state plus that action and returns a brand-new state object. The store holds that state and notifies subscribers. Selectors project the state into the exact shape a component needs.
| Concept | Role | Pure? |
|---|---|---|
| Action | Describes an event (“what happened”), carries an optional payload | n/a |
| Reducer | (state, action) => newState — computes next state | Yes |
| Store | Single source of truth; observable holding state | n/a |
| Selector | Reads and derives a slice of state | Yes |
Reducers must be pure: no HTTP calls, no mutation, no
Date.now(). Side effects belong in NgRx Effects, not reducers.
Defining actions
Actions are plain objects with a type string. The createAction helper gives you a strongly typed action creator. Group related actions and name their type as [Source] Event so they read clearly in the DevTools log.
// counter.actions.ts
import { createAction, props } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
export const setBy = createAction(
'[Counter] Set By',
props<{ amount: number }>(),
);
props<{ amount: number }>() declares the payload shape. Calling setBy({ amount: 5 }) returns { type: '[Counter] Set By', amount: 5 }.
Writing the reducer
The reducer owns a slice’s initial state and maps actions to new state. createReducer pairs each action with an on handler. Always return a new object — spread the previous state rather than mutating it.
// counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset, setBy } from './counter.actions';
export interface CounterState {
count: number;
}
export const initialState: CounterState = { count: 0 };
export const counterReducer = createReducer(
initialState,
on(increment, (state) => ({ ...state, count: state.count + 1 })),
on(decrement, (state) => ({ ...state, count: state.count - 1 })),
on(reset, () => initialState),
on(setBy, (state, { amount }) => ({ ...state, count: state.count + amount })),
);
Registering the store
In a standalone app, wire NgRx through provideStore in app.config.ts. Each feature slice is keyed by name. Add provideStoreDevtools so the Redux DevTools extension can inspect every dispatch and time-travel through state.
// app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { counterReducer } from './counter.reducer';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ counter: counterReducer }),
provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
],
};
For lazy-loaded feature routes, register slices locally with
provideState('feature', featureReducer)in the route’sprovidersarray instead of bloating the root store.
Selecting state
Selectors are memoized, composable functions that read from the store. createFeatureSelector grabs a top-level slice by key; createSelector derives values from it and caches the result until its inputs change — so derived computations don’t re-run on unrelated state updates.
// counter.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CounterState } from './counter.reducer';
export const selectCounter =
createFeatureSelector<CounterState>('counter');
export const selectCount = createSelector(
selectCounter,
(state) => state.count,
);
export const selectIsEven = createSelector(
selectCount,
(count) => count % 2 === 0,
);
Wiring up a component
Inject the Store, read selectors as observables, and dispatch actions on user events. Modern Angular pairs nicely with toSignal so you can drop the async pipe and use the new @if/@for control flow against plain signals.
// counter.component.ts
import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { toSignal } from '@angular/core/rxjs-interop';
import { increment, decrement, reset } from './counter.actions';
import { selectCount, selectIsEven } from './counter.selectors';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
@if (isEven()) {
<p>The count is even.</p>
}
<button (click)="store.dispatch(increment())">+</button>
<button (click)="store.dispatch(decrement())">-</button>
<button (click)="store.dispatch(reset())">Reset</button>
`,
})
export class CounterComponent {
readonly store = inject(Store);
readonly count = toSignal(this.store.select(selectCount), { initialValue: 0 });
readonly isEven = toSignal(this.store.select(selectIsEven), {
initialValue: true,
});
}
Clicking + twice produces this DevTools action log and rendered state.
Output:
[Counter] Increment { count: 1 }
[Counter] Increment { count: 2 }
Count: 2
The count is even.
Best practices
- Keep reducers pure and synchronous — push HTTP, timers, and routing into effects.
- Treat state as immutable; always spread instead of mutating, and consider
@ngrx/storeruntime checks (strictStateImmutability) in dev mode. - Name actions descriptively as
[Source] Eventso the DevTools log reads like a story; prefer event-based actions over generic setters. - Read state only through selectors — never reach into
storeshape directly in components — so derivations stay memoized and refactorable. - Co-locate
actions,reducer, andselectorsper feature, and register feature state lazily withprovideState. - Reach for NgRx when state is shared widely and mutated from many places; for local or simple state, signals or a service are lighter.