react-state-machines について
React 開発者向けに、XState v5 で UI 状態を設計する方法を案内するスキルです。状態機械、React 連携、よくある問題への対処を扱います。
主な機能
- 01状態機械の概念とパターン
- 02XState v5 setup と TypeScript の案内
- 03React フックの選択と連携
- 04Actor の合成と非同期フロー
- 05テスト、永続化、パフォーマンスの案内
活用シーン
- 01依存するブール値を明示的な状態に置き換える
- 02非同期 UI フローとリトライを調整する
- 03useMachine と Actor セレクターを選ぶ
- 04既存の useState または useReducer のロジックを移行する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill react-state-machinesインストール後、Claude Code で/react-state-machinesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.jsonreferences/composition-patterns.mddecision-trees.mderror-handling.mdmigration-guide.mdperformance.mdpersistence-hydration.mdreact-integration.mdreal-world-patterns.mdskills-architecture.mdtesting-patterns.mdxstate-v5-patterns.md詳細
XState v5 を使った React の状態機械
概要
状態機械では、UI の振る舞いを明示的な状態、遷移、イベントとしてモデル化することで、あり得ない状態を表現できないようにします。XState v5(npm の週間ダウンロード数 250 万以上)は、状態機械とアクターモデルを統合します。各機械は独自のライフサイクルを持つ独立したエンティティであり、高度な合成パターンを実現できます。
このスキルを使用する場面
該当するパターン:
- ブール値フラグの増加:複数の
isLoading、isError、isSuccessフラグを使っている - 暗黙的な状態:モードを判定するために
if (isLoading && !isError && data)を記述している - 防御的なコーディング:無効な遷移を防ぐため、状態更新の前にガードを設けている
- タイミングの調整:状態間でタイムアウト、遅延、デバウンスを扱っている
- 状態の依存関係:ある状態が正しく更新されるために、別の状態に依存している
次の場合は使用しないでください:
- 非同期処理を伴わない単純なブール値の切り替え(useState のほうが簡単です)
- 基本的な検証を行う単一のフォームフィールド(useReducer で十分です)
- サーバー状態のキャッシュ(React Query/TanStack Query が適しています)
- 静的データの変換(useMemo のほうが適しています)
- 単純なカウンターや切り替え(useState のほうが明快です)
包括的な判断ガイドについては decision-trees.md を参照してください
基本的な考え方
有限状態は振る舞いのモードを表します:idle、loading、success、error。コンポーネントが同時に存在できる状態は 1 つだけです。
**コンテキスト(拡張状態)**は、個別の状態を定義しない量的なデータを格納します。有限状態は「再生中」を示し、コンテキストは何を、どの音量で再生しているかを示します。
イベントは状態間の遷移を引き起こします。イベントはオブジェクトです:{ type: 'SUBMIT', data: formData }。
ガードは遷移を条件付きで許可またはブロックします:{ guard: 'hasValidInput' }。
アクションは、遷移中や状態への進入・退出時に実行される、結果を待たない副作用です。
起動されたアクターは、ライフサイクル管理とクリーンアップを伴う長時間実行プロセス(API 呼び出し、サブスクリプションなど)です。
クイックスタート:XState v5 の setup() パターン
import { setup, assign, fromPromise } from 'xstate';
const fetchMachine = setup({
types: {
context: {} as { data: User | null; error: string | null },
events: {} as
| { type: 'FETCH'; userId: string }
| { type: 'RETRY' }
},
actors: {
fetchUser: fromPromise(async ({ input, signal }) => {
const res = await fetch(`/api/users/${input.userId}`, { signal });
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
},
actions: {
setData: assign({ data: ({ event }) => event.output }),
setError: assign({ error: ({ event }) => event.error.message })
}
}).createMachine({
id: 'fetch',
initial: 'idle',
context: { data: null, error: null },
states: {
idle: { on: { FETCH: 'loading' } },
loading: {
invoke: {
src: 'fetchUser',
input: ({ event }) => ({ userId: event.userId }),
onDone: { target: 'success', actions: 'setData' },
onError: { target: 'failure', actions: 'setError' }
}
},
success: { on: { FETCH: 'loading' } },
failure: { on: { RETRY: 'loading' } }
}
});
React 統合の判断ツリー
| 使用例 | フック | 理由 |
|---|---|---|
| 単純なコンポーネント状態 | useMachine | シンプルで、すべての変更時に再レンダリングされます |
| パフォーマンスが重要 | useActorRef + useSelector | 必要な場合のみ再レンダリングされます |
| グローバル/共有状態 | createActorContext | React Context と統合できます |
基本パターン:
import { useMachine } from '@xstate/react';
function Toggle() {
const [snapshot, send] = useMachine(toggleMachine);
return (
<button onClick={() => send({ type: 'TOGGLE' })}>
{snapshot.matches('inactive') ? 'Off' : 'On'}
</button>
);
}
パフォーマンス重視のパターン:
import { useActorRef, useSelector } from '@xstate/react';
const selectCount = (s) => s.context.count;
const selectLoading = (s) => s.matches('loading');
function Counter() {
const actorRef = useActorRef(counterMachine);
const count = useSelector(actorRef, selectCount);
const loading = useSelector(actorRef, selectLoading);
// Only re-renders when count or loading changes
}
避けるべきアンチパターン
❌ 状態の爆発的な増加:直交する関心事にフラットな状態を使うこと。代わりに並列状態を使用してください。
❌ アクションからイベントを送信する:assign 内で send() を呼び出さないでください。内部イベントには raise を使用してください。
❌ 副作用のあるガード:ガードは純粋でなければなりません。副作用や外部の変更を行わないでください。
❌ 状態全体をサブスクライブする:useSelector で対象を絞ったセレクターを使用してください。
❌ モデルをメモ化しない:
// WRONG
const model = Model.fromJson(layout); // New model every render
// CORRECT
const modelRef = useRef(Model.fromJson(layout));
リファレンスの案内
基本パターン
- xstate-v5-patterns.md:v5 API、ステートチャート(階層/並列/履歴)、Promise アクターの完全な解説
- react-integration.md:useMachine と useActorRef の比較、Context パターン、副作用の処理
- testing-patterns.md:ユニットテスト、アクターのモック、可視化によるデバッグ
意思決定とベストプラクティス
- decision-trees.md:状態機械と useState/useReducer/React Query の使い分け、機械を分割する方法
- real-world-patterns.md:認証フロー、ファイルのアップロード、ウィザード、元に戻す/やり直し、ショッピングカートの完全な例
- error-handling.md:エラーバウンダリー、再試行戦略、サーキットブレーカー、グレースフルデグラデーション
- performance.md:セレクターのメモ化、React.memo との統合、パフォーマンス向上のための機械の分割
高度なトピック
- persistence-hydration.md:localStorage への永続化、SSR/Next.js のハイドレーション、スナップショットのシリアライズ
- migration-guide.md:useState/useReducer からの段階的な移行と、移行前後の例
- composition-patterns.md:アクター間の通信、機械の合成、高階機械、systemId
- skills-architecture.md:入力/出力のパラメーター化、ライブラリ構成
主な注意事項
- setup() は v5 の方法です:TypeScript の強力な型推論、アクターの登録、アクションの定義
- 非同期処理には Invoke、同期処理にはアクションを使います:アクションは実行後に結果を待たず、Invoke されたアクターにはライフサイクルがあります
- モードには有限状態、データにはコンテキストを使います:データのバリエーションごとに状態を作らないでください
- まず可視化します:Stately Studio (stately.ai/editor) を使うと、マシンが生きたドキュメントになります
要注意のサイン
- ブール型フラグが 3~4 個を超えている → ステートマシンが必要です
- モードの判定に
if (a && !b && c)を書いている → 状態を明示するべきです - 無効な状態の組み合わせが原因でバグが発生している → マシンによって不可能な状態を防げます
- 関係者に状態遷移を説明できない → 可視化によって解決できます
関連スキル
- react:React パターンの親スキル
- nextjs:サーバーとクライアントの状態の調整
- test-driven-development:UI に統合する前に createActor でマシンをテストします