ReactとVueでXStateを使い複雑なコンポーネントの状態をマスターする
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに:UI状態の獣を飼いならす
現代のWebアプリケーションはますます複雑になっており、この複雑さの多くは、個々のUIコンポーネントの状態管理にあります。コンポーネントが機能やインタラクションを増やすにつれて、その内部状態はすぐに、ブール値、列挙型、条件付きロジックの絡み合ったウェブになる可能性があります。このスパゲッティコードは、デバッグを困難にし、微妙なバグを導入し、コラボレーションを悪夢にします。予期せぬ副作用、不可能な状態、そしてコンポーネントがどのように動作するかについての全体的な明確さの欠如と戦っていることがよくあります。ここで、ステートマシン、特にXStateのようなライブラリが、強力でエレガントなソリューションを提供します。XStateは、コンポーネントの動作をモデル化するための公式で予測可能な方法を提供することにより、状態管理を危険な旅から明確に定義されたパスに変え、より堅牢で理解しやすく保守性の高いReactおよびVueアプリケーションを構築できるようにします。
コアコンセプト:ステートマシンの言語を理解する
実際に応用する前に、これらのXStateの基礎を形成するため、ステートマシンの背後にある基本的な概念を把握することが重要です。
ステート
ステートは、コンポーネントのライフサイクルにおける個別の瞬間または条件を表します。たとえば、ボタンはidle
、loading
、またはsuccess
のステートになる可能性があります。重要なことに、ステートマシンは、いつでも1つのステートしか存在できません。この排他性が、不可能な組み合わせを防ぐ鍵となります。
イベント
イベントは、あるステートから別のステートへの遷移を引き起こすトリガーです。イベントは通常、ユーザーインタラクション(例:CLICK
、SUBMIT
)、データ取得結果(例:FETCH_SUCCESS
、FETCH_ERROR
)、またはシステム生成信号(例:TIMER_EXPIRED
)です。
トランジション
トランジションは、特定のイベントによってトリガーされる、あるステートから別のステートへの移動です。トランジションは、マシンが特定のステートにあるときに特定のイベントが発生した場合に何が起こるかを定義します。たとえば、idle
ステートにあるとき、CLICK
イベントはloading
ステートへの遷移を引き起こす可能性があります。
アクション
アクションは、トランジション中またはステートの開始/終了時に発生する副作用です。ここで、API呼び出し、ローカルストレージの更新、またはReduxアクションのディスパッチなどの操作を実行します。アクションはトランジションとは異なります。トランジションはどこへ行くかを定義し、アクションは、その旅の間に何をするかを定義します。
コンテキスト
コンテキスト(拡張ステートとも呼ばれます)は、時間とともに変化するが、システムの基本的な「ステート」を定義しない、変更可能なデータを格納する場所です。たとえば、フォームでは、currentInput
の値やerrorMessage
は通常コンテキストに配置されますが、フォームのediting
またはsubmitting
ステートは、個別のステートになります。
XState:原則、実装、および実際的な例
XStateは、ステートマシンおよびステートチャートを定義、解釈、および実行できるライブラリです。フロントエンド開発に、正式なシステムモデリングの堅牢性と予測可能性をもたらします。
フォーマルモデリングの力
XStateのコア原則は、コンポーネントの動作のフォーマルモデリングです。すべての可能なステート、遷移をトリガーするイベント、および発生するアクションを明示的に定義することにより、曖昧さを排除します。この宣言的なアプローチにより、コンポーネントのロジックは本質的にテスト可能で理解しやすくなります。
ステートマシンの定義
簡単なシナリオを考えてみましょう。データを取得するボタンです。idle
、loading
、success
、またはerror
のいずれかになります。
// ReactまたはVueコンポーネントファイル内 import { createMachine, assign } from 'xstate'; const fetchMachine = createMachine({ id: 'fetch', initial: 'idle', context: { data: null, error: null, }, states: { idle: { on: { FETCH: 'loading', }, }, loading: { invoke: { id: 'fetchData', src: async (context, event) => { // API呼び出しをシミュレート await new Promise(resolve => setTimeout(resolve, 1000)); if (Math.random() > 0.5) { return { data: 'Some fetched data!' }; } else { throw new Error('Failed to fetch data.'); } }, onDone: { target: 'success', actions: assign({ data: (context, event) => event.data.data, // event.dataには'src'プロミスからの結果が含まれます error: null, }), }, onError: { target: 'error', actions: assign({ error: (context, event) => event.data.message, // event.dataには'src'プロミスからのエラーが含まれます data: null, }), }, }, }, success: { on: { DISMISS: 'idle', }, }, error: { on: { RETRY: 'loading', DISMISS: 'idle', }, }, }, });
ここで定義するのは次のとおりです。
id
:マシンのユニークな識別子。initial
:開始ステート。context
:コンポーネントの初期データ(例:data
とerror
)。states
:すべての可能なステートを定義するオブジェクト。on
:そのステートのイベントによってトリガーされる遷移を定義します。invoke
:ステートのライフサイクルの一部として非同期操作(API呼び出しなど)を実行できます。onDone
とonError
は結果を処理します。actions
:assign
を使用してcontext
を変更する関数。
React統合例
@xstate/react
フックを使用して、Reactコンポーネントでこれを使用する方法を見てみましょう。
// MyFetcherButton.jsx (React) import React from 'react'; import { useMachine } from '@xstate/react'; import { createMachine, assign } from 'xstate'; // (上記のfetchMachine定義がここに入ります) function MyFetcherButton() { const [current, send] = useMachine(fetchMachine); return ( <div> <p>Status: {current.value}</p> {current.matches('idle') && ( <button onClick={() => send('FETCH')}>Fetch Data</button> )} {current.matches('loading') && <p>Loading...</p>} {current.matches('success') && ( <> <p>Data: {current.context.data}</p> <button onClick={() => send('DISMISS')}>Dismiss</button> </> )} {current.matches('error') && ( <> <p style={{ color: 'red' }}>Error: {current.context.error}</p> <button onClick={() => send('RETRY')}>Retry</button> <button onClick={() => send('DISMISS')}>Dismiss</button> </> )} </div> ); } export default MyFetcherButton;
useMachine
はcurrent
(現在のステートとコンテキスト)とsend
(イベントをディスパッチする関数)を返します。current.matches()
を使用して、アクティブなステートに基づいてUIを条件付きでレンダリングします。
Vue統合例
Vueの場合、@xstate/vue
パッケージを使用します。
<!-- MyFetcherButton.vue (Vue 3) --> <template> <div> <p>Status: {{ state.value }}</p> <button v-if="state.matches('idle')" @click="send('FETCH')">Fetch Data</button> <p v-if="state.matches('loading')">Loading...</p> <div v-if="state.matches('success')"> <p>Data: {{ state.context.data }}</p> <button @click="send('DISMISS')">Dismiss</button> </div> <div v-if="state.matches('error')"> <p style="color: red;">Error: {{ state.context.error }}</p> <button @click="send('RETRY')">Retry</button> <button @click="send('DISMISS')">Dismiss</button> </div> </div> </template> <script setup> import { useMachine } from '@xstate/vue'; import { createMachine, assign } from 'xstate'; // (上記のfetchMachine定義がここに入ります) const { state, send } = useMachine(fetchMachine); </script>
Reactと同様に、VueのuseMachine
はstate
(リアクティブな現在のステートとコンテキスト)とsend
を提供します。
高度なシナリオ:ステートチャートと階層型ステート
真に複雑なコンポーネントの場合、XStateは階層型および並列ステートを拡張するステートチャートのサポートで優れています。
VideoPlayer
コンポーネントを考えてみましょう。これはplaying
またはpaused
ですが、fetching
中にbuffering
したり、paused
中にseeking
したりすることもできます。
const videoPlayerMachine = createMachine({ id: 'videoPlayer', initial: 'idle', states: { idle: { on: { PLAY: 'playing' } }, playing: { initial: 'playingVideo', states: { playingVideo: { on: { PAUSE: 'paused', BUFFER: 'buffering' } }, buffering: { on: { BUFFER_COMPLETE: 'playingVideo', PAUSE: 'paused' } } }, on: { STOP: 'idle' } // 親で処理されるイベント }, paused: { on: { PLAY: 'playing', SEEK: 'seeking' } }, seeking: { // ... シークのステート on: { SEEK_COMPLETE: 'paused', PLAY: 'playing' // シーク後に再生を開始できます } } } });
ここでは、playing
はplayingVideo
、buffering
などのネストされた子ステートを持つ親ステートです。これにより、関連する動作をグループ化し、複雑さを管理できます。イベントは階層のどのレベルでも処理でき、バブリングメカニズムに従います。
アプリケーションシナリオ
XStateは、これらのシナリオで特に価値があります。
- 複雑な検証および送信フローを持つフォーム:
editing
、validating
、submitting
、submitted
、error
ステートの追跡。 - **ウィザードまたはマルチステッププロセス:**ステップ間のフロー、条件付きナビゲーションの管理。
- **メディアプレーヤーまたはインタラクティブUI要素:**複雑なインタラクションで
playing
、paused
、buffering
、seeking
、error
ステートを処理する。 - ドラッグアンドドロップインターフェイス:
idle
、dragging
、hovering
、dropping
ステートを追跡する。 - ステートロジックが多数の条件分岐(if/else、switchステートメント)および可能性のある不可能なステートにつながるコンポーネント。
結論:UIステートのパラダイムシフト
複雑なReactおよびVueコンポーネントの状態管理は、もはや絶え間ない頭痛の種である必要はありません。ステートマシンを採用し、XStateのような堅牢なライブラリを活用することにより、UIロジックに明確さ、予測可能性、および保守性をもたらすことができます。XStateは、コンポーネントの動作を明示的に定義し、不可能なステートを防ぎ、アプリケーションを推論、デバッグ、拡張しやすくするための強力なフレームワークを提供します。これは、開発者が散在する変数のコレクションではなく、決定論的なシステムとしてステートをモデル化できるパラダイムシフトであり、最終的にはより安定した、より楽しいユーザーエクスペリエンスにつながります。