Boolean Soup(boolean変数の乱立)が抱える問題
React開発において、コンポーネントのロジックを管理する際、バラバラのuseStateフックに頼ってしまうことがよくあります。以下のようなコードを書いたり、目にしたりしたことがあるはずです:
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [data, setData] = useState(null);
const [isSuccess, setIsSuccess] = useState(false);
一見すると管理しやすそうに見えます。しかし、これではisLoadingとisSuccessが技術的に同時にtrueになり得るといった「ありえない状態(impossible states)」を生む隙を作ってしまいます。こうした論理的な矛盾こそが、最も厄介なUIバグの温床となります。最近のプロジェクトでレガシーなチェックアウトフローを移行した際、booleanからステートマシンに切り替えたことで、状態関連のバグ報告が約60%減少しました。
ステートマシンを採用することで、どの状態が有効であるかを厳密に定義できます。アプリケーションがある地点から次の地点へどのように遷移するかを正確に規定し、偶発的な重複の余地を排除できるのです。
クイックスタート:最初のマシンを作成する
機能的なトグルスイッチを作ってみましょう。まず、必要なパッケージをインストールします:
npm install xstate @xstate/react
手動でbooleanを管理する代わりに、マシンを定義します。これはコンポーネントの振る舞いの設計図のようなもので、状態(states)とイベント(events)で構成されます。
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>
);
};
このマシンは、inactiveまたはactiveのいずれか1つの状態のみに存在します。TOGGLEイベントがトリガーされると、もう一方の状態へ遷移します。両方の状態に同時に存在したり、どちらでもない状態になったりすることは物理的に不可能であり、UIに揺るぎない基盤を提供します。
なぜステートマシンがゲームチェンジャーなのか
ステートマシンは単なる凝ったオブジェクトではありません。ロジックの数学的モデルです。UI開発において、コンポーネントが取り得るすべての「モード」をマッピングします。XStateは、ステートチャート(Statecharts)を実装することでこれをさらに進化させています。これにより、階層化された状態、並行ロジック、履歴管理といった高度なパターンが可能になります。
「ありえない状態」の排除
ビデオアップローダーを例に考えてみましょう。通常、ロジックにはidle、selecting、uploading、success、errorが含まれます。booleanに依存していると、アップロードが既に80%完了しているのに、ユーザーが「ファイル選択」ダイアログをトリガーできてしまうかもしれません。XStateでは、uploading状態のときにSELECT_FILE遷移を定義しないだけで、これを防ぐことができます。UIはロジックを直接的かつ予測通りに反映するものになります。
コーディング前にロジックを可視化する
XStateの真の力は可視化にあります。作成したマシンをStately Visualizerに貼り付けると、ライブフローチャートが生成されます。これにより、エンジニアリングとプロダクトデザインの間の溝が埋まります。ステークホルダーに200行のコードを説明する代わりに、ビジネスロジックの視覚的な図を見せることができるのです。
高度な活用法:データとサイドエフェクト
現実のアプリでは、ボタンの切り替え以上のことが必要です。データの取得もその一つです。XStateは、Context(内部ストレージ)とActors(非同期ロジック)を介してこれを管理します。
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: { data: null, error: null },
states: {
idle: {
on: { FETCH: 'loading' }
},
loading: {
invoke: {
src: 'getUser',
onDone: {
target: 'success',
actions: assign({ data: ({ event }) => event.output })
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error })
}
}
},
success: {},
failure: {
on: { RETRY: 'loading' }
}
}
});
invokeを使用することで、XStateはPromiseのライフサイクルを管理できます。結果に基づいて、successまたはfailureへの遷移を自動的に処理します。assign関数はマシンの内部メモリとして機能するcontextを更新します。
ガード(Guards)とアクション(Actions)
- ガード: 条件付きのゲートとして機能します。例えば、
formIsValidのチェックをパスしない限り「送信」遷移を阻止するといったことが可能です。 - アクション: 「投げっぱなし」のサイドエフェクトに使用します。特定の状態に入ったときにトースト通知を表示したり、アナリティクスのログを記録したりするのに最適です。
実践的な導入のヒント
「状態ファースト」の考え方にシフトするには時間がかかります。XStateを本番のReactアプリに効果的に統合する方法をいくつか紹介します:
複雑度の高い箇所から始める
アプリケーション全体を一つの巨大なマシンで包むのは避けましょう。多ステップのチェックアウトフォーム、複雑な認証フロー、データ量の多いダッシュボードなど、状態管理が破綻しやすいコンポーネントに焦点を当ててください。
インスペクターを活用する
開発中は@xstate/inspect

