mcpField
N
ngrx-signals

ngrx-signals

github.com/danielsogl開発542 回インストール7 週間前に更新
リポジトリ

ngrx-signals について

ngrx-signalsは、`@ngrx/signals`を使った慣用的で型安全なAngularの状態管理コードを作成するスキルです。Signal Storeの構成、非同期処理、エンティティ、設計、テストを扱います。

主な機能

  1. 01型付きSignal Storeの状態と更新
  2. 02組み合わせ可能なStore機能
  3. 03非同期メソッドのパターン
  4. 04エンティティコレクションのパターン
  5. 05公開APIを通じたテスト

活用シーン

  1. 01Angularの機能やページの状態管理
  2. 02BehaviorSubjectベースのサービスのリファクタリング
  3. 03エンティティCRUDと楽観的更新の実装
  4. 04再利用可能なSignal Store機能の作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/danielsogl/skills --skill ngrx-signals

インストール後、Claude Code で/ngrx-signalsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
evals/
evals.json
evals/checks/
tsc.sh
references/
api-reference.md
custom-features.md
entities.md
patterns.md
testing.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 つのルール:

  1. 状態の更新には patchState(store, updater) を使います。 直接変更する方法はありません。アップデーターには部分オブジェクト、関数 (state) => Partial<State>、またはそのいずれかを返すカスタムアップデーターを指定できます。
  2. ストアは Angular のプロバイダーです。 inject(Store) または TestBed.inject(Store) で注入します。ルート、ルート定義、またはコンポーネント(providers: [Store])にプロバイダーを設定し、コンポーネントスコープの状態を管理します。
  3. 継承より合成を優先します。 再利用には、クラスの拡張ではなく 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 を参照してください。

ワークフロー

  1. スコープを決めます。 コンポーネントローカルですか?providers: [Store] を使います。アプリ全体ですか?{ providedIn: 'root' } を使います。ルートごとですか?ルートにプロバイダーを設定します。ルートスコープの状態を root に置かないでください。
  2. ストアとサービスの分担を決めます。 状態、計算、オーケストレーションはストアに含めます。HTTP/永続化/WebSocket/IndexedDB は、ストアが inject() する別のサービスに任せます。どちらも独立してテストでき、ストアに手を加えずにバックエンドを交換できます。
  3. 状態に明示的な型を指定します。 withState<FooState>(initialState) を使い、推論のみに頼らないでください。後から呼び出す patchState でのタイプミスを検出するには、型が必要です。
  4. 機能を合成します。 リクエスト状態、エンティティ管理、ロギングを signalStoreFeature(...) に切り出し、複数のストアで再利用できるようにします。
  5. 適切な非同期処理ツールを選びます。
    • 単一の呼び出しを待つ場合は async メソッド。
    • RxJS オペレーター(debounceTime、switchMap、リトライ、キャンセル)が必要な場合は rxMethod<Input](pipe(...))。
    • 入力がシグナル/値で、本文が同期処理の場合は signalMethod<Input>((value) => ...) — RxJS のオーバーヘッドはありません。
    • どちらかにシグナルまたは計算関数を渡しますか?インジェクションコンテキスト内で呼び出すか、{ injector } を渡してください。インジェクションコンテキスト外での呼び出しは非推奨です。
  6. 公開 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 による派生 obscomputed(...) を返す 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() です。

コメント