mcpField
W
web-state-ngrx-signalstore

web-state-ngrx-signalstore

github.com/agents-inc開発2637 回インストール2 日前に更新
リポジトリ

web-state-ngrx-signalstore について

Angular開発者向けに、NgRx SignalStoreの機能構成、状態、エンティティ、副作用、テスト、移行パターンを解説します。機能の順序、型付きストア、タスクに応じたリアクティブ処理の選択に重点を置いています。

主な機能

  1. 01順序に沿ったストア機能の構成
  2. 02patchStateによる状態更新
  3. 03withEntitiesによるID付きコレクション管理
  4. 04rxMethodによる非同期副作用
  5. 05再利用可能なストア機能の作成
  6. 06ストアのテストと移行

活用シーン

  1. 01アプリ全体またはコンポーネント単位のストア構築
  2. 02IDによるエンティティ管理
  3. 03新しい検索で古いリクエストをキャンセル
  4. 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.yaml
reference.md
examples/
core.md
effects.md
entities.md
features.md
migration.md
testing.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
rxMethodsignalMethod
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>

コメント