XState の使い方 — アクターモデルベースの状態管理ライブラリ
一言でいうと
XState は、有限状態マシン(FSM)とステートチャートをベースにした JavaScript/TypeScript 向けの状態管理・オーケストレーションライブラリです。ゼロ依存で、複雑なアプリケーションロジックをイベント駆動・アクターモデルで予測可能かつ視覚的に管理できます。
どんな時に使う?
- 複雑なUIフロー管理 — マルチステップフォーム、ウィザード、モーダルの開閉状態など、状態遷移が複雑になりがちなUIを宣言的に定義したい場合
- 非同期処理のオーケストレーション — API呼び出しのローディング→成功→エラー→リトライといった非同期フローを、状態マシンで安全に管理したい場合
- ビジネスロジックの可視化・共有 — 決済フロー、承認ワークフローなど、ビジネスルールをステートチャートとして可視化し、チーム間で認識を揃えたい場合
インストール
# npm
npm install xstate
# yarn
yarn add xstate
# pnpm
pnpm add xstate
Note: この記事は XState v5(v5.30.0)を対象としています。v4 とは API が大きく異なるため、バージョンにご注意ください。
React / Vue / Svelte などのフレームワーク連携が必要な場合は、対応パッケージも追加します。
# React の場合
npm install @xstate/react
# Vue の場合
npm install @xstate/vue
# Svelte の場合
npm install @xstate/svelte
基本的な使い方
最もよく使うパターンは「状態マシンを定義 → アクターを生成 → イベントを送信」の3ステップです。
import { createMachine, createActor, assign } from 'xstate';
// 1. 状態マシンを定義
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
context: {
count: 0,
},
states: {
inactive: {
on: {
TOGGLE: { target: 'active' },
},
},
active: {
entry: assign({ count: ({ context }) => context.count + 1 }),
on: {
TOGGLE: { target: 'inactive' },
},
},
},
});
// 2. アクター(マシンの実行インスタンス)を生成
const toggleActor = createActor(toggleMachine);
// 3. 状態変化を購読
toggleActor.subscribe((state) => {
console.log(state.value, state.context);
});
// 4. アクターを開始
toggleActor.start();
// => 'inactive', { count: 0 }
// 5. イベントを送信して状態遷移
toggleActor.send({ type: 'TOGGLE' });
// => 'active', { count: 1 }
toggleActor.send({ type: 'TOGGLE' });
// => 'inactive', { count: 1 }
よく使うAPI
1. createMachine — 状態マシンの定義
状態マシンの設計図を作成します。XState の中核となる API です。
import { createMachine } from 'xstate';
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: {
data: null as string | null,
error: null as string | null,
},
states: {
idle: {
on: { FETCH: 'loading' },
},
loading: {
invoke: {
id: 'fetchData',
src: 'fetchData',
onDone: {
target: 'success',
actions: assign({ data: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => String(event.error) }),
},
},
},
success: {
type: 'final',
},
failure: {
on: { RETRY: 'loading' },
},
},
});
2. createActor — アクターの生成と制御
定義したマシンから実行可能なアクター(インスタンス)を生成します。start() で開始、send() でイベント送信、subscribe() で状態変化を監視します。
import { createActor } from 'xstate';
const actor = createActor(fetchMachine, {
// アクター生成時にロジック実装を注入
input: { url: 'https://api.example.com/data' },
});
// スナップショット(現在の状態)を取得
actor.start();
const snapshot = actor.getSnapshot();
console.log(snapshot.value); // 'idle'
console.log(snapshot.context); // { data: null, error: null }
// 状態変化を購読
const subscription = actor.subscribe((state) => {
console.log('Current state:', state.value);
});
// イベント送信
actor.send({ type: 'FETCH' });
// 購読解除
subscription.unsubscribe();
// アクター停止
actor.stop();
3. assign — コンテキスト(データ)の更新
状態遷移時にコンテキスト(マシンが保持するデータ)を更新するアクションです。
import { createMachine, assign } from 'xstate';
const counterMachine = createMachine({
id: 'counter',
initial: 'active',
context: {
count: 0,
lastUpdated: null as Date | null,
},
states: {
active: {
on: {
INCREMENT: {
actions: assign({
count: ({ context }) => context.count + 1,
lastUpdated: () => new Date(),
}),
},
DECREMENT: {
actions: assign({
count: ({ context }) => Math.max(0, context.count - 1),
}),
},
// イベントペイロードを使う場合
SET: {
actions: assign({
count: ({ event }) => event.value,
}),
},
},
},
},
});
const actor = createActor(counterMachine);
actor.start();
actor.send({ type: 'INCREMENT' });
actor.send({ type: 'SET', value: 42 });
4. fromPromise / fromCallback — 外部ロジックのアクター化
非同期処理やコールバックベースの処理をアクターとして扱えます。invoke と組み合わせて使います。
import { createMachine, createActor, assign, fromPromise } from 'xstate';
// Promise ベースのアクターロジック
const fetchUser = fromPromise(async ({ input }: { input: { userId: string } }) => {
const response = await fetch(`https://api.example.com/users/${input.userId}`);
if (!response.ok) throw new Error('Fetch failed');
return response.json() as Promise<{ name: string; email: string }>;
});
const userMachine = createMachine({
id: 'user',
initial: 'idle',
context: {
userId: '123',
user: null as { name: string; email: string } | null,
error: null as string | null,
},
states: {
idle: {
on: { FETCH: 'loading' },
},
loading: {
invoke: {
src: fetchUser,
input: ({ context }) => ({ userId: context.userId }),
onDone: {
target: 'success',
actions: assign({ user: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => String(event.error) }),
},
},
},
success: {},
failure: {
on: { RETRY: 'loading' },
},
},
});
5. ガード(条件付き遷移)
guard を使って、条件を満たす場合のみ状態遷移を許可できます。
import { createMachine, assign } from 'xstate';
const paymentMachine = createMachine({
id: 'payment',
initial: 'idle',
context: {
amount: 0,
balance: 1000,
},
states: {
idle: {
on: {
SET_AMOUNT: {
actions: assign({ amount: ({ event }) => event.amount }),
},
PAY: [
{
// 残高が足りる場合のみ processing へ遷移
guard: ({ context }) => context.amount > 0 && context.amount <= context.balance,
target: 'processing',
},
{
// 条件を満たさない場合は error へ
target: 'error',
},
],
},
},
processing: {
// ...
},
error: {
on: { RETRY: 'idle' },
},
},
});
React での使い方
@xstate/react を使うと、React コンポーネントからシームレスに利用できます。
import { useMachine } from '@xstate/react';
import { toggleMachine } from './toggleMachine';
function ToggleButton() {
const [state, send] = useMachine(toggleMachine);
return (
<div>
<p>State: {String(state.value)}</p>
<p>Count: {state.context.count}</p>
<button onClick={() => send({ type: 'TOGGLE' })}>
{state.matches('inactive') ? 'Activate' : 'Deactivate'}
</button>
</div>
);
}
類似パッケージとの比較
| 特徴 | XState | Zustand | Redux Toolkit | robot3 |
|---|---|---|---|---|
| 状態マシン / ステートチャート | ✅ 本格対応 | ❌ | ❌ | ✅ 軽量対応 |
| アクターモデル | ✅ | ❌ | ❌ | ❌ |
| 視覚化ツール | ✅ Stately Studio | ❌ | ❌ | ❌ |
| TypeScript 型推論 | ✅ 強力 | ✅ | ✅ | △ |
| バンドルサイズ(minified) | ~40KB | ~1KB | ~10KB | ~2KB |
| 学習コスト | 高い | 低い | 中程度 | 中程度 |
| フレームワーク非依存 | ✅ | ✅ | ✅ | ✅ |
| 主な用途 | 複雑なフロー制御 | シンプルな状態管理 | グローバル状態管理 | 軽量な状態マシン |
選定の目安:
- 単純なグローバルステート管理 → Zustand / Redux Toolkit
- 複雑な状態遷移・ワークフロー・ビジネスロジック → XState
- 軽量な状態マシンだけ欲しい → robot3
注意点・Tips
1. v4 → v5 の破壊的変更に注意
XState v5 は v4 から大幅に API が変更されています。Machine() → createMachine()、interpret() → createActor() など、移行ガイドを必ず確認してください。
// ❌ v4 スタイル(v5 では動かない)
import { Machine, interpret } from 'xstate';
// ✅ v5 スタイル
import { createMachine, createActor } from 'xstate';
2. context の更新は必ず assign を使う
コンテキストを直接変更(ミューテーション)してはいけません。必ず assign アクションを通じて更新します。
// ❌ NG: 直接変更
actions: ({ context }) => { context.count++ }
// ✅ OK: assign を使う
actions: assign({ count: ({ context }) => context.count + 1 })
3. 状態の判定には state.matches() を使う
ネストされた状態(階層的ステートチャート)でも安全に判定できます。
const snapshot = actor.getSnapshot();
// ✅ ネストされた状態にも対応
if (snapshot.matches({ loading: 'fetching' })) {
// ...
}
// 文字列比較は単純な状態のみ
if (snapshot.matches('idle')) {
// ...
}
4. バンドルサイズを意識する
XState はフル機能で約40KBあります。シンプルなトグルやフラグ管理だけなら過剰です。状態遷移が3つ以上あり、遷移条件が複雑になるケースで真価を発揮します。
5. Stately Studio を活用する
state.new でビジュアルエディタを使うと、ステートチャートの設計・共有・コード生成が格段に楽になります。非エンジニアとのコミュニケーションにも有効です。
まとめ
XState は、複雑な状態遷移やビジネスロジックを有限状態マシン・ステートチャート・アクターモデルで宣言的に管理できるライブラリです。学習コストはやや高いものの、一度理解すれば「あり得ない状態」を設計段階で排除でき、バグの少ない堅牢なアプリケーションを構築できます。特に、マルチステップのフォームや非同期ワークフローなど、状態遷移が複雑になるシーンでは、導入を強くおすす