NgRx SignalStore
NgRx SignalStore is the modern, signal-native state management primitive from the NgRx team. Instead of actions, reducers, and observable selectors, it lets you declare state, derived values, and methods in one cohesive unit built entirely on Angular signals. The result is far less boilerplate than the classic Redux-style Store, while keeping the structure, testability, and tooling you expect from NgRx. For feature state in modern Angular apps, SignalStore is often the sweet spot.
Why SignalStore
A SignalStore is a tree-shakeable Angular service created with signalStore(). You compose it from small features: withState for the data, withComputed for derived values, and withMethods for the logic that updates state. Because everything is a signal, your components read state synchronously in templates with the () call syntax and the new control flow, with no async pipe and no manual subscriptions.
npm install @ngrx/signals
Defining a store
You declare the shape of your state and pass an initial value to withState. withComputed exposes Signal values derived from state, and withMethods returns the imperative API. Use patchState to apply immutable updates.
import { computed } from '@angular/core';
import {
signalStore,
withState,
withComputed,
withMethods,
patchState,
} from '@ngrx/signals';
interface Todo {
id: number;
title: string;
done: boolean;
}
interface TodosState {
todos: Todo[];
filter: 'all' | 'active' | 'done';
}
const initialState: TodosState = {
todos: [],
filter: 'all',
};
export const TodosStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ todos, filter }) => ({
remaining: computed(() => todos().filter((t) => !t.done).length),
visible: computed(() => {
switch (filter()) {
case 'active':
return todos().filter((t) => !t.done);
case 'done':
return todos().filter((t) => t.done);
default:
return todos();
}
}),
})),
withMethods((store) => ({
add(title: string): void {
const todo: Todo = { id: Date.now(), title, done: false };
patchState(store, { todos: [...store.todos(), todo] });
},
toggle(id: number): void {
patchState(store, {
todos: store.todos().map((t) =>
t.id === id ? { ...t, done: !t.done } : t
),
});
},
setFilter(filter: TodosState['filter']): void {
patchState(store, { filter });
},
}))
);
The { providedIn: 'root' } config registers the store as a singleton, exactly like any other root-provided service. Drop it to scope the store to a component’s providers array instead.
Consuming the store in a component
Inject the store with inject() and read its signals directly in the template. Every property declared in withState and withComputed is exposed as a Signal.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { TodosStore } from './todos.store';
@Component({
selector: 'app-todos',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input #box (keyup.enter)="store.add(box.value); box.value = ''" />
<p>{{ store.remaining() }} item(s) left</p>
<ul>
@for (todo of store.visible(); track todo.id) {
<li (click)="store.toggle(todo.id)">
@if (todo.done) { <s>{{ todo.title }}</s> } @else { {{ todo.title }} }
</li>
} @empty {
<li>No todos yet</li>
}
</ul>
`,
})
export class TodosComponent {
protected readonly store = inject(TodosStore);
}
Because the store properties are signals, the template recomputes only the affected bindings when state changes — no zone-wide checks required, which pairs naturally with OnPush.
Output:
2 item(s) left
- Buy milk
- Write docs
Async logic with rxMethod
For side effects such as HTTP calls, the rxMethod operator (from @ngrx/signals/rxjs-interop) bridges signals and RxJS. It returns a reactive method you can call with a value, a signal, or an observable, and it manages subscriptions for you.
import { inject } from '@angular/core';
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap, tap } from 'rxjs';
import { HttpClient } from '@angular/common/http';
export const UsersStore = signalStore(
{ providedIn: 'root' },
withState<{ users: string[]; loading: boolean }>({ users: [], loading: false }),
withMethods((store, http = inject(HttpClient)) => ({
load: rxMethod<void>(
pipe(
tap(() => patchState(store, { loading: true })),
switchMap(() =>
http.get<string[]>('/api/users').pipe(
tap((users) => patchState(store, { users, loading: false }))
)
)
)
),
}))
);
Tip:
rxMethodautomatically unsubscribes when the store is destroyed. You never need anngOnDestroyortakeUntilDestroyedfor store-owned streams.
Entity management and lifecycle hooks
NgRx ships an entity plugin (@ngrx/signals/entities) with withEntities, setAllEntities, addEntity, updateEntity, and removeEntity for normalized collections. The withHooks feature lets you run logic on init and destroy.
import { signalStore, withHooks } from '@ngrx/signals';
import { withEntities } from '@ngrx/signals/entities';
export const ProductsStore = signalStore(
withEntities<{ id: number; name: string }>(),
withHooks({
onInit(store) {
console.log('store ready', store.entities());
},
})
);
SignalStore vs the classic NgRx Store
| Aspect | SignalStore | Classic Store |
|---|---|---|
| Primitive | Signals | Observables |
| Boilerplate | Low (one unit) | High (actions/reducers/selectors) |
| Reading state | store.value() | store.select(...) + async |
| Async | rxMethod | Effects |
| DevTools | Optional plugin | Built in |
| Best for | Feature/local state | Large global, event-sourced state |
Best Practices
- Keep state immutable: always update through
patchState, never mutate arrays or objects in place. - Split logic into small, named store features and compose them, rather than one giant
withMethods. - Put derived data in
withComputedinstead of recomputing it in components or templates. - Use
rxMethodfor async work so subscription lifecycle is handled automatically. - Reach for
withEntitieswhenever you manage collections keyed by an id. - Scope stores to a component’s
providersfor ephemeral feature state, and use{ providedIn: 'root' }only for genuinely shared state.