問題:useStateとuseEffectが「スパゲッティ状態」になるとき
在庫チェック、割引コードの適用、Stripeの統合など、複雑な条件が絡む5ステップの決済フローを構築したことはありますか? useState を使用していると、すぐに「Boolean Soup(フラグ地獄)」、つまり isLoading、isError、isSuccess といった変数が複雑に絡み合った状態に陥ってしまいます。
フラグ変数のリセットを一つ忘れるだけで、アプリケーションは即座にエラー状態に陥ります。非公式な統計によると、インターフェースのバグの70%以上は、システムが予期しない状態(impossible states)に陥ることに起因しています。
XStateは、有限状態マシン(FSM: Finite State Machine)モデルによってこの問題を根本的に解決します。簡単に言えば、ある時点において、アプリケーションは必ず唯一の特定の状態にしか存在できないということです。「決済中」でありながら「カートが未入力」という状態はあり得ないのです。
クイックスタート:5分で最初のステートマシンを作成する
シンプルながらもプロフェッショナルに管理されたToggleボタンを作ってみましょう。まず、ライブラリをインストールします:
npm install xstate @xstate/react
boolean変数を使う代わりに、データの流れを明確に定義します:
import { createMachine } from 'xstate';
import { useMachine } from '@xstate/react';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
export const Toggle = () => {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send({ type: 'TOGGLE' })}>
{state.value === 'inactive' ? '有効化' : '実行中'}
</button>
);
};
このアプローチにより、ビジネスロジックがUIから完全に分離されます。コンポーネントは非常に軽量になり、イベントを送信してMachineから返された状態を表示するだけの役割になります。
なぜXStateがゲームチェンジャーなのか?
1. 状態の絶対的な制御
従来のコードでは、API呼び出しでレースコンディションが発生しがちです。XStateでは、「IDLE 状態からのみ LOADING に遷移できる」といった定義を明確に行います。データを受信している最中にユーザーが再度Fetchボタンを押しても、Machineはそれを自動的に無視するか、定義済みのシナリオに従って処理します。予期せぬ挙動や制御不能なサイドエフェクトはもう発生しません。
2. アクターモデル:分割して統治
XState v5は、アクターモデル(Actor Model)の概念をさらに進化させました。各Machineを特定の専門家と考えてください。あるアクターは決済を担当し、別のアクターは通知を担当します。これらはメッセージを通じて通信します。この手法により、巨大なロジックファイルを、管理しやすく独立してテスト可能な小さな断片に分割できます。
実践的なデータ取得フローのモデリング
以下は、エラー処理とデータ割り当てを含む、標準的なAPI呼び出しタスクの処理方法です:
import { createMachine, assign } from 'xstate';
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: { data: null, error: null },
states: {
idle: { on: { FETCH: 'loading' } },
loading: {
invoke: {
src: 'fetchData',
onDone: {
target: 'success',
actions: assign({ data: ({ event }) => event.output })
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error })
}
}
},
success: { on: { FETCH: 'loading' } },
failure: { on: { RETRY: 'loading' } }
}
});
「エラー時のリトライ」や「ローディングの上書き」といったあらゆるシナリオが集中管理されます。Reactコンポーネントは state.matches('loading') を使ってスピナーを表示するだけです。非常に明快です!
コードをより「クリーン」にするための実践的なヒント
多くのプロジェクトでXStateを導入してきた経験から、ワークフローを最適化するための3つの秘訣を紹介します:
Stately Visualizerを活用する
コードを打ち込むだけではなく、ロジックを Stately Viz に貼り付けてみてください。このツールは実際の実行フローをチャートとして描画してくれます。もしチャートがクモの巣のように複雑に見えるなら、それはアクターを分割すべきサインです。
Contextを物置にしない
よくある間違いは、すべての変数を context に詰め込んでしまうことです。states はフローの制御のため、context はデータの保持のため、という役割分担を忘れないでください。もし context 内で if 文を多用している自分に気づいたら、それらをサブ状態(sub-states)に移行することを検討しましょう。
JSONの効率的なデバッグ
Machine内で複雑に入れ子になったオブジェクトを扱う際、コンソールのログを確認するのは大変です。私はよくデータを ToolcraftのJSON Formatter などのツールにコピーして整形しています。context の構造を明確に把握することで、ロジックのミスを数倍早く発見できるようになります。
応用:TypeScriptによる型安全性
XState v5はTypeScriptを強力にサポートしています。イベントの誤送信を防ぐために、ぜひ活用しましょう:
const machine = createMachine({
setup: {
types: {} as {
context: { items: string[] };
events: { type: 'ADD_ITEM'; item: string } | { type: 'CLEAR' };
},
},
// ... オートコンプリートにより、より安全なロジックが書けます
});
XStateを導入すべき時(とそうでない時)
XStateはすべてのプロジェクトに効く魔法の杖ではありません。単純な入力フォームや静的なブログサイトには使わないでください。しかし、以下のような場合はすぐに導入を検討すべきです:
- アプリケーションに複雑なビジネスフローがあり、多くのステップが互いに依存している場合。
- あまりに多くの
isSomethingというboolean変数の管理に疲れ果てている場合。 - 開発者、デザイナー、BA(ビジネスアナリスト)の間で共有できる共通の図面(ステートチャート)が必要な場合。
- 重いUIをレンダリングすることなく、ロジックのユニットテストを書きたい場合。
この記事が、皆さんが「手強い」ロジックに立ち向かう自信に繋がれば幸いです。バグのないクリーンなコードを目指しましょう!

