web-state-ngrx-signalstore について
Angular開発者向けに、NgRx SignalStoreの機能構成、状態、エンティティ、副作用、テスト、移行パターンを解説します。機能の順序、型付きストア、タスクに応じたリアクティブ処理の選択に重点を置いています。
主な機能
- 01順序に沿ったストア機能の構成
- 02patchStateによる状態更新
- 03withEntitiesによるID付きコレクション管理
- 04rxMethodによる非同期副作用
- 05再利用可能なストア機能の作成
- 06ストアのテストと移行
活用シーン
- 01アプリ全体またはコンポーネント単位のストア構築
- 02IDによるエンティティ管理
- 03新しい検索で古いリクエストをキャンセル
- 04actionsとreducersからの移行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-state-ngrx-signalstoreインストール後、Claude Code で/web-state-ngrx-signalstoreを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/core.mdeffects.mdentities.mdfeatures.mdmigration.mdtesting.md詳細
NgRx SignalStore のパターン
クイックガイド: SignalStore は、
withState、withComputed、withMethods、withHooksの各機能をこの順番で適用して構成する Angular サービスです。各機能から参照できるのは、それより前に適用された機能だけです。更新はすべてpatchStateを通じて行います。v18 以降、state は外部からの書き込みから保護され、v19 以降は開発環境でディープフリーズされます。そのため、FormGroupやその他の可変オブジェクトは state ではなくwithPropsに含めます。
詳細なリソース:
- examples/core.md —
signalStore、withState、withComputed、withMethods、withProps、DI、コンポーネントスコープのストア - examples/entities.md —
withEntities、CRUD アップデーター、prependEntity/upsertEntity(v20 以降) - examples/effects.md —
rxMethod、signalMethod(v19 以降)、副作用 - examples/features.md —
signalStoreFeature、カスタム機能、呼び出し状態、DevTools - examples/testing.md — ユニットテスト、
unprotected()、モックの方法 - examples/migration.md — アクション、リデューサー、エフェクトからの移行
- reference.md — 比較表、アンチパターンのコード、TypeScript のレシピ、移行手順
該当する方法
- アプリ全体を対象とするストア —
signalStore({ providedIn: "root" })。 examples/core.md を参照してください。 - コンポーネントを対象とするストア — コンポーネントの
providersに登録します。コンポーネントとともに破棄され、rxMethodのサブスクリプションも同時に破棄されます。examples/core.md を参照してください。 - ID をキーとするコレクション — state に配列を置くのではなく、
withEntitiesを使用します。examples/entities.md を参照してください。
<critical_requirements>
SignalStore のコードを書く前に
状態は patchState() を通じて更新してください。 新しい状態オブジェクトを生成してシグナルに通知します。既存のオブジェクトに値を代入しても、監視しているものには何も通知されません。また、v18 以降は状態が保護されているため、代入自体が拒否されます。
各機能が必要なものを使えるよう、順序を整えてください。 機能から参照できるのは、それより前に宣言されたメンバーだけです。そのため、withState、withComputed、それを読み取る withMethods の順に配置します。
コレクションは withEntities() で管理してください。 entityMap と ids 配列を管理するため、ID による検索を一定時間で行えるほか、CRUD 用の更新関数も利用できます。状態に配列を保持すると、検索のたびに走査が必要になり、更新ロジックも自分で記述することになります。
変更可能なオブジェクトは状態ではなく withProps() に置いてください。 v19 以降、開発ビルドでは状態が再帰的に凍結されるため、FormGroup やクラスのインスタンスを状態に格納すると、内部で最初に書き込みを行ったときにエラーが発生します。
キャンセル可能な非同期処理には rxMethod() を使ってください。 サブスクリプションをストアのライフサイクルに結び付け、switchMap を使う場所を提供します。これにより、置き換えられたリクエストが後から反映されるのではなく、停止するようになります。
</critical_requirements>
自動検出: signalStore、signalStoreFeature、withState、withComputed、withMethods、withHooks、withProps、withEntities、withFeature、withLinkedState、patchState、rxMethod、signalMethod、entityConfig、unprotected、@ngrx/signals
対象:
- 機能を組み合わせてストアを構成する方法と、その順序
- 状態の更新、計算値、エンティティコレクション
- RxJS オペレーターを使うかどうかを含む、副作用
- 再利用可能な機能、ライフサイクルフック、呼び出し状態の追跡
- ストアのテストと、アクションやリデューサーからの移行
ここでは扱わないもの:
- ネットワーク経由の取得 — リクエスト自体はサービスに置きます。このスキルでは、その周辺での保持、導出、キャンセルを扱います
- 1 つのコンポーネントが読み取る値 — コンポーネント内の通常のシグナルのほうが、仕組みが簡潔です
- フィルター、検索、ページネーション — リロード後も維持され、共有もできるよう、ルートのクエリに置きます
ストアとは、機能を左から右へ適用するリストです。それぞれの機能がメンバーを追加し、その結果を次の機能が受け取ります。仕組みはこれだけです。アクションもリデューサーもセレクターもレジストリもありません。ストアの型は機能を積み重ねた結果になるため、存在しないメンバーは undefined になるのではなく、コンパイルエラーになります。
ここから 2 つの帰結が導かれます。構成は直線的であるため、順序は見た目の問題ではなく意味上の問題です。たとえば、読み取り対象の withState より前に withComputed を置いても、その状態は参照できません。また、ストアは Angular のサービスであるため、そのライフタイムは DI に関する問題です。1 つのインスタンスにするには providedIn: "root" を、コンポーネントごとに 1 つのインスタンスにしてコンポーネントとともに破棄するには、コンポーネントの providers を使います。
<decision_framework>
withState と withEntities のどちらを使うか
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 bulk
withState と withProps のどちらを使うか
Does the value participate in reactivity?
├─ YES → withState — signals, patchState, and the dev-mode freeze
└─ NO → withProps — services, subjects, form groups, anything that mutates itself
</decision_framework>
rxMethod または signalMethod
rxMethod | signalMethod | |
|---|---|---|
| RxJS が必要 | はい | いいえ |
オペレーター — debounceTime、switchMap | はい | いいえ |
| 後続の呼び出しによる先行呼び出しのキャンセル | 組み込み | 自分で処理 |
| 受け付ける値 | T、Signal<T>、Observable<T> | T、Signal<T> |
| インジェクションコンテキスト | 必要 | 不要 |
競合が起きる可能性のあるもの、たとえば入力中の検索や、変化する ID をキーにしたリクエストには、switchMap を使う rxMethod が適しています。シグナルにのみ反応する副作用には、RxJS 依存のない signalMethod が適しています。
withMethods 内の単純な async メソッドは第 3 の選択肢であり、ユーザーの明示的な操作によって実行される単一のフェッチには適切です。キャンセルするものも、反応するストリームもない場合に使えます。try/catch が使える一方で、キャンセル機能はありません。まず確認すべきなのは、これらの処理のうち 2 つが同時に実行される可能性があるかどうかです。
</decision_framework>
基本パターン
パターン 1:ストアの構成
機能は順番に適用します。まず状態、次にそこから導出される値、その後に状態を変更するメソッド、最後にフックです。
export const CounterStore = signalStore(
{ providedIn: "root" },
withState({ count: 0 }),
withComputed(({ count }) => ({ doubled: computed(() => count() * 2) })),
withMethods((store) => ({
increment: () => patchState(store, { count: store.count() + 1 }),
})),
);
コード全文:examples/core.md
パターン 2:patchState による状態更新
部分オブジェクト、アップデーター関数、またはその複数を 1 回の呼び出しで受け取り、単一の通知として適用します。
patchState(store, { isLoading: true });
patchState(store, (state) => ({ count: state.count + 1 }));
patchState(store, setAllEntities(users), { isLoading: false });
コード全文:examples/core.md
パターン 3:withEntities によるエンティティコレクション
正規化されたストレージとアップデーターを提供します:setAllEntities、addEntity/addEntities、setEntity/setEntities、updateEntity/updateEntities、removeEntity/removeEntities。
export const UserStore = signalStore(
withEntities<User>(),
withMethods((store) => ({
add: (user: User) => patchState(store, addEntity(user)),
})),
);
id 以外の ID フィールド名を指定するには entityConfig() を使います。また、名前付きコレクションでは、各アップデーターにその名前を渡す必要があります。
コード全文:examples/entities.md
パターン 4:rxMethod による RxJS の副作用
値、シグナル、または Observable を受け付けます。ファクトリーがストリームにパイプを適用し、ストアとともにサブスクリプションを終了します。
const search = rxMethod<string>(
pipe(
debounceTime(SEARCH_DEBOUNCE_MS),
switchMap((term) => api.search(term)),
tap((results) => patchState(store, { results })),
),
);
外側のストリームにエラーが到達すると、そのストリームは完全に終了するため、catchError は switchMap の内側に置きます。
コード全文:examples/effects.md
パターン 5:シグナルのみの副作用に signalMethod を使う(v19 以降)
競合が起きない副作用には、RxJS とインジェクションコンテキストを使わず、同じリアクティブなトリガーを使用できます。
const logSelection = signalMethod<string>((id) => {
analytics.track("selected", id);
});
追跡されるのはパラメーターのシグナルのみです。プロセッサー内で読み取られるシグナルは追跡されません。
コード全文:examples/effects.md
パターン 6:signalStoreFeature による再利用可能な機能
名前付きの機能グループで、宣言された形状を満たす任意のストアに適用できます。
export function withLoading() {
return signalStoreFeature(
withState({ isLoading: false }),
withMethods((store) => ({
setLoading: (isLoading: boolean) => patchState(store, { isLoading }),
})),
);
}
type<...>() ヘルパーは、ホストストアがすでに備えている必要のあるものを宣言します。これにより、その機能独自の型を解決できます。
コード全文:examples/features.md
パターン 7:withHooks によるライフサイクル
onInit はストアの構築時に実行され、onDestroy はそのインジェクターが破棄されるときに実行されます。そのため、コンポーネントスコープのストアは、コンポーネント自身のライフサイクルに合わせてクリーンアップされます。
コード全文:examples/core.md
パターン 8:呼び出し状態
ストアまたは名前付きコレクションの loading/loaded/error を保持する機能です。これにより、すべての非同期処理で同じ 3 つの状態を報告できます。コミュニティの機能ライブラリにも実装がありますが、形状は簡潔なので、自分で作成することもできます。
コード全文:examples/features.md
### パターン 9:withFeature を使ったコンテキスト対応機能(v20+)signalStoreFeature がストアを参照せずに合成するのに対し、withFeature はストアを受け取るため、汎用機能からストアがすでに持っているメソッドを呼び出せます。
// 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 です。fetch 関数は store.load をクロージャーで参照します。このメソッドは、その上にある withMethods が適用されるまでは存在しません。
全コード:examples/features.md
パターン 10:withLinkedState(v20+)
ソースシグナルから派生しながら書き込みも可能な状態です。デフォルトではデータに追従し、ユーザーが何かを選択するとその値を保持する選択状態です。
export const FilterStore = signalStore(
withState({ items: [] as Item[] }),
withLinkedState(({ items }) => ({
selectedId: () => items()[0]?.id ?? null,
})),
);
withComputed でも同じデフォルト値を設定できますが、書き込みはできません。
全コード:examples/features.md
<red_flags>
注意すべき点
実行時に問題が発生します:
patchStateを呼び出さずに状態へ代入すること — v18 以降は保護機能によって拒否されます。それ以前は、どのシグナルも監視していない値が変更されていました。withStateに保持された、FormGroup、クラスインスタンス、その他の自己変更するオブジェクト — v19 の開発時フリーズは再帰的に適用されるため、オブジェクトが自身を更新しようとすると、最初の時点で例外が発生します。このようなオブジェクトはwithPropsに置くのが適切です。- 後から宣言されるメンバーを機能から読み取ること — 合成は左から右に行われるため、そのメンバーはまだ存在せず、ストアはコンパイルできません。
rxMethodの内部switchMapの外側にcatchErrorを置くこと — エラーが外側のストリームに伝わってストリームが完了し、メソッドはストアの存続期間中、以降ひそかに動作しなくなります。- 名前付きコレクションに対して、
collectionオプションを指定せずにエンティティ更新関数を使うこと — 代わりにデフォルトのコレクションが操作されるため、書き込み先の値は何からも読み取られません。 - 合成した機能を返さない
signalStoreFeature— 関数としては型チェックを通りますが、何も追加されないため、追加されるはずだったメンバーが存在しません。 - 2 つのストアが互いにインジェクトすること — 構築時に循環が発生し、明確な DI エラーではなく、メンバーが undefined のストアとして表面化します。
予想外の動作:
rxMethodはストアの破棄時に自動で購読解除されます。手動で追加の購読解除を行っても、せいぜい重複するだけです。withHooks.onInitは構築中に実行されるため、初期化時にデータを取得するストアは、それをインジェクトするすべてのテストでデータを取得します。signalMethodが追跡するのは、パラメーターとして渡されたシグナルだけです。プロセッサー内で読み取られるシグナルは、意図的に追跡されません。signalMethodにはキャンセル機能がないため、進行中の非同期呼び出しが 2 つあると、完了した順に解決され、最後に書き込まれた値が勝ちます。どちらの呼び出しが後に要求されたかは関係ありません。- コンポーネントスコープのストアは、コンポーネントのインスタンスごとに新しく作成されます。それが本来の動作であり、ナビゲーションで一度離れて戻っても状態が保持されない理由です。
- 深くネストされた状態では、
patchStateのたびにパス全体が再構築されます。複数のフラットな状態スライスなら、それぞれ独立して更新されます。 - エンティティオブジェクトをその場で変更すると
updateEntityがスキップされるため、entityMapの参照が変わらず、それを読み取る側は再レンダリングされません。 idという名前ではない ID フィールドをwithEntitiesに指定するのはentityConfig()の役割です。これがないと、更新関数はキーとしてundefinedを使います。
</red_flags>