Managing Localized Reactive State with NgRx SignalStore: A Practical Guide
NgRx SignalStore lets you keep component‑level state concise and reactive by exposing source signals and computed signals. This post walks through a concrete todo‑list example, highlights trade‑offs, and gives actionable steps to start using SignalStore in your Angular app.
12 Jul 2026, 13:08 UTC

Problem: Localized State with Derived View Data
In many Angular apps, a component needs its own small slice of state—think a todo list, a filter form, or a modal dialog. The classic NgRx Store forces you to define actions, reducers, selectors, and often effects even when the state is local to a single component. This results in a lot of boilerplate for very little gain.
What you really want is: 1) a mutable source that stays immutable under the hood, 2) derived data that updates automatically, and 3) a clean, readable API for components. NgRx SignalStore addresses exactly that.
SignalStore 101 – A Declarative Store on Angular Signals
SignalStore is a lightweight NgRx feature that builds on Angular’s signals API. It replaces the classic Store + selector pattern with a single, declarative store definition:
source– the raw state exposed as a readonly signal.updaters– functions that perform immutable state transitions viapatchState.computed– derived signals that memoize expensive calculations (e.g., filtering, sorting).
Components read the source and computed signals directly; no async pipe or manual subscription is necessary. This eliminates the typical “observable‑to‑template” boilerplate.
Concrete Example: Todo List with Filter
Below is a minimal, reproducible example that shows how to set up a SignalStore for a todo list that supports filtering by status.
// todo-store.ts
import { signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import { signal } from '@angular/core';
export interface Todo {
id: number;
title: string;
completed: boolean;
}
export interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
}
export const todoStore = signalStore(
withState<TodoState>({
todos: [],
filter: 'all'
}),
withMethods(<T extends TodoState>({
addTodo: (todo: Todo) => void,
toggleTodo: (id: number) => void,
setFilter: (filter: 'all' | 'active' | 'completed') => void
}) => ({
addTodo: (todo) => patchState({ todos: [...state.todos, todo] }),
toggleTodo: (id) => patchState({
todos: state.todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
}),
setFilter: (filter) => patchState({ filter })
})),
withComputed(<T extends TodoState>({
filteredTodos: () => signal(Todo[])
}) => ({
filteredTodos: () => signal(
state.todos.filter(t => {
if (state.filter === 'all') return true;
if (state.filter === 'active') return !t.completed;
return t.completed;
})
)
}))
);
Key points:
- All state lives inside
todoStore; components never mutate it directly. - The
filteredTodoscomputed signal automatically recalculates only whenstate.todosorstate.filterchanges. - Because computed signals are memoized, repeated reads in the template are cheap.
Using the Store in a Component
// todo.component.ts
import { Component } from '@angular/core';
import { todoStore } from './todo-store';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html'
})
export class TodoComponent {
todos = todoStore.filteredTodos;
add = todoStore.addTodo;
toggle = todoStore.toggleTodo;
setFilter = todoStore.setFilter;
}
Template:
<div>
<input type="text" #newTodo (keydown.enter)="add({ id: Date.now(), title: newTodo.value, completed: false })" placeholder="New todo" />
<ul>
<li *ngFor="let t of todos()">
<label>
<input type="checkbox" [checked]="t.completed" (change)="toggle(t.id)" />
{{ t.title }}
</label>
</li>
</ul>
<div>
<button (click)="setFilter('all')">All</button>
<button (click)="setFilter('active')">Active</button>
<button (click)="setFilter('completed')">Completed</button>
</div>
</div>
Notice that the template calls todos() to read the computed signal. No async pipe, no manual subscription, and no change‑detection gymnastics.
Trade‑offs and Limitations
- Scope vs. Global State – SignalStore is ideal for component‑level or feature‑level state. If you need to share state across unrelated components, the classic NgRx Store (with actions, reducers, selectors) remains the better choice.
- Side‑Effect Handling – SignalStore does not replace
@ngrx/effects. For async operations (HTTP calls, websockets), you still need to integrate RxJS or Angular services that dispatch updates via the store’s updaters. - Angular Signals Enablement – Your project must enable Angular signals (Angular 16+). Mixing signal reads with manual
subscribepatterns in the same component can lead to subtle change‑detection bugs. - API Stability – The
signalStoreAPI evolves. Verify@ngrx/signalsversion compatibility with your Angular major version before upgrading.
Getting Started – Practical Checklist
- Ensure Angular 16 or later and install the signals package:
npm i @ngrx/signals # or ng add @ngrx/signals - Create a feature‑level SignalStore as shown in the example, or use
ng generate storewith the--signalflag if available. - Replace any local component state with a source signal and add computed signals for derived data.
- Update templates to call signals directly (e.g.,
todos()) and removeasyncpipes. - Run
ng testorng serveand verify that UI updates only when the underlying source signals change. Useconsole.loginside computed signals to confirm memoization. - When you need to perform async work, keep using a service or effect that calls
store.updater()once data arrives.
Conclusion
NgRx SignalStore gives Angular developers a clean, declarative way to manage localized, reactive state without the verbosity of actions, reducers, and selectors. By exposing source signals and memoized computed signals, it reduces boilerplate and keeps templates simple. However, keep its scope in mind: it’s best for component‑level state, not for cross‑feature shared data, and it does not eliminate the need for effects when dealing with asynchronous side effects.
Try adding a SignalStore to one of your feature modules today, and see how much less code you write while keeping the same reactive guarantees that NgRx is known for.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.