skip to content

Signal Store Anatomy

A SignalStore is built from features: state, computed signals, methods and hooks, provided at root or per component. Interviewers ask how it differs from the global Store and when each fits.

on this pageshow

explore

questions

6

What is an NgRx SignalStore, and how does it differ from an Angular service that keeps its own writable signals?

level: juniorimportance: must knowfreq 62%

answer

  1. a function that returns a class
  2. features composed in order
  3. state slices become signals
  4. patchState, not set()
  5. state protected by default

basics

~20 s

An NgRx SignalStore is an injectable class built by signalStore() from features: withState turns state slices into signals, withComputed and withMethods add derived values and operations, and by default only the store's own methods can change state.

solid answer

~40 s

`signalStore()` from `@ngrx/signals` takes a sequence of features and returns an injectable class. `withState` creates one signal per root state slice (a `DeepSignal` when the slice is an object literal), `withComputed` adds derived signals, `withMethods` adds the operations, and writes go through `patchState(store, ...)`. Compared with a hand-written service holding `signal()` fields, you get one fixed shape instead of a per-team convention, nested signals for free, store-level lifecycle hooks with `withHooks`, and **protected state**: the store's public type exposes state as read-only, so only its own methods can update it. It is still an ordinary Angular provider, registered nowhere until you pass `{ providedIn: 'root' }` or list it in a `providers` array.

code

ts · 30 lines
ts
import { computed } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';

type Session = { id: string; title: string; track: string };

export const ScheduleStore = signalStore(
  { providedIn: 'root' },
  withState({
    sessions: [] as Session[],
    favouriteIds: [] as string[],
    trackFilter: 'all',
  }),
  withComputed(({ sessions, trackFilter }) => ({
    visibleSessions: computed(() =>
      trackFilter() === 'all' ? sessions() : sessions().filter((s) => s.track === trackFilter())
    ),
  })),
  withMethods((store) => ({
    setTrack(trackFilter: string): void {
      patchState(store, { trackFilter });
    },
    toggleFavourite(id: string): void {
      patchState(store, ({ favouriteIds }) => ({
        favouriteIds: favouriteIds.includes(id)
          ? favouriteIds.filter((f) => f !== id)
          : [...favouriteIds, id],
      }));
    },
  }))
);

go deeper

for a junior

Recall that signalStore() returns a class built from features, that state slices are signals you read with (), and that patchState is how state changes.

for a middle

Explain what each core feature adds, why exposed state is typed without set(), and how providedIn: 'root' differs from listing the store in providers.

for a senior

Argue when a SignalStore beats a hand-written signal service: enforced read-only state, DeepSignals, hooks, and features reused across many stores.

for a principal

Frame the choice as team consistency: one store shape across features lowers review cost, at the price of a dependency and its release cadence.

## What `signalStore()` returns A **SignalStore** comes from the `@ngrx/signals` package. You do not write a class for it; you call `signalStore()` with a list of **features**, and it returns a class for you. Angular can inject that class like any service. Each time Angular creates an instance, the constructor runs the features in order, and each feature adds members to the instance. For a conference schedule planner, the store might hold the list of `sessions`, the attendee's `favouriteIds` and a `trackFilter`. Components inject the store, read its signals in templates and call its methods from event handlers. ## The building blocks | Feature | What it adds | Schedule-planner example | |---|---|---| | `withState` | one signal per root state slice | `sessions`, `favouriteIds`, `trackFilter` | | `withComputed` | derived, read-only signals | `visibleSessions` filtered by track | | `withMethods` | functions that read state and call `patchState` | `setTrack()`, `toggleFavourite()` | | `withHooks` | `onInit` / `onDestroy` for the store instance | load the schedule when the store is created | Other features (`withProps`, `withLinkedState` and reusable custom features) extend the same pattern, but these four are what most stores are made of. ## Reading and writing state - **Reading.** Every root slice is a signal: `store.trackFilter()` in a template or a `computed`. When a slice holds an **object literal**, the store wraps it in a `DeepSignal`, so `store.filter()` returns the whole object and `store.filter.day()` is its own signal, created lazily on first access. Arrays and primitives stay plain signals. - **Writing.** The exposed signals are typed as read-only `Signal`s, with no `set()` or `update()`. State changes go through `patchState(store, partialState)` or `patchState(store, (state) => partialState)`, which updates only the slices you name. - **Snapshot.** `getState(store)` returns a plain object of the current state; inside a reactive context such as `computed` or Angular's `effect()` it also tracks every slice. - **Protection.** By default the store is created with **protected state**. The type a component sees exposes state read-only, so `patchState(this.store, ...)` in a component does not compile. Writes belong in methods defined by `withMethods`, which names every change the store allows. ## How it compares with a service that holds signals A plain Angular service with `private readonly _favourites = signal<string[]>([])` and a public `favourites = this._favourites.asReadonly()` can do the same job. The difference is who enforces the shape. | Concern | Hand-written signal service | NgRx SignalStore | |---|---|---| | Read-only public state | you remember `asReadonly()` per field | protected state by default | | Nested object fields | one signal per object, or manual splitting | `DeepSignal` per object literal slice | | Partial updates | a `set`/`update` call per signal | one `patchState` call with several slices | | Lifecycle logic | constructor plus `DestroyRef` by hand | `withHooks` with `onInit` / `onDestroy` | | Reuse across stores | inheritance or copy-paste | features composed into several stores | For a service with two fields the difference is small. For a team with many feature stores, the fixed structure means every store reads the same way. ## Where the store is provided `signalStore()` registers the class with **no** injector by default. You either: 1. pass a config object first, `signalStore({ providedIn: 'root' }, withState(...))`, for one instance shared by the whole app; or 2. list the store in a component's or route's `providers` array, which gives that component subtree its own instance. In the planner, the schedule store sits at root, while each session-detail component provides a small store of its own for the open session. ## Common misunderstandings - A SignalStore is **not** a module-level object you import and mutate; it is a class that Angular instantiates. - A plain SignalStore has **no actions or reducers**; methods change state directly through `patchState`. The global NgRx Store is a separate model, and the optional Events plugin is what adds a Flux-style layer to SignalStore. - Its state signals are typed **read-only** from outside; a `set()` call on them does not compile. - To type a variable or constructor parameter as the store, use `InstanceType<typeof ScheduleStore>`, because `ScheduleStore` itself is a class value, not a type. ## Functional definition versus a class Because `signalStore()` returns a class, a team can also write `export class ScheduleStore extends signalStore(...) { ... }` and add members in the class body. The NgRx docs allow this but recommend the functional style shown above: every member then comes from a feature, the order of features documents the dependencies, and protected state keeps working without extra configuration. Mixing both styles in one codebase makes stores harder to compare, so pick one and keep it.

  • If a SignalStore slice holds an object literal such as filter: { track, day }, how do you read filter.track as a signal?
    `withState` wraps object-literal slices in a `DeepSignal`: `store.filter()` returns the whole object, and `store.filter.track()` is a signal of its own, created lazily the first time you access it. Arrays and primitives stay plain signals. In NgRx 22 a union slice that contains an object literal gets a `DeepSignal` for each object member instead of one signal for the whole union.
  • How do you use a SignalStore as a type, for a function parameter or constructor injection?
    `signalStore()` returns a class value, so its instance type is `InstanceType<typeof ScheduleStore>`. The NgRx docs suggest exporting a type alias with the same name as the store; then `constructor(readonly store: ScheduleStore)` works as well as `inject(ScheduleStore)`.

saying these in an interview costs you the question

  • A SignalStore is an object you import and mutate directly, like a module-level variable.
  • signalStore() registers the store in the root injector automatically.
  • You update SignalStore state by calling set() on the state signals it exposes.
  • A SignalStore needs actions, reducers and effects just like the global NgRx Store.
  • Object slices in withState are one signal only; their fields cannot be read as signals.
open as a page

In an NgRx SignalStore, how do withState, withComputed and withMethods build the store, and why does the order of features matter?

level: middleimportance: must knowfreq 55%

basics

~20 s

signalStore() applies its features left to right, and each feature's factory receives only the members added before it. withState adds state signals, withComputed derives read-only signals, and withMethods adds operations that call patchState, so a feature cannot use a member declared later.

open as a page

With NgRx SignalStore, when do withHooks' onInit and onDestroy run for a store provided at root versus one in a component's providers?

level: middleimportance: should knowfreq 42%

basics

~20 s

A SignalStore's onInit runs once per instance, in its constructor on first injection, inside the injection context. onDestroy runs when the creating injector is destroyed: with the component for a component-provided store, at application teardown for a root store.

open as a page

With an NgRx SignalStore, why does patchState(this.store, { trackFilter: 'web' }) in a component fail to compile, and how should you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

SignalStore state is protected by default: the store's public type exposes a read-only state source, and patchState needs a writable one. Add a method such as setTrack() in withMethods; protectedState: false also works but drops the guarantee.

open as a page

For a large Angular app on NgRx 22, how do you decide between the global Store and SignalStores, and where does signalState fit?

level: principalimportance: should knowfreq 48%

basics

~20 s

Neither replaces the other in NgRx 22. Use the global Store where many features react to shared events and you want an action log and Store DevTools; SignalStores for feature- or view-scoped state; signalState for small private state in one class.

open as a page

In an NgRx SignalStore, what do withProps and withLinkedState add that withComputed and withState cannot, and when would you use each?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

withProps adds arbitrary members, such as observables, injected services or private helpers, that are not state. withLinkedState adds state slices derived from other signals that stay writable with patchState and recompute when their sources change.

open as a page