Choosing NgRx SignalStore for Local State in Angular Applications
Learn how NgRx SignalStore reduces boilerplate for local Angular state while keeping updates explicit, with a practical shopping‑cart example and guidance on when to use it.
09 May 2026, 23:42 UTC

The problem: boilerplate for simple component state
When a component needs only a handful of values — like a shopping‑cart list and a total price — the classic NgRx Store still forces you to write actions, reducers, selectors, and possibly effects. That overhead can feel disproportionate for UI‑scoped state that never leaves the component tree.
Thesis: SignalStore cuts the boilerplate while keeping updates explicit
NgRx SignalStore, part of the @ngrx/signals package, lets you define a signal‑based store with withState, withComputed, and withMethods. The resulting API is consumed directly in templates as ordinary Angular signals, so change detection works without extra selectors.
What SignalStore provides
- withState – declares the mutable state as a signal.
- withComputed – derives read‑only signals from state.
- withMethods – groups state‑transition functions that use
patchStateto keep updates explicit and testable. - withHooks (optional) – lifecycle hooks like
onInitfor side effects.
All pieces are plain functions; there are no action types or reducer switches to maintain.
Worked example: a shopping‑cart SignalStore
The following snippet shows a minimal cart store defined in an injectable service. It holds an array of items, computes the total price, and provides add/remove methods.
import { inject } from '@angular/core';
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
interface CartState {
items: { id: number; name: string; price: number }[];
}
const initialState: CartState = { items: [] };
export const CartStore = signalStore(
{ providedIn: 'root' },
withState<CartState>(initialState),
withComputed(({ items }) => ({
total: items.reduce((sum, i) => sum + i.price, 0)
})),
withMethods(({ items, total }) => ({
addItem(item: { id: number; name: string; price: number }) {
patchState(this, { items: [...items(), item] });
},
removeItem(id: number) {
patchState(this, { items: items().filter(i => i.id !== id) });
},
clear() {
patchState(this, { items: [] });
}
}))
);
In a component you inject the store and use its signals directly:
import { Component } from '@angular/core';
import { CartStore } from './cart.store';
@Component({
selector: 'app-cart',
template: `
- {{ i.name }} – ${{ i.price }}
Total: ${{ cartStore.total() }}
Add demo item
`
})
export class CartComponent {
readonly cartStore = inject(CartStore);
}
The template reads cartStore.items() and cartStore.total() as signals; any change triggered by the methods updates the view automatically.
Trade‑off and limitation
SignalStore removes the indirection that gives the global NgRx Store its audit‑friendly features: time‑travel debugging, action logging, and centralized reducer logic. If you replace every piece of state with a SignalStore, you may end up with scattered state that is harder to reason about across features. Teams should adopt a convention — e.g., use SignalStore only for component‑scoped or feature‑scoped state that does not need cross‑slice effects — and keep the classic Store for truly global concerns.
Actionable closing: decide and verify
- Check your project’s
package.jsonfor@ngrx/signalsand ensure it matches your Angular major version (SignalStore requires Angular 16+). - Create a minimal store in a sandbox component (as shown above) and run
ng serve. Verify that updating the state via a method instantly reflects in the template. - Compare the line count of this SignalStore version with an equivalent actions/reducer/selector/effects implementation for the same feature. If the reduction is meaningful and the state stays local, SignalStore is a good fit.
- Document the chosen scope (component vs. feature) in your team’s architecture guide to avoid fragmentation.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.