Skip to content
Back to skills

Ngrx Signals

ASecurity

Build, refactor, and architect Angular state with the @ngrx/signals Signal Store. Use whenever the user mentions signalStore, withState, withMethods, withComputed, withHooks, withLinkedState, withFeature, signalStoreFeature, withEntities, rxMethod, signalMethod, patchState, the SignalStore events plugin (withReducer, withEventHandlers), or wants to refactor BehaviorSubject/service-based state to Signal Store. Also use for designing custom store features, request status, optimistic updates, en...

  • 39 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmenttypescriptgoreactangulartestingrefactoringapibackend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add danielsogl/copilot-workflow-demo --skill ngrx-signals --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ngrx Signals?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ngrx Signals
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/danielsogl-ngrx-signals/badge)](https://www.skillsdirectory.com/skills/danielsogl-ngrx-signals)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ngrx-signals
description: Build, refactor, and architect Angular state with the @ngrx/signals Signal Store. Use whenever the user mentions signalStore, withState, withMethods, withComputed, withHooks, withLinkedState, withFeature, signalStoreFeature, withEntities, rxMethod, signalMethod, patchState, the SignalStore events plugin (withReducer, withEventHandlers), or wants to refactor BehaviorSubject/service-based state to Signal Store. Also use for designing custom store features, request status, optimistic updates, entity collections, and the store/service split in Angular. Do NOT use for @ngrx/store (classic Redux), createReducer/createEffect, or generic RxJS questions unrelated to Signal Store.
---

# NgRx Signal Store

Modern, lightweight, signal-based state management for Angular. This skill produces idiomatic, type-safe, well-architected `@ngrx/signals` code.

## When to use this skill

Trigger on any of these signals:

- The user names the API: `signalStore`, `signalStoreFeature`, `withState`, `withMethods`, `withComputed`, `withLinkedState`, `withHooks`, `withProps`, `withFeature`, `withEntities`, `rxMethod`, `signalMethod`, `patchState`, `getState`, `watchState`, `signalState`, or `@ngrx/signals/events` / `@ngrx/signals/testing`.
- The user wants to **refactor** a service that uses `BehaviorSubject`/`Subject` or a class with mutable fields into a Signal Store.
- The user asks for **feature/page state** in Angular and either mentions Angular Signals or asks for a "modern" approach.
- The user wants a **custom store feature** (`withRequestStatus`, `withSelectedEntity`, `withLogger`, etc.) or asks how to compose features.
- The user asks about **optimistic updates**, **request status**, **entity collections**, or **CRUD against an HTTP API** in an Angular feature.

Do NOT use for classic `@ngrx/store` (actions, reducers, effects, separate-file selectors), Redux DevTools wiring without Signal Store, or pure RxJS pipeline questions.

## Mental model

A Signal Store is a tree of composed **features**. Each feature contributes some combination of state, computed signals, methods, props, and lifecycle hooks. Composition happens at definition time:

```typescript
export const FooStore = signalStore(
  { providedIn: 'root' },     // optional Angular provider config
  withState(initialState),    // declares state slices (signals)
  withComputed(...),          // derived signals
  withProps(...),             // non-reactive deps & values (often via inject())
  withMethods(...),           // actions / mutations
  withHooks({ onInit, onDestroy })
);
```

Three rules every Signal Store author needs to internalize:

1. **State updates go through `patchState(store, updater)`.** Direct mutation does not exist. Updaters can be a partial object, a function `(state) => Partial<State>`, or a custom updater that returns one of those.
2. **Stores are Angular providers.** Inject them with `inject(Store)` or `TestBed.inject(Store)`. Provide them at root, on a route, or on a component (`providers: [Store]`) for component-scoped state.
3. **Composition over inheritance.** Reuse comes from custom features built with `signalStoreFeature(...)`, not by extending classes.

## Canonical scaffold (use as a starting point)

For 80% of feature stores, this is the shape. Copy it, adapt it, then read the references for anything non-obvious.

```typescript
import { computed, inject } from '@angular/core';
import {
  patchState, signalStore, withComputed, withMethods, withState,
} from '@ngrx/signals';
import { Foo, FooApi } from './foo.api';
import {
  setError, setFulfilled, setPending, withRequestStatus,
} from './with-request-status';

type FooState = {
  items: Foo[];
  filter: string;
};

const initialState: FooState = {
  items: [],
  filter: '',
};

export const FooStore = signalStore(
  { providedIn: 'root' },
  withState<FooState>(initialState),
  withRequestStatus(),
  withComputed(({ items, filter }) => ({
    filteredItems: computed(() =>
      items().filter((i) => i.name.includes(filter()))
    ),
    count: computed(() => items().length),
  })),
  withMethods((store, api = inject(FooApi)) => ({
    setFilter(filter: string): void {
      patchState(store, { filter });
    },
    async loadAll(): Promise<void> {
      patchState(store, setPending());
      try {
        const items = await api.getAll();
        patchState(store, { items }, setFulfilled());
      } catch (e) {
        patchState(store, setError(e instanceof Error ? e.message : 'Failed'));
      }
    },
  })),
);
```

For collections with stable IDs, swap `items: T[]` in state for `withEntities<T>()`. See `references/entities.md`.

## Workflow

1. **Decide the scope.** Component-local? `providers: [Store]`. App-wide? `{ providedIn: 'root' }`. Per-route? Provide on the route. Don't put route-scoped state in `root`.
2. **Decide on store/service split.** State, computed, and orchestration belong in the store. **HTTP / persistence / WebSocket / IndexedDB** belong in a separate service the store `inject()`s. Both pieces become independently testable; the backend can be swapped without touching the store.
3. **Type the state explicitly.** `withState<FooState>(initialState)` — never inferred-only. Future `patchState` callers depend on the type to catch typos.
4. **Compose features.** Pull request status, entity management, logging into `signalStoreFeature(...)` so they are reusable across stores.
5. **Pick the right async tool.**
   - **`async` method** for a single awaited call.
   - **`rxMethod<Input>(pipe(...))`** when you need RxJS operators (`debounceTime`, `switchMap`, retry, cancellation).
   - **`signalMethod<Input>((value) => ...)`** when input is a signal/value and the body is synchronous — no RxJS overhead.
   - Passing a signal or computation function to either? Call it in an injection context or pass `{ injector }` — calling outside one is deprecated.
6. **Test through the public API only.** `TestBed.inject(Store)`, mock injected services with `provide:`, assert on signals + method effects. Seed protected state with `patchState(unprotected(store), ...)` from `@ngrx/signals/testing`.

## Wrong → Right (the corrections that matter most)

These are the mistakes that show up most often. Keep these patterns visible whenever generating code.

### 1. `updateEntity` — function form for state-dependent updates

```typescript
// ❌ Wrong: read entityMap manually then patch with a partial — race-prone
//          and leaks store internals into the method body.
const current = store.entityMap()[id];
patchState(store, updateEntity({ id, changes: { completed: !current.completed } }));

// ✅ Right: function-form changes — atomic, no manual lookup.
patchState(store, updateEntity({
  id,
  changes: (todo) => ({ completed: !todo.completed }),
}));
```

### 2. `removeEntities` — predicate form, not filter+map

```typescript
// ❌ Wrong: pulls everything into the method body, fragile if signal shape changes.
const ids = store.entities().filter((t) => t.completed).map((t) => t.id);
patchState(store, removeEntities(ids));

// ✅ Right: predicate is evaluated against each entity by the updater.
patchState(store, removeEntities((todo) => todo.completed));
```

### 3. Optimistic update — snapshot with `getState`, restore on error

```typescript
// ❌ Wrong: no rollback path — the UI lies if the API call fails.
patchState(store, addEntity(item));
await api.add(item);

// ✅ Right: snapshot, patch optimistically, restore on failure.
const previous = getState(store);
patchState(store, addEntity(item));
try {
  await api.add(item);
} catch (e) {
  patchState(store, { entityMap: previous.entityMap, ids: previous.ids });
  throw e;
}
```

### 4. Request status — standalone updaters, not store-bound methods

```typescript
// ❌ Wrong: reaching into the store from the feature, can't compose into patchState calls.
withMethods((store) => ({ setPending() { patchState(store, { requestStatus: 'pending' }); } }))

// ✅ Right: standalone updater functions that return Partial<State>.
//          Compose with other updaters in a single patchState call.
export function setPending(): RequestStatusState { return { requestStatus: 'pending' }; }
patchState(store, setAllEntities(items), setFulfilled());
```

### 5. Custom feature with prerequisites — typed overload

```typescript
// ❌ Wrong: bare signalStoreFeature — no compile error if used without withEntities.
export function withSelectedEntity<T>() {
  return signalStoreFeature(withState({ selectedId: null }), /* ... */);
}

// ✅ Right: declare prerequisite via { state: type<EntityState<T>>() }.
//          Now misuse is a compile error.
export function withSelectedEntity<T extends { id: EntityId }>() {
  return signalStoreFeature(
    { state: type<EntityState<T>>() },
    withState<{ selectedId: EntityId | null }>({ selectedId: null }),
    withComputed(({ selectedId, entityMap }) => ({
      selectedEntity: computed(() => {
        const id = selectedId();
        return id == null ? null : (entityMap()[id] ?? null);
      }),
    })),
    withMethods((store) => ({
      select(id: EntityId): void { patchState(store, { selectedId: id }); },
      clearSelection(): void { patchState(store, { selectedId: null }); },
    })),
  );
}
```

### 6. Refactoring `BehaviorSubject` — what maps to what

| Before                                | After                                                 |
| ------------------------------------- | ----------------------------------------------------- |
| `BehaviorSubject<T>` field            | state slice in `withState<FooState>(...)`             |
| `.next(...)`                          | `patchState(store, ...)`                              |
| `.value`                              | signal getter `store.foo()` (or `getState(store)`)    |
| `combineLatest` / `map` derived obs   | `withComputed(...)` returning `computed(...)`         |
| HTTP method on the service            | new `FooApi` service injected by the store            |
| Component subscribes via `\| async`   | reads signal: `{{ store.foo() }}` (no async pipe)     |

## Output contract

When generating code:

- **Always** TypeScript with explicit state types and explicit imports from `@ngrx/signals` / `@ngrx/signals/entities` / `@ngrx/signals/rxjs-interop`.
- **Always** use `patchState` for mutations — never assign to signals directly.
- **Always** keep HTTP in a service injected via `inject(SomeApi)`; the store does not import `HttpClient`.
- **Prefer** named, typed updater functions (`setPending()`, `setError(msg)`) when the same update happens in multiple places.
- **Prefer** `withEntities<T>()` over a hand-rolled `T[]` array when items have stable `id`s.
- **Prefer** `signalMethod` over `rxMethod` when no RxJS operator is needed.
- Comment only when the WHY is non-obvious. Don't narrate the obvious.

## Pattern → reference quick map

Read the **one** reference file that matches the task. Most tasks need only one.

| Task                                        | Read                              |
| ------------------------------------------- | --------------------------------- |
| Plain feature store, computed, methods      | `references/api-reference.md`     |
| `rxMethod` / `signalMethod` / RxJS bridge   | `references/api-reference.md`     |
| `withLinkedState`, `watchState`, events plugin, resource extensions | `references/api-reference.md` |
| Entity collection (CRUD with IDs)           | `references/entities.md`          |
| Custom reusable feature (`withXxx`)         | `references/custom-features.md`   |
| Architecture: scoping, optimistic, BS-refactor | `references/patterns.md`        |
| Tests: TestBed, mocks, signalMethod tests   | `references/testing.md`           |

If a task spans patterns (e.g., "entity store with optimistic updates and request status"), the scaffold above plus `entities.md` is usually enough.

## Anti-patterns to flag

If the request or existing code shows any of these, point it out:

- **Confusing `@ngrx/store` with `@ngrx/signals`.** Different packages. `createReducer`/`createEffect`/`@Effect` is classic NgRx, not Signal Store.
- **HTTP inside the store.** Push it into a service. The store should not import `HttpClient`.
- **Cross-store imports.** Stores never inject other stores. Lift state up, compose via a feature, or share a service.
- **Hand-rolled `T[]` arrays for entity data.** With stable IDs, use `withEntities<T>()`.
- **Direct signal mutation from outside.** `store.count.set(...)` breaks encapsulation. Expose a method.
- **`rxMethod` for trivial sync logic.** Use `signalMethod` or a plain method.
- **Re-syncing one slice when another changes** (an `effect` or method that resets `selectedId` when `items` change). Use `withLinkedState`.
- **Untyped state.** `withState({ count: 0 })` works; `withState<CounterState>({ count: 0 })` makes future patches type-safe.
- **Outdated API names.** `withEffects` (events plugin) is now `withEventHandlers`; entity `idKey` is now `selectId` via `entityConfig`; `rxMethod(...).unsubscribe()` is now `.destroy()`.

Files in this skill

  • README.md4.9 KB
  • SKILL.md12.8 KB
  • evals/checks/tsc.sh2.8 KB
  • evals/evals.json12.2 KB
  • references/api-reference.md17.1 KB
  • references/custom-features.md6.1 KB
  • references/entities.md6 KB
  • references/patterns.md7.5 KB
  • references/testing.md7.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.