ngrx-signals について
ngrx-signalsは、`@ngrx/signals`を使った慣用的で型安全なAngularの状態管理コードを作成するスキルです。Signal Storeの構成、非同期処理、エンティティ、設計、テストを扱います。
主な機能
- 01型付きSignal Storeの状態と更新
- 02組み合わせ可能なStore機能
- 03非同期メソッドのパターン
- 04エンティティコレクションのパターン
- 05公開APIを通じたテスト
活用シーン
- 01Angularの機能やページの状態管理
- 02BehaviorSubjectベースのサービスのリファクタリング
- 03エンティティCRUDと楽観的更新の実装
- 04再利用可能なSignal Store機能の作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ danielsogl/ skills --skill ngrx-signalsインストール後、Claude Code で/ngrx-signalsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdevals/evals.jsonevals/checks/tsc.shreferences/api-reference.mdcustom-features.mdentities.mdpatterns.mdtesting.md詳細
NgRx Signal Store
Angular 向けのモダンで軽量な、シグナルベースの状態管理です。このスキルでは、慣用的で型安全、適切に設計された @ngrx/signals コードを作成します。
このスキルを使う場面
次のいずれかに該当する場合に使用してください。
- ユーザーが API を指定している場合:
signalStore、signalStoreFeature、withState、withMethods、withComputed、withLinkedState、withHooks、withProps、withFeature、withEntities、rxMethod、signalMethod、patchState、getState、watchState、signalState、または@ngrx/signals/events/@ngrx/signals/testing。 BehaviorSubject/Subjectを使うサービスや、変更可能なフィールドを持つクラスを Signal Store にリファクタリングしたい場合。- Angular の機能/ページの状態を求めており、Angular Signals に言及しているか、「モダン」なアプローチを求めている場合。
- カスタムストア機能(
withRequestStatus、withSelectedEntity、withLoggerなど)や、機能の合成方法について質問している場合。 - Angular の機能における楽観的更新、リクエスト状態、エンティティコレクション、または HTTP API に対する CRUD について質問している場合。
従来の @ngrx/store(アクション、リデューサー、エフェクト、ファイルを分けたセレクター)、Signal Store を使わない Redux DevTools の設定、または純粋な RxJS パイプラインに関する質問には使用しないでください。
基本的な考え方
Signal Store は、合成された機能のツリーです。各機能は、状態、計算シグナル、メソッド、プロパティ、ライフサイクルフックを組み合わせて追加します。合成は定義時に行われます。
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 })
);
すべての Signal Store 作成者が理解しておくべき 3 つのルール:
- 状態の更新には
patchState(store, updater)を使います。 直接変更する方法はありません。アップデーターには部分オブジェクト、関数(state) => Partial<State>、またはそのいずれかを返すカスタムアップデーターを指定できます。 - ストアは Angular のプロバイダーです。
inject(Store)またはTestBed.inject(Store)で注入します。ルート、ルート定義、またはコンポーネント(providers: [Store])にプロバイダーを設定し、コンポーネントスコープの状態を管理します。 - 継承より合成を優先します。 再利用には、クラスの拡張ではなく
signalStoreFeature(...)で作成したカスタム機能を使います。
標準的なひな形(出発点として使用)
機能ストアの 80% は、この形になります。これをコピーして調整し、分かりにくい点についてはリファレンスを確認してください。
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'));
}
},
})),
);
安定した ID を持つコレクションでは、状態の items: T[] を withEntities<T>() に置き換えてください。references/entities.md を参照してください。
ワークフロー
- スコープを決めます。 コンポーネントローカルですか?
providers: [Store]を使います。アプリ全体ですか?{ providedIn: 'root' }を使います。ルートごとですか?ルートにプロバイダーを設定します。ルートスコープの状態をrootに置かないでください。 - ストアとサービスの分担を決めます。 状態、計算、オーケストレーションはストアに含めます。HTTP/永続化/WebSocket/IndexedDB は、ストアが
inject()する別のサービスに任せます。どちらも独立してテストでき、ストアに手を加えずにバックエンドを交換できます。 - 状態に明示的な型を指定します。
withState<FooState>(initialState)を使い、推論のみに頼らないでください。後から呼び出すpatchStateでのタイプミスを検出するには、型が必要です。 - 機能を合成します。 リクエスト状態、エンティティ管理、ロギングを
signalStoreFeature(...)に切り出し、複数のストアで再利用できるようにします。 - 適切な非同期処理ツールを選びます。
- 単一の呼び出しを待つ場合は
asyncメソッド。 - RxJS オペレーター(
debounceTime、switchMap、リトライ、キャンセル)が必要な場合はrxMethod<Input](pipe(...))。 - 入力がシグナル/値で、本文が同期処理の場合は
signalMethod<Input>((value) => ...)— RxJS のオーバーヘッドはありません。 - どちらかにシグナルまたは計算関数を渡しますか?インジェクションコンテキスト内で呼び出すか、
{ injector }を渡してください。インジェクションコンテキスト外での呼び出しは非推奨です。
- 単一の呼び出しを待つ場合は
- 公開 API のみを通してテストします。
TestBed.inject(Store)を使い、注入サービスはprovide:でモックし、シグナルとメソッドの効果をアサートします。@ngrx/signals/testingのpatchState(unprotected(store), ...)を使って、保護された状態に初期値を設定します。
誤り → 正解(特に重要な修正点)
よく見られる間違いを紹介します。コードを生成するときは、常にこれらのパターンを意識してください。
1. updateEntity — 状態に依存する更新には関数形式を使用
// ❌ 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 — 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. 楽観的更新 — getState でスナップショットを取得し、エラー時に復元
// ❌ 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. リクエストの状態 — ストアにバインドされたメソッドではなく、独立したアップデーターを使用
// ❌ 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. 前提条件のあるカスタム機能 — 型付きオーバーロード
// ❌ 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. BehaviorSubject のリファクタリング — 何を何に置き換えるか
| 変更前 | 変更後 |
|---|---|
BehaviorSubject<T> フィールド | withState<FooState>(...) 内の状態スライス |
.next(...) | patchState(store, ...) |
.value | シグナルのゲッター store.foo()(または getState(store)) |
combineLatest / map による派生 obs | computed(...) を返す withComputed(...) |
| サービスの HTTP メソッド | ストアがインジェクトする新しい FooApi サービス |
コンポーネントが | async で購読 | シグナルを読み取る:{{ store.foo() }}(async pipe なし) |
出力の要件
コードを生成するとき:
- 必ず、状態の型と
@ngrx/signals/@ngrx/signals/entities/@ngrx/signals/rxjs-interopからの明示的なインポートを含む TypeScript を使用してください。 - 必ず、変更には
patchStateを使用してください。シグナルに直接代入してはいけません。 - 必ず、HTTP 処理は
inject(SomeApi)でインジェクトするサービスに置いてください。ストアでHttpClientをインポートしてはいけません。 - 同じ更新を複数の箇所で行う場合は、名前付きの型付きアップデーター関数(
setPending()、setError(msg))を優先してください。 - アイテムに安定した
idがある場合は、独自実装のT[]配列よりwithEntities<T>()を優先してください。 - RxJS オペレーターが不要な場合は、
rxMethodよりsignalMethodを優先してください。 - WHY が自明でない場合にのみコメントを記述してください。自明なことを説明するコメントは不要です。
パターン → リファレンス早見表
タスクに合ったリファレンスファイルを1つ読んでください。ほとんどのタスクで必要なのは1つだけです。
| タスク | 参照先 |
|---|---|
| 基本的な機能ストア、computed、メソッド | references/api-reference.md |
rxMethod / signalMethod / RxJS ブリッジ | references/api-reference.md |
withLinkedState、watchState、イベントプラグイン、リソース拡張 | references/api-reference.md |
| エンティティコレクション(ID を使った CRUD) | references/entities.md |
再利用可能なカスタム機能(withXxx) | references/custom-features.md |
| アーキテクチャ:スコープ設定、楽観的更新、BS のリファクタリング | references/patterns.md |
| テスト:TestBed、モック、signalMethod のテスト | references/testing.md |
複数のパターンにまたがるタスク(例:「楽観的更新とリクエスト状態を備えたエンティティストア」)では、通常、上記のひな形と entities.md だけで十分です。
指摘すべきアンチパターン
依頼内容または既存のコードに以下のいずれかが含まれている場合は、指摘してください。
@ngrx/storeと@ngrx/signalsの混同。 別のパッケージです。createReducer/createEffect/@Effectは従来の NgRx であり、Signal Store ではありません。- ストア内での HTTP 通信。 サービスに移してください。ストアで
HttpClientをインポートしないでください。 - ストア間のインポート。 ストアから別のストアをインジェクトしないでください。状態を上位に引き上げる、フィーチャー経由で合成する、またはサービスを共有してください。
- エンティティデータに手書きの
T[]配列を使用する。 安定した ID がある場合は、withEntities<T>()を使用してください。 - 外部からのシグナルの直接変更。
store.count.set(...)はカプセル化を損ないます。メソッドを公開してください。 - 単純な同期ロジックに
rxMethodを使用する。signalMethodまたは通常のメソッドを使用してください。 - 別のスライスが変更されたときに、あるスライスを再同期すること(
itemsの変更時にselectedIdをリセットするeffectまたはメソッドなど)。withLinkedStateを使用してください。 - 型なしの状態。
withState({ count: 0 })でも動作しますが、withState<CounterState>({ count: 0 })にすると、将来のパッチが型安全になります。 - 古い API 名。
withEffects(イベントプラグイン)は現在withEventHandlersです。エンティティのidKeyは現在、entityConfig経由のselectIdです。rxMethod(...).unsubscribe()は現在.destroy()です。