xstate の使い方

Finite State Machines and Statecharts for the Modern Web.

v5.30.04.0M/週MIT状態管理
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

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>
  );
}

類似パッケージとの比較

特徴XStateZustandRedux Toolkitrobot3
状態マシン / ステートチャート✅ 本格対応✅ 軽量対応
アクターモデル
視覚化ツール✅ 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 は、複雑な状態遷移やビジネスロジックを有限状態マシン・ステートチャート・アクターモデルで宣言的に管理できるライブラリです。学習コストはやや高いものの、一度理解すれば「あり得ない状態」を設計段階で排除でき、バグの少ない堅牢なアプリケーションを構築できます。特に、マルチステップのフォームや非同期ワークフローなど、状態遷移が複雑になるシーンでは、導入を強くおすす