web-state-ngrx-signalstore-534e52 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-state-ngrx-signalstore-534e52 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: A SignalStore is an Angular service assembled from features —withState,withComputed,withMethods,withHooks— applied in that order, since each one sees only what came before it. Every update goes throughpatchState; state is protected from outside writes from v18 and deep-frozen in development from v19, which is why aFormGroupor any other mutable object belongs inwithPropsrather than in state.
Detailed Resources:
signalStore, withState, withComputed, withMethods, withProps, DI and component-scoped storeswithEntities, the CRUD updaters, prependEntity / upsertEntity (v20+)rxMethod, signalMethod (v19+), side effectssignalStoreFeature, custom features, call state, DevToolsunprotected(), mocking strategiessignalStore({ providedIn: "root" }); follow examples/core.md.providers, so it is destroyed with the component and its rxMethod subscriptions go with it; follow examples/core.md.withEntities rather than an array in state; follow examples/entities.md.<critical_requirements>
Update state through `patchState()`. It produces a new state object and notifies the signals; assigning into the existing one changes a value nothing is watching — and from v18 the state is protected, so the assignment is rejected outright.
Order the features so each one has what it needs. A feature can only see members declared before it, so withState precedes withComputed, which precedes the withMethods that reads it.
Hold collections in `withEntities()`. It maintains an entityMap and an ids array, giving lookup by id in constant time and the CRUD updaters; an array in state gives a scan per lookup and update logic written by hand.
Put mutable objects in `withProps()`, not in state. From v19 development builds freeze state recursively, so a FormGroup or a class instance stored there throws on its first internal write.
Drive cancellable async through `rxMethod()`. It ties the subscription to the store's lifetime and gives switchMap somewhere to live, which is what makes a superseded request stop rather than land late.
</critical_requirements>
Auto-detection: signalStore, signalStoreFeature, withState, withComputed, withMethods, withHooks, withProps, withEntities, withFeature, withLinkedState, patchState, rxMethod, signalMethod, entityConfig, unprotected, @ngrx/signals
Applies to:
Handled elsewhere:
<philosophy>
A store is a list of features applied left to right, each one adding members to the store the next one receives. That is the whole model: no actions, no reducers, no selectors, and no registry — the store's type is the accumulated result of the features, so a member that is not there is a compile error rather than an undefined.
Two consequences follow. The composition is linear, so ordering is semantic rather than stylistic — a withComputed placed above the withState it reads cannot see it. And a store is an Angular service, so its lifetime is a DI question: providedIn: "root" for one instance, a component's providers for one per component, destroyed with it.
</philosophy>
<decision_framework>
Is it a collection of items with unique ids?
├─ NO → withState
└─ YES → Are items looked up or updated individually?
├─ YES → withEntities — entityMap for O(1) reads, updaters for writes
└─ NO → withState with an array, if it is only ever read in bulkDoes the value participate in reactivity?
├─ YES → withState — signals, patchState, and the dev-mode freeze
└─ NO → withProps — services, subjects, form groups, anything that mutates itselfrxMethod | signalMethod | |
|---|---|---|
| Needs RxJS | yes | no |
Operators — debounceTime, switchMap | yes | no |
| Superseded calls cancelled | built in | yours to handle |
| Accepts | T, Signal<T>, Observable<T> | T, Signal<T> |
| Injection context | required | not required |
Anything that can race — a search-as-you-type, a request keyed on a changing id — wants rxMethod with switchMap. A side effect that only reacts to a signal wants signalMethod and no RxJS dependency.
A plain async method in withMethods is the third option, and a legitimate one for a single fetch triggered by an explicit user action, where there is nothing to cancel and no stream to react to. It buys try/catch and costs the cancellation — so the question to settle first is whether two of these can ever be in flight at once.
</decision_framework>
<patterns>
Features apply in order: state, then values derived from it, then the methods that change it, then the hooks.
export const CounterStore = signalStore(
{ providedIn: "root" },
withState({ count: 0 }),
withComputed(({ count }) => ({ doubled: computed(() => count() * 2) })),
withMethods((store) => ({
increment: () => patchState(store, { count: store.count() + 1 }),
})),
);Full code: examples/core.md
Takes a partial object, an updater function, or several of either in one call — applied as a single notification.
patchState(store, { isLoading: true });
patchState(store, (state) => ({ count: state.count + 1 }));
patchState(store, setAllEntities(users), { isLoading: false });Full code: examples/core.md
Normalised storage plus updaters: setAllEntities, addEntity/addEntities, setEntity/setEntities, updateEntity/updateEntities, removeEntity/removeEntities.
export const UserStore = signalStore(
withEntities<User>(),
withMethods((store) => ({
add: (user: User) => patchState(store, addEntity(user)),
})),
);entityConfig() names the id field where it is not id, and named collections need that name passed to every updater.
Full code: examples/entities.md
Accepts a value, a signal or an observable; the factory pipes the stream and the subscription ends with the store.
const search = rxMethod<string>(
pipe(
debounceTime(SEARCH_DEBOUNCE_MS),
switchMap((term) => api.search(term)),
tap((results) => patchState(store, { results })),
),
);An error reaching the outer stream ends it for good, so the catchError goes inside the switchMap.
Full code: examples/effects.md
The same reactive trigger with no RxJS and no injection context, for effects that cannot race.
const logSelection = signalMethod<string>((id) => {
analytics.track("selected", id);
});Only the parameter signal is tracked; signals read inside the processor are not.
Full code: examples/effects.md
A named group of features, applied to any store that satisfies its declared shape.
export function withLoading() {
return signalStoreFeature(
withState({ isLoading: false }),
withMethods((store) => ({
setLoading: (isLoading: boolean) => patchState(store, { isLoading }),
})),
);
}The type<...>() helper declares what the host store must already provide, which is what makes the feature's own types resolve.
Full code: examples/features.md
onInit runs as the store is constructed and onDestroy when its injector is destroyed — so a component-scoped store cleans up on the component's own timeline.
Full code: examples/core.md
One feature carrying loading / loaded / error for a store or a named collection, so every async surface reports the same three states. A community feature library ships one; the shape is short enough to write in-house.
Full code: examples/features.md
Where signalStoreFeature composes blind, withFeature receives the store, so a generic feature can call a method that store already has.
// withEntityLoader here is a feature you wrote, not an NgRx export:
// it takes a fetch function and returns signalStoreFeature(...)
export const ProductStore = signalStore(
{ providedIn: "root" },
withMethods((store) => ({ load: rxMethod<string>(/* ... */) })),
withFeature((store) =>
withEntityLoader((id) => firstValueFrom(store.load(id))),
),
);withFeature is what makes that possible: the fetch function closes over store.load, which does not exist until the withMethods above it has been applied.
Full code: examples/features.md
State that derives from a source signal and is still writable — a selection that follows the data by default and holds when the user picks something.
export const FilterStore = signalStore(
withState({ items: [] as Item[] }),
withLinkedState(({ items }) => ({
selectedId: () => items()[0]?.id ?? null,
})),
);withComputed would give the same default and refuse the write.
Full code: examples/features.md
</patterns>
<red_flags>
Breaks at runtime:
withProps is where it belongs.Surprising behaviour:
rxMethod unsubscribes on store destroy by itself; an extra manual unsubscribe is at best redundant.withHooks.onInit runs during construction, so a store that fetches on init fetches in every test that injects it.signalMethod tracks only the signal passed as its parameter — signals read inside the processor are untracked, deliberately.signalMethod has no cancellation, so two async calls in flight resolve in whatever order they finish and the later write wins regardless of which was requested last.patchState rebuild the whole path; several flat slices update independently.updateEntity, so the entityMap reference never changes and nothing reading it re-renders.entityConfig() is what points withEntities at an id field that is not called id; without it the updaters key on undefined.</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.