State Management
Signal Store
Master NgRx Signal Store, covering functional state slices, custom methods, computed selectors, and signals-first state management.
1. Learning Objectives
In this lesson, you will master the NgRx Signal Store, a lightweight, functional, signals-first state management library. By the end of this topic, you will be able to:
- Explain the architecture of NgRx Signal Store and how it differs from traditional Redux Stores.
- Initialize state properties dynamically using the
withStatebuilder plugin. - Define computed derived state selectors utilizing
withSelectors. - Write state mutation methods and side-effects inside
withMethods. - Coordinate lifecycle events in stores using
withHooks.
2. Overview
The NgRx Signal Store is a functional state management library for Angular built natively on top of the Signals API. It allows developers to define state, selectors, and methods in a modular, tree-shakeable structure. By combining state properties as signals, it eliminates the actions-reducers-effects boilerplate of traditional Redux, offering a clean API surface that can be injected as a local component provider or a global singleton.
3. Why This Topic Matters
Signal Store solves the scalability and boilerplate issues of traditional store frameworks:
- Boilerplate Reduction: Traditional Redux requires writing separate actions, reducers, selectors, and effects files for simple state slices. Signal Store consolidates these definitions into a single, functional builder pattern.
- Signals Native: As Angular transitions to signals-based fine-grained change detection (Zoneless), relying on RxJS observables in templates introduces mapping overhead. Signal Store exposes state slices directly as read-only signals.
4. Real-World Analogy
Think of NgRx Signal Store like **a Smart Vending Machine**:
- The Inventory State (withState): The items inside the machine (e.g. sodas, chips, chocolate) and their quantities are stored in database arrays.
- The Display Screen (withSelectors): Displays filtered calculations, like showing which products are "Sold Out" or showing the current balance you have inserted. The screen updates instantly as state changes.
- The Control Buttons (withMethods): You press buttons (e.g. "Select Soda 12", "Insert Coin") to purchase an item. The machine processes your input, updates the inventory list, and returns your item without needing separate bank vaults or ledgers.
- Machine Diagnostics (withHooks): Self-checks its sensors and connections on startup (OnInit) and safely shuts down its mechanical arms when turned off (OnDestroy).
5. Core Concepts
Below is a comparison of traditional NgRx Global Store versus the NgRx Signal Store:
| Property | NgRx Global Store (Classic Redux) | NgRx Signal Store (Modern Signals) |
|---|---|---|
| Reactivity Base | RxJS Observables (Requires async pipes / subscribe). | Angular Signals (Synchronous, getter-based values). |
| Boilerplate Level | High (Requires Actions, Reducers, Effects files). | Low (Consolidated functional builder pattern). |
| Granularity Scope | Global singleton. | Highly flexible (Global singleton or local component state). |
| Tree-shaking Support | Difficult due to centralized static registration. | Excellent (Functional plugins compile on demand). |
6. Syntax & API Reference
Below is the basic syntax for constructing a state store using the signalStore builder:
7. Visual Diagram
This diagram displays how state, selectors, and methods coordinate inside a Signal Store:
8. Live Example — Full Working Code
Here is a complete standalone task manager dashboard using a local injected TodoStore that filters entries reactively:
What just happened? The TodoDashboardComponent registers the TodoStore under its providers array, ensuring it gets a dedicated local state scope. Inside the template, we query the store properties as read-only signals (e.g. store.filteredTodos()). Clicking a list item calls store.toggleTodo(), which calls patchState() to update the model. Angular detects the signal change and re-renders the item.
9. Interactive Playground
Try It Yourself Challenges:
- Add a
withHooksblock that logs "TodoStore Initialized" inside theonInithook and "TodoStore Cleared" insideonDestroy. - Register the
TodoStoreusing@Injectable({ providedIn: 'root' })instead of component providers, and verify that mounting multiple dashboard widgets causes them to share the exact same list state.
10. Common Mistakes
| Mistake | Why it happens | Wrong | Correct |
|---|---|---|---|
| Mutating State Properties directly | Attempting to assign values directly to store properties outside patchState. Store signals are read-only. | store.filter = 'all' |
patchState(store, { filter }) |
| Omitting Spread Operators in patchState | Mutating child properties of a state object reference directly. patchState does a shallow merge only. |
patchState(store, (state) => { |
patchState(store, (state) => ({ |
11. Best Practices
- Restrict Store writes: Always perform state updates exclusively inside methods exposed by
withMethodsusingpatchState(). - Keep selectors optimized: Ensure computed selectors inside
withSelectorsare pure functions that avoid complex side effects. - Use local providers for transient state: Register stores in component
providersarrays for widgets that should reset their state when unmounted. - Utilize RxJS when necessary: Use the
rxMethodhelper from `@ngrx/signals/rxjs-interop` to handle asynchronous events inside methods.
12. Browser Compatibility/Requirements
NgRx Signal Store requires Angular 16+ or newer and performs consistently across all modern web browsers.
13. Interview Questions
Q1: How does NgRx Signal Store manage state updates, and what role does patchState play?
Answer: Signal Store state is immutable. State updates must go through the patchState() function. It takes the store instance and either an object representing the updated state slice or a updater function returning the next state reference. patchState() performs a shallow merge and notifies all dependent signal consumers synchronously.
Q2: Can NgRx Signal Store be used as both a global singleton and a local component provider?
Answer: Yes. Unlike traditional global stores, Signal Store is highly flexible. To make it a global singleton, decorate it with @Injectable({ providedIn: 'root' }). To isolate state to a specific component lifecycle (so it destroys when the component unmounts), register it in the component's providers array instead.
14. Debugging Exercise
Identify why the compiler throws an error stating that property "count" is read-only when executing the increment method:
Diagnosis: Properties exposed by signalStore are read-only signals. They do not expose writable methods like .set() or .update() directly. To update state inside a method, you must call patchState().
Fix: Wrap updates in patchState().
15. Practice Exercises
Exercise 1: Create a theme configuration store
Build a global ThemeStore using Signal Store. Include state parameters for theme mode (light/dark) and layout sidebar collapse toggles. Expose methods to update states, and verify all routing views read state values reactively.
16. Scenario-Based Challenge
The Dynamic Stock Trades Watcher:
You are designing a high-frequency trading desk view. Stocks arrive via custom sockets and are appended to state list arrays. State calculations must compile average pricing dynamically across differing stock categories. Design a local component-level Signal Store with custom selectors computing average prices lazily, caching calculations to keep view rendering performant under rapid updates.
17. Quick Quiz
Q1: Which helper function from @ngrx/signals must be called to modify store states?
A) updateState()
B) patchState()
C) storeState()
Answer: B — patchState() is the only API utility that performs safe state slice updates.
18. Summary & Key Takeaways
- Signal Store is a functional, signals-first state management library for Angular.
- It exposes state slices as read-only signals, eliminating classic Redux action/reducer boilerplates.
- Updates are executed inside custom methods using the
patchState()helper. - Stores are highly flexible and can be injected as global singletons or local component providers.
19. Cheat Sheet
| Builder Plugin | Purpose |
|---|---|
withState(initialState) |
Defines the baseline state model properties. |
withSelectors(store => computed) |
Defines computed derived projections. |
withMethods(store => methods) |
Defines state mutation operations. |
withHooks({ onInit, onDestroy }) |
Hooks into store initialization and destruction lifecycles. |