In an NgRx SignalStore, what do withProps and withLinkedState add that withComputed and withState cannot, and when would you use each?
answer
- not everything is a signal
- derived, yet still writable
- linkedSignal under the hood
- part of getState, or not
- withComputed sits on withProps
basics
~20 swithProps 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.
solid answer
~40 s`withProps` (NgRx 19) takes a factory, run in the injection context, and adds whatever it returns to the store: an observable made with `toObservable`, a group of injected services later features share, or `_`-prefixed private helpers. `withComputed` is itself built on `withProps`; it just wraps functions in `computed()`. `withLinkedState` (NgRx 20) adds **state** whose value derives from other signals: a computation function is wrapped in Angular's `linkedSignal()`, or you pass a `WritableSignal` such as a `linkedSignal({ source, computation })`. Linked slices behave like `withState` slices: they are `DeepSignal`s, appear in `getState()`, stay protected, and `patchState` can overwrite them until a source changes. In the planner, `selectedSessionId` falls back to the first visible session when the track filter changes, yet the user can still pick another.
code
ts · 31 linesimport { computed, inject } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import {
patchState, signalStore, withComputed, withLinkedState, withMethods, withProps, withState,
} from '@ngrx/signals';
import { Session, SessionsApi } from './sessions-api';
export const ScheduleStore = signalStore(
{ providedIn: 'root' },
withState({ sessions: [] as Session[], trackFilter: 'all' }),
withComputed(({ sessions, trackFilter }) => ({
visibleSessions: computed(() =>
trackFilter() === 'all' ? sessions() : sessions().filter((s) => s.track === trackFilter())
),
})),
withLinkedState(({ visibleSessions }) => ({
selectedSessionId: (): string | null => visibleSessions()[0]?.id ?? null,
})),
withProps(({ trackFilter }) => ({
_api: inject(SessionsApi),
trackFilter$: toObservable(trackFilter),
})),
withMethods((store) => ({
select(id: string): void {
patchState(store, { selectedSessionId: id });
},
async load(): Promise<void> {
patchState(store, { sessions: await store._api.getAll() });
},
}))
);go deeper
Recall that withProps adds non-state members and withLinkedState adds state that follows other signals but can still be patched.
Explain that linked slices wrap a computation in linkedSignal, appear in getState, and are overwritten when a source changes.
Replace effect-driven resets with linked state, group dependencies with withProps, and know which members a persistence feature built on getState will miss.
Weigh how much derivation belongs in the store versus components; linked state is powerful but makes a store's rules less obvious to readers.
## Two gaps in the core features `withState` holds values you set, and `withComputed` holds values you derive. Two common needs fit neither: - A store member that is **not state and not a signal**: an observable for an RxJS-based consumer, an injected API client several features share, a static configuration object. - A value that is **derived by default but may be overridden**: the selected session should follow the list of visible sessions, but the user may also select a different one. `withProps` covers the first; `withLinkedState` covers the second. ## `withProps`: arbitrary store members `withProps(factory)` runs its factory in the injection context. The factory receives state signals, earlier props and methods, and the writable state source, and returns an object whose members are added to the store as **props**. Typical uses: - **Exposing observables:** `trackFilter$: toObservable(trackFilter)` for a consumer built on RxJS. - **Grouping dependencies:** `_api: inject(SessionsApi)` once, then every later `withMethods` and `withHooks` destructures `_api` instead of injecting again. - **Private helpers:** any member whose name starts with `_` is hidden from the store's public type. Props are **not state**. They do not appear in `getState(store)`, and `patchState` cannot touch them. `withComputed` is implemented by calling `withProps`, which is why computed signals are also props. ## `withLinkedState`: state that follows its sources `withLinkedState(factory)` also runs in the injection context. Its factory receives **state signals and props** (not methods) and returns a dictionary whose values are either: 1. a **computation function**, which the store wraps in Angular's `linkedSignal()`, so it recomputes whenever a signal it reads changes; or 2. a **`WritableSignal`**, such as `linkedSignal({ source, computation })`, when the new value must depend on the previous one. The store and that signal stay synchronised. The result is a normal state slice: a `DeepSignal`, included in `getState()`, protected like other state, and writable from the store's methods with `patchState`. | Feature | Is it state? | Writable with `patchState`? | Recomputes from sources? | In `getState()`? | |---|---|---|---|---| | `withState` | yes | yes | no | yes | | `withComputed` | no (a prop) | no | yes | no | | `withLinkedState` | yes | yes | yes | yes | | `withProps` | no | no | only if you return signals | no | ## The planner, concretely - `visibleSessions` is a `withComputed` signal filtered by `trackFilter`. - `selectedSessionId` is linked state: `() => visibleSessions()[0]?.id ?? null`. When the user picks the "web" track, the selection jumps to the first web session. - A `select(id)` method calls `patchState(store, { selectedSessionId: id })`. The user's choice holds until `visibleSessions` changes again, at which point the computation runs and overwrites it. - If the user's choice should survive a filter change when it is still visible, pass a `linkedSignal` with `source` and `computation` that looks up the previous id in the new list. How `linkedSignal` itself works is Angular's topic; the SignalStore part is only that it can be handed to `withLinkedState`. Before `withLinkedState` existed, the usual workaround was an Angular `effect()` in `withHooks` that patched the selection whenever the filter changed. That runs asynchronously and leaves a moment where the selection points at a hidden session; linked state recomputes on read. ## Choosing between them - Need a derived, read-only value: **`withComputed`**. - Need a value you set: **`withState`**. - Need a derived value the user or a method can override: **`withLinkedState`**. - Need a non-state member (stream, service, config, private helper): **`withProps`**. ## Ordering and naming A common order in a store that uses both is `withState`, `withComputed`, `withLinkedState`, `withProps`, `withMethods`, `withHooks`, but the only real rule is that each factory sees what came before. A linked slice that reads a computed signal must come after `withComputed`; a prop that other features destructure must come before them. Names share one space across state, props and methods, so a linked slice cannot reuse the name of a computed signal, and dev mode warns if you try. ## Pitfalls - Expecting a `withProps` member to be saved by a feature that persists `getState()`: props are not in the snapshot. - Calling a store method from the `withLinkedState` factory: methods are not passed to it. - Forgetting that a linked slice is overwritten when its source changes; if the override must persist, model it with a `linkedSignal` that uses the previous value, or as plain state.
- After select('s42') sets a SignalStore's linked selectedSessionId, what happens when trackFilter changes?The linked slice keeps `'s42'` until a signal its computation reads changes. Changing `trackFilter` changes `visibleSessions`, so the computation runs again and the slice becomes the first visible session's id, discarding the user's choice. To keep a still-visible choice, pass a `linkedSignal({ source, computation })` that checks the previous value.
- Why can't a SignalStore's withLinkedState factory call one of the store's methods?Its factory receives only state signals and props, not methods, so a linked slice is derived from readable values rather than from operations. Put the derivation logic in a computation function or a prop, and keep methods for writes.
saying these in an interview costs you the question
- withLinkedState slices are read-only derived values, like withComputed results.
- withProps may only hold signals; observables must live outside the store.
- Linked state is derived, so getState() leaves it out of the snapshot.
- A withProps member can be updated with patchState like any state slice.
- An effect() in withHooks that resets the selection is equivalent to linked state.