Skip to content
Angular ng state 4 min read

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.

ConceptRolePure?
ActionDescribes an event (“what happened”), carries an optional payloadn/a
Reducer(state, action) => newState — computes next stateYes
StoreSingle source of truth; observable holding staten/a
SelectorReads and derives a slice of stateYes

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’s providers array 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/store runtime checks (strictStateImmutability) in dev mode.
  • Name actions descriptively as [Source] Event so the DevTools log reads like a story; prefer event-based actions over generic setters.
  • Read state only through selectors — never reach into store shape directly in components — so derivations stay memoized and refactorable.
  • Co-locate actions, reducer, and selectors per feature, and register feature state lazily with provideState.
  • Reach for NgRx when state is shared widely and mutated from many places; for local or simple state, signals or a service are lighter.
Last updated June 14, 2026
Was this helpful?