In an NgRx SignalStore, how do withState, withComputed and withMethods build the store, and why does the order of features matter?
answer
- applied left to right
- each sees only earlier members
- factories run in injection context
- withComputed cannot patchState
- partials, updaters, or a sequence
basics
~20 ssignalStore() 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 linesimport { 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
Recall the usual order: withState, then withComputed, then withMethods, and that methods change state with patchState.
Explain that features apply left to right, what each factory receives, why withComputed cannot write, and how patchState merges partials and updaters.
Show you have hit the traps: same-reference updates that never render, duplicate member warnings, and methods that need each other inside one factory.
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.