本文へ移動
cccskills
無料GitHub で公開

ngrx-signals

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.

日本語の概要は準備中です。原文の説明を表示しています。

インストール方法を見る

含まれるファイル(9)

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

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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:

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.

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

// ❌ 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

// ❌ 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

// ❌ 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

// ❌ 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

// ❌ 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

BeforeAfter
BehaviorSubject<T> fieldstate slice in withState<FooState>(...)
.next(...)patchState(store, ...)
.valuesignal getter store.foo() (or getState(store))
combineLatest / map derived obswithComputed(...) returning computed(...)
HTTP method on the servicenew FooApi service injected by the store
Component subscribes via | asyncreads 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 ids.
  • 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.

TaskRead
Plain feature store, computed, methodsreferences/api-reference.md
rxMethod / signalMethod / RxJS bridgereferences/api-reference.md
withLinkedState, watchState, events plugin, resource extensionsreferences/api-reference.md
Entity collection (CRUD with IDs)references/entities.md
Custom reusable feature (withXxx)references/custom-features.md
Architecture: scoping, optimistic, BS-refactorreferences/patterns.md
Tests: TestBed, mocks, signalMethod testsreferences/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().

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

Run a structured bug-focused code review for selected files, folders, or the whole workspace.

日本語の概要は準備中です。原文の説明を表示しています。

danielsogl/copilot-workflow-demo392026年10月22日 更新

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, services, or HTTP communication, or for best practices on reactivity (signals, linkedSignal, resource, httpResource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, naming conventions, or CLI tooling.

日本語の概要は準備中です。原文の説明を表示しています。

danielsogl/copilot-workflow-demo392026年10月22日 更新

Generate an Angular Signal Forms component with schema validation and dynamic field management for a given entity.

日本語の概要は準備中です。原文の説明を表示しています。

danielsogl/copilot-workflow-demo392026年10月22日 更新

bdd

無料

Write disciplined Behavior-Driven Development specs and tests with Gherkin / Cucumber. Use whenever the user mentions BDD, Gherkin, Cucumber, .feature files, Given/When/Then, scenarios, Scenario Outline, Background, step definitions, acceptance criteria, executable specifications, living documentation, the Three Amigos, or Example Mapping. Also trigger when the user asks to "write tests in plain English", convert acceptance criteria into tests, design step definitions, refactor an imperative or UI-coupled feature file, or wire up BDD with Vitest (unit/component) or Playwright (end-to-end). Do NOT use for pure unit tests with no business-readable contract, pure performance/load tests, or generic test-framework questions unrelated to behavior specs.

日本語の概要は準備中です。原文の説明を表示しています。

danielsogl/copilot-workflow-demo392026年10月22日 更新

Generate an NgRx Signal Store with complete CRUD operations (API service, store, tests) for a given entity.

日本語の概要は準備中です。原文の説明を表示しています。

danielsogl/copilot-workflow-demo392026年10月22日 更新

Review the current branch against its base branch: lint, tests, architecture and Angular 22 conventions, reported by severity.

日本語の概要は準備中です。原文の説明を表示しています。

danielsogl/copilot-workflow-demo392026年10月22日 更新

danielsogl のスキルをすべて見る

このスキルの問題を報告する