skip to content

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%

answer

  1. applied left to right
  2. each sees only earlier members
  3. factories run in injection context
  4. withComputed cannot patchState
  5. partials, updaters, or a sequence

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.

solid answer

~40 s

`signalStore()` runs its features in order inside the store's constructor, and each feature passes an enriched store to the next. `withState` adds the state slices, `withComputed` receives state signals, earlier props and methods and returns derived signals (a plain function is wrapped in `computed()`), and `withMethods` receives the same members plus the writable state source, so its methods can call `patchState`. `withComputed` gets no writable source, so it cannot write state. The factories run in the **injection context**, so `inject()` works as a default parameter. `patchState(store, ...)` takes partial objects, updater functions or a sequence of both, applied in order, and sets only the slices whose reference changed. Put a feature before the one that uses it; reusing a member name triggers a dev-mode warning.

code

ts · 24 lines
ts
import { computed, inject } from '@angular/core';
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import { Session, SessionsApi } from './sessions-api';

export const ScheduleStore = signalStore(
  { providedIn: 'root' },
  withState({ sessions: [] as Session[], favouriteIds: [] as string[], trackFilter: 'all' }),
  withComputed(({ sessions, favouriteIds }) => ({
    favouriteSessions: computed(() => sessions().filter((s) => favouriteIds().includes(s.id))),
    favouriteCount: () => favouriteIds().length, // wrapped in computed()
  })),
  withMethods((store, api = inject(SessionsApi)) => ({
    async load(): Promise<void> {
      patchState(store, { sessions: await api.getAll() });
    },
    resetView(): void {
      patchState(
        store,
        { trackFilter: 'all' },
        (state) => ({ favouriteIds: state.favouriteIds.filter((id) => store.sessions().some((s) => s.id === id)) })
      );
    },
  }))
);

go deeper

for a junior

Recall the usual order: withState, then withComputed, then withMethods, and that methods change state with patchState.

for a middle

Explain that features apply left to right, what each factory receives, why withComputed cannot write, and how patchState merges partials and updaters.

for a senior

Show you have hit the traps: same-reference updates that never render, duplicate member warnings, and methods that need each other inside one factory.

for a principal

Treat feature order as the store's dependency graph; a store whose features reference each other in knots is a sign it should be split.

## Features are functions applied in order In `@ngrx/signals`, a **feature** such as `withState(...)` is a function that takes the store being built and returns an enriched copy. `signalStore(f1, f2, f3)` starts from an empty inner store and applies `f1`, then `f2`, then `f3` inside the generated class's constructor. The result is copied onto the instance: state signals, props (computed signals are props) and methods. Two consequences follow: - A feature's factory sees **only what earlier features added**. The TypeScript types enforce this: `withComputed(({ favouriteIds }) => ...)` placed before `withState({ favouriteIds: [] })` does not compile. - Every factory runs **during construction, in the injection context**, so a factory can call `inject()` (commonly as a default parameter: `withMethods((store, api = inject(SessionsApi)) => ...)`). ## What each core feature receives and adds | Feature | Factory receives | Adds | Can call `patchState`? | |---|---|---|---| | `withState` | nothing (an object, or a factory run in the injection context) | one signal per root slice | not applicable | | `withComputed` | state signals, earlier props and methods | derived signals, as props | **no**: no writable state source | | `withMethods` | state signals, props, methods **and** the writable state source | methods | **yes** | `withComputed` accepts either ready-made `computed()` signals or plain functions, which it wraps in `computed()` for you. It is in fact built on top of `withProps`: its results are props that happen to be signals. ## `patchState` in detail `patchState(store, ...updaters)` is the standard write API for a SignalStore (and for `signalState`). It works like this: 1. It takes the current state as a snapshot, outside any reactive tracking. 2. It applies each argument in order. A **partial object** is merged in; an **updater function** receives the state as it stands after the earlier arguments and returns a partial object. 3. For each resulting slice, it calls `set()` on that slice's signal **only if the reference changed**. A slice you did not mention is untouched. 4. In dev mode, a key that is not in the initial state produces a console warning and is **ignored**; root slices cannot be added after creation. So `patchState(store, { trackFilter: 'web' }, (state) => ({ favouriteIds: [] }))` applies two changes in one call, and the updater already sees `trackFilter` as `'web'`. Named updater functions that return a partial state are a common way to reuse such changes across methods. Because the comparison is by reference, updaters must be **immutable**. Pushing into `favouriteIds` and returning the same array changes nothing observable. A dev-mode freeze that would have thrown on such mutations shipped in the 19.0.0 beta and was reverted in 19.0.1, so nothing warns you. ## Why order matters in practice - **Dependencies point backwards.** Put `withState` first, then `withComputed` that reads it, then `withMethods` that uses both. A method can call a computed signal only if `withComputed` came earlier. - **Hooks see what precedes them.** A `withHooks` placed before `withMethods` cannot call those methods. - **Names are unique.** Adding a member whose name already exists logs `SignalStore members cannot be overridden` in dev mode. Rename rather than shadow. - **Same-feature references.** Inside one `withMethods` factory, `store` does not yet contain the methods you are defining. Either define a local helper in the factory and call it from both methods, or split into two `withMethods` features. The NgRx docs prefer the local helper, which keeps related code in one place. ## Reading state inside a method Inside a method, read the current value of a slice by calling its signal, `store.trackFilter()`, or take a plain snapshot of all slices with `getState(store)`. Both are fine in ordinary methods. Prefer the updater form of `patchState` when the new value depends on the old one: the updater receives the state as it is at the moment of the update, including changes made by earlier arguments in the same call, so the method never works from a value it read too early. ## Pitfalls - Trying to write state from `withComputed`: the factory has no writable source, which is deliberate. Derivations stay pure. - Passing a whole new state to `patchState` out of habit: it is a partial update, so pass only what changes. - Expecting a mutated array to re-render: return a new array. - Injecting a service in the component and passing it into methods: inject it inside the store's factory instead, where the injection context is available.

  • A SignalStore updater pushes an id into favouriteIds and returns the same array; why does nothing update?
    `patchState` compares each resulting slice with the current one by reference and calls `set()` only when they differ. The pushed array is the same reference, so no signal changes and nothing re-renders. Return a new array, for example `[...favouriteIds, id]`. The dev-mode freeze that would have caught this was reverted in 19.0.1, so the docs simply require immutable updaters.
  • How can one method in a SignalStore's withMethods call another method defined in the same factory?
    The `store` argument only holds members from earlier features, so a sibling method is not on it yet. Define a local function inside the factory and call it from both methods, or move one method into an earlier `withMethods`. The NgRx docs recommend the local helper, keeping related logic in one feature.

Building a SignalStore is like building floors of a house from the ground up: each new floor can rest on and use the floors already built, but it cannot hang from a floor that does not exist yet.

saying these in an interview costs you the question

  • Feature order in signalStore() is cosmetic; every feature sees every member.
  • withComputed can call patchState, because it receives the whole store.
  • patchState replaces the whole state, so you must pass every slice.
  • patchState takes one partial object, so two changes need two calls.
  • Store factories run outside DI, so services must be passed in from components.