mcpField
X
xstate

xstate

github.com/pedronauck開発648239 回インストール2 日前に更新
リポジトリ

xstate について

XState v5を使う開発者向けの実践ガイドです。ステートマシン、@xstate/store、TanStack Queryとの連携に加え、型安全性やReact統合の指針をまとめています。

主な機能

  1. 01ステートマシンとアクターモデリング
  2. 02@xstate/storeによるイベント駆動型状態管理
  3. 03XStateとTanStack Queryの連携パターン
  4. 04React統合の指針
  5. 05注意点と検証チェックリスト

活用シーン

  1. 01複数ステップのアプリケーションフロー構築
  2. 02軽量なイベント駆動型状態管理
  3. 03データ取得とUI状態の連携

インストール

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

$npx skills add https://github.com/pedronauck/skills --skill xstate

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

同梱ファイル

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

references/
machine-patterns.md
query-patterns.md
store-patterns.md

詳細

XState v5 開発ガイド

このスキルでは、このプロジェクトで XState v5 エコシステムを利用するための包括的なガイドライン、パターン、ベストプラクティスを紹介します。以下が含まれます。

  • XState ステートマシン - 複雑なアプリケーションロジックのための有限ステートマシンとアクター
  • @xstate/store - 軽量な状態管理のためのシンプルなイベント駆動型ストア
  • XState + TanStack Query - データ取得オーケストレーションのための統合パターン

クイックリファレンス

ユースケースツールリファレンス
複雑な状態フロー、ガード、階層状態XState ステートマシンreferences/machine-patterns.md
シンプルなイベント駆動型状態、アトム、元に戻す/やり直し@xstate/storereferences/store-patterns.md
状態オーケストレーションを伴うデータ取得XState + TanStack Queryreferences/query-patterns.md

基本原則

XState ステートマシン
  • 明示的な状態:アプリケーションロジックを、明示的な有限状態とイベントとしてモデル化します
  • アクターモデル:複雑なオーケストレーションにはアクターモデルを採用します
  • あり得ない状態:ステートチャートを使って、あり得ない状態を排除します
  • 宣言的な考え方:状態遷移を宣言的に捉えます
  • 可視化:ステートマシンの視覚的な作成とデバッグには Stately Editor を使います
@xstate/store
  • イベント駆動型の更新:すべての状態更新にイベントを使います(直接変更はしません)
  • ストアをシンプルに保つ:複雑なロジックではなく、データ管理に専念します
  • 最適化されたサブスクリプション:セレクターを活用して、効率的に状態へアクセスします
  • リアクティブな状態:合成可能なリアクティブ状態にはアトムを使います
  • アップグレードパス:複雑さが増したら XState ステートマシンへ移行します
XState + TanStack Query の統合
  • オーケストレーションには XState:ワークフローのオーケストレーションには XState ステートマシンを使います
  • データには TanStack Query:データ取得とキャッシュには TanStack Query を使います
  • 橋渡し役としてのアクター:Query 操作を XState アクターとしてラップします
  • ミューテーション内でのキャッシュ更新:クエリーキャッシュの更新はミューテーションコールバック内に保ちます

XState ステートマシン

setup() API を使ったステートマシンの作成

型安全なステートマシンには、必ず setup() API を使います。

import { setup, createActor } from 'xstate';

export enum CounterEventType {
  INCREMENT = 'increment',
  DECREMENT = 'decrement',
}

const counterMachineSetup = setup({
  types: {
    context: {} as { count: number },
    events: {} as
      | { type: CounterEventType.INCREMENT }
      | { type: CounterEventType.DECREMENT },
    input: {} as { initialCount: number },
  },
  guards: {
    isPositive: ({ context }) => context.count > 0,
  },
});

// Define actions OUTSIDE setup using type-bound helpers
const incrementCount = counterMachineSetup.assign({
  count: ({ context }) => context.count + 1,
});

export const counterMachine = counterMachineSetup.createMachine({
  id: 'counter',
  context: ({ input }) => ({ count: input.initialCount }),
  initial: 'active',
  states: {
    active: {
      on: {
        [CounterEventType.INCREMENT]: {
          actions: incrementCount,
        },
      },
    },
  },
});
React との統合

コンポーネントスコープのステートマシンには useMachine() を使い、アクションのオーバーライドには .provide() を使います。

import { useMachine } from '@xstate/react';
import { useMemo } from 'react';

function Counter({ onExternalAction }) {
  const machine = useMemo(
    () =>
      counterMachine.provide({
        actions: {
          logEvent: () => onExternalAction(),
        },
      }),
    [onExternalAction]
  );

  const [state, send] = useMachine(machine);

  return (
    <button onClick={() => send({ type: CounterEventType.INCREMENT })}>
      Count: {state.context.count}
    </button>
  );
}

詳細なパターンについて:references/machine-patterns.md を参照してください


@xstate/store

ストアの作成

明確な初期コンテキストとイベントベースの遷移を使ってストアを定義します。

import { createStore } from '@xstate/store';

const userStore = createStore({
  context: {
    user: null as { id: string; name: string } | null,
    isLoading: false,
    error: null as string | null,
  },
  on: {
    setUser: (context, event: { user: { id: string; name: string } }) => ({
      ...context, // Always spread to preserve other properties
      user: event.user,
      error: null,
    }),
    setLoading: (context, event: { isLoading: boolean }) => ({
      ...context,
      isLoading: event.isLoading,
    }),
  },
});

// Use the trigger API for type-safe event sending
userStore.trigger.setUser({ user: { id: '1', name: 'John' } });
React との統合
import { useSelector, useStore } from '@xstate/store/react';

function Counter() {
  const store = useStore({
    context: { count: 0 },
    on: {
      increment: (context, event: { by: number }) => ({
        ...context,
        count: context.count + event.by,
      }),
    },
  });
  const count = useSelector(store, (state) => state.context.count);
  return <button onClick={() => store.trigger.increment({ by: 1 })}>{count}</button>;
}
元に戻す/やり直しのパターン
import { createStore } from '@xstate/store';
import { undoRedo } from '@xstate/store/undo';

const editorStore = createStore(
  undoRedo({
    context: { text: '' },
    on: {
      insertText: (context, event: { text: string }) => ({
        ...context,
        text: context.text + event.text,
      }),
    },
  })
);

editorStore.trigger.undo();
editorStore.trigger.redo();

詳細なパターンについて:references/store-patterns.md を参照してください


XState + TanStack Query の統合

QueryObserver によるクエリーのサブスクリプション

リアクティブなサブスクリプションには、QueryObserver とともに fromCallback を使います。

import { fromCallback } from "xstate";
import { QueryObserver, type QueryClient } from "@tanstack/react-query";

const subscribeToQuery = fromCallback(({ input, sendBack }) => {
  const { queryClient, entityId } = input as {
    queryClient: QueryClient;
    entityId: string;
  };

  const observer = new QueryObserver(queryClient, {
    queryKey: ["data", entityId],
    queryFn: async () => { /* fetch logic */ },
  });

  const unsubscribe = observer.subscribe((result) => {
    if (result.data) {
      sendBack({ type: "REMOTE_UPDATE", data: result.data });
    }
  });

  // Emit current result immediately
  const currentResult = observer.getCurrentResult();
  if (currentResult.data) {
    sendBack({ type: "REMOTE_UPDATE", data: currentResult.data });
  }

  return () => unsubscribe();
});
useMutation によるミューテーション

useMutation フックを fromPromise アクターとしてラップし、.provide() 経由で渡します。

const machine = useMemo(
  () =>
    createDataMachine().provide({
      actors: {
        saveData: fromPromise(async ({ input }) => {
          return await saveMutation.mutateAsync(input.data);
        }),
      },
    }),
  [saveMutation]
);

詳細なパターンについて:references/query-patterns.md を参照してください


必須パターン(すべてのツール)

  1. すべてのステートマシンで setup() を使う - 型推論の精度が向上します
  2. イベントタイプには列挙型を使う - UPPER_SNAKE_CASE 規約に従います
  3. アクションは setup() の外で定義する - setup().assign() のような型に紐づいたヘルパーを使います
  4. 連続したアクションには enqueueActions() を使う - 関連するアクションをまとめます
  5. React との統合には .provide() を使う - 外部メソッドでアクションをオーバーライドします
  6. input からコンテキストを初期化する - 動的な初期化には関数形式を使います
  7. ストアでは必ず ...context をスプレッドする - ほかのプロパティを維持します
  8. サブスクリプションをクリーンアップする - コールバックアクターからクリーンアップ関数を返します

各ツールを使う場面

XState ステートマシンを使う場面:
  • 特定の遷移を持つ有限状態が必要な場合
  • ガードや条件付きの遷移ロジックが必要な場合
  • 非同期オーケストレーションのための呼び出しアクターが必要な場合
  • 階層状態または並列状態が必要な場合
  • 複雑な複数ステップのワークフローを構築する場合
@xstate/store を使用する場合:
  • シンプルなグローバル状態管理またはローカル状態管理
  • 複雑なフローを伴わない、イベント駆動型の状態更新
  • 元に戻す/やり直す機能が必要な場合
  • XState への移行手段がある、軽量な代替手段を利用したい場合
XState + TanStack Query を使用する場合:
  • データ取得を UI の状態と連携させる必要がある場合
  • キャッシュ同期を伴うリアルタイム機能を構築する場合
  • サーバー状態を伴う複雑な非同期ワークフローを管理する場合

よくある落とし穴

すべての XState ツール
  • ジェネリックパラメーターは使用せず、代わりに setup() を使用してください
  • setup() の内部でアクションを定義せず、型に対応したヘルパーを使って外部で定義してください
  • イベント型に文字列リテラルを使用せず、代わりに列挙型を使用してください
  • コンテキストを直接変更せず、必ず assign() を使用するか、新しいオブジェクトを返してください
  • ガードを副作用のあるものや、不純なものにしないでください
  • 非推奨の v4 API(interpret()、cond など)を使用しないでください
  • アクターの .start() を忘れないでください
@xstate/store 固有
  • 戻り値では必ず ...context を展開してください
  • クリーンアップなしで購読しないでください
  • 複雑な状態機械にはストアを使用しないでください(XState に移行してください)
TanStack Query との統合
  • QueryObserver をコールバックアクターの外部で作成しないでください
  • マシンのアクション内でクエリーキャッシュを更新しないでください
  • ミューテーションフックをアクターに直接渡さず、fromPromise でラップしてください
  • クロージャーから QueryClient にアクセスせず、コンテキスト経由で渡してください

検証チェックリスト

XState エコシステムに関わるタスクを完了する前に、以下を確認してください。

状態機械の場合
  • 型安全性のために、マシンで setup() API を使用している
  • イベント型が列挙型を使って定義されている
  • 型に対応したヘルパーを使って、アクションが setup() の外部で定義されている
  • 関数形式を使って、input からコンテキストが初期化されている
  • ガードが副作用のない純粋関数である
  • React コンポーネントで useMachine() または useActorRef() を使用している
@xstate/store の場合
  • トランジションの戻り値では、常に ...context を展開している
  • 型安全なイベント送信に trigger API を使用している
  • サブスクリプションを最適化するためにセレクターを作成している
  • React コンポーネント内でサブスクリプションをクリーンアップしている
TanStack Query との統合の場合
  • QueryObserver のサブスクリプションを fromCallback でラップしている
  • ミューテーションを fromPromise でラップし、.provide() 経由で提供している
  • クエリーキャッシュの更新をマシンのアクションではなく、ミューテーションのコールバック内で行っている
  • クリーンアップ関数をコールバックアクターから返している
  • QueryClient をマシンの input/コンテキスト経由で渡している
常に実行する項目
  • 型チェックを実行する(pnpm run typecheck)
  • テストを実行する(pnpm run test)

詳細なリファレンスドキュメント

パターン、例、詳細なガイダンスについては、以下のリファレンスファイルを参照してください。

  • references/machine-patterns.md - 階層状態、並列状態、アクター、テスト、v4 からの移行など、XState マシンのパターンを網羅しています
  • references/store-patterns.md - セレクター、アトム、エフェクト、元に戻す/やり直す、マシンへの移行タイミングなど、@xstate/store のパターンを詳しく解説しています
  • references/query-patterns.md - QueryObserver、ミューテーション、マシン全体の例を含む、TanStack Query 統合パターンを網羅しています

コメント