Manage Component-Local UI State with NgRx Signal Stores
NgRx signal stores let you manage component-local UI state without reducer boilerplate. Learn when to use them and when classic NgRx is still better.
01 Sept 2026, 15:45 UTC

One filter panel, three state options
You are building a task list with a filter bar: All, Active, Completed, plus a search box. The state is small—two strings—but it needs to react instantly. You could store it in plain component properties, but then every filter change requires manual change detection tricks if anything outside the component reads it. You could also reach for a classic NgRx store with actions, reducers, and selectors, but that is heavy machinery for UI state that dies when the component is destroyed.
NgRx signal stores offer a middle ground. They give you typed, reactive local state without actions or reducers, scoped to a component or service.
What a signal store actually is
A signal is a wrapper around a value that notifies Angular when the value changes. patchState is the controlled way to update a signal store so Angular can track the change. A computed value derives new state from existing signals automatically.
The @ngrx/signals package exposes signalStore, withState, withMethods, and withComputed. You define the store once, inject it into a component, and call methods from the template.
Worked example: a task filter store
Version assumption: Angular v17 or later and an @ngrx/signals release compatible with your installed NgRx version. Verify peer dependencies before copying this into production.
import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
import { inject, computed } from '@angular/core';
export type TaskFilter = 'all' | 'active' | 'completed';
export interface TaskFilterState {
filter: TaskFilter;
searchTerm: string;
}
export const TaskFilterStore = signalStore(
withState({ filter: 'all', searchTerm: '' }),
withMethods((store) => ({
setFilter(filter: TaskFilter) {
patchState(store, { filter });
},
setSearch(searchTerm: string) {
patchState(store, { searchTerm });
}
})),
withComputed((store) => ({
hasActiveSearch: computed(() => store.searchTerm().length > 0)
}))
);
withState declares the initial shape and types it. withMethods receives the store and returns functions that call patchState. Never assign directly to store.filter or mutate state outside patchState; doing so bypasses update tracking and can leave the UI stale.
Bind the store in a component template
import { Component, inject } from '@angular/core';
import { TaskFilterStore } from './task-filter.store';
@Component({
selector: 'app-task-filter',
standalone: true,
template: `
All
Active
Completed
@if (hasActiveSearch()) {
Filtering by search term
}
`
})
export class TaskFilterComponent {
protected readonly store = inject(TaskFilterStore);
protected readonly searchTerm = store.searchTerm;
protected readonly hasActiveSearch = store.hasActiveSearch;
}
Signals are read by calling them as functions in the template: searchTerm(). If you prefer observable-based tooling, convert a signal with toObservable and use the async pipe. Direct signal reads avoid extra subscriptions in simple UI components.
Add the package and verify the install
Run this at the Angular workspace root, not inside a feature folder:
ng add @ngrx/signals
- Where: terminal at the workspace root.
- Permissions: write access to package.json and node_modules.
- Placeholder: none; the command installs the latest compatible release.
- Expected check: package.json lists @ngrx/signals under dependencies or devDependencies.
- Risk: if your Angular or NgRx version is older than the package's peer requirements, the install may fail or pull incompatible peer dependencies.
To revert the package change, run npm uninstall @ngrx/signals and remove any imports you added.
Trade-off: local state is not shared state
Signal stores work well for modals, form wizards, list filters, and other UI state owned by one feature. They are a poor fit for data that multiple features must read and write, such as the authenticated user or a global shopping cart. For that, classic NgRx selectors, effects, and Redux DevTools remain the better choice.
| Approach | Best for | Boilerplate | Scope |
|---|---|---|---|
| Component property | Simple toggles | None | One component |
| Signal store | Feature-scoped UI state | Low | Component or service |
| Classic NgRx store | Shared cross-feature state | High | Application |
Check the result before you ship
- Run ng add @ngrx/signals and confirm the package appears in package.json.
- Create a minimal component using the store example above.
- Click a filter button and type in the search box. The template should update without calling ChangeDetectorRef.markForCheck.
- Open your browser's developer tools and confirm no change-detection errors appear after rapid input.
If the template does not reflect updates, check that you are calling patchState and not mutating state directly. Also confirm your Angular version supports reading signals directly in templates; older versions may require toObservable and the async pipe.
Use signal stores for UI state, not domain state
Start with component properties when state is truly trivial. Move to a signal store when the local state needs methods, computed values, or reactive template binding. Keep classic NgRx for state that crosses feature boundaries. That division keeps your codebase easier to read without giving up reactivity where it matters.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.