アクションとサービス
次は、ステートマシンにおけるアクションとサービスについて解説します。アクションは、ステート遷移時に実行される副作用を持つ処理です。サービスは、非同期処理や長時間実行されるタスクを扱うための機能です。
アクション
アクションは、状態遷移時に副作用として実行される処理です。アクションは、actionsプロパティを使用して定義されます(リスト8)。
import { createMachine, assign } from "xstate"; export const machine = createMachine( { id: "vending-machine", description: "自動販売機", initial: "idle", context: { // 投入した金額 amount: 0, }, states: { idle: { description: "初期状態", on: { INSERT_COIN: { description: "お金を投入する", target: "inserting", // (1) コインの追加を処理するアクション actions: assign((context, event) => { // (2) // eventは { amount: 100 } のような投入金額についてのオブジェクト return { amount: context.amount + event.amount, // (3) }; }), }, }, }, // その他のステートを定義 }, } );
(1)で、INSERT_COIN
イベントによる inserting
への状態遷移が発生した場合に発動する処理を actions
に記述しています。actions
に関数を登録すると、現在の context
の値と、UIから send
メソッドなどで送られた event
オブジェクトが引数として受け取れます。
(2)で使用している assign
関数は context
を更新する際に使用する高階関数です。 assign
の引数に渡した関数で(3)のように更新したいプロパティ(今回の例では amount
)と新しい値をオブジェクトに記載して戻り値として返すと、context
内で該当のプロパティが更新されます。
リスト8では、INSERT_COIN
イベントが発生したときに、inserting
ステートに遷移するとともに、amount
コンテキストの値を更新するアクションが実行されます。
サービス
サービスは、非同期処理や長時間実行されるタスクを扱うための機能です。サービスは、各ステートの定義内の invoke
プロパティを使用して定義されます(リスト9)。
import { createMachine } from 'xstate'; // (3) const fetchItemDetails = async (context, event) => { // API呼び出しや非同期処理を実行 }; const vendingMachine = createMachine({ id: 'vendingMachine', initial: 'idle', states: { idle: { on: { SELECT_ITEM: 'fetchingItemDetails', }, }, fetchingItemDetails: { // (1) invoke: { src: fetchItemDetails, // (2) サービスとして実行される関数 onDone: { // (4) target: 'itemDetailsFetched', actions: /* ... */, }, onError: { // (5) target: 'error', actions: /* ... */, }, }, }, // その他のステートを定義 }, });
この例では、(1)の fetchingItemDetails
ステートに遷移したときに、 (2)で指定した fetchItemDetails
関数がサービスとして実行されます。(3)の fetchItemDetails
関数で実装した非同期処理が成功すると、(4)の onDone
プロパティに定義されたステートへ遷移し、エラーが発生した場合には(5)の onError
プロパティに定義されたステートへ遷移します。(2)にはPromiseを登録できるので、(3)のようにasync関数でサービスを定義すると、通信処理などは実装しやすいかもしれません。
アクションとサービスの違い
アクションとサービスは少し似ていますが、同期か非同期か、処理結果によってステートが変わるかどうかなど、細かい点で違いがあります。違いを図で確認してみましょう。アクションを実装したリスト8を図示すると図1のようになります。
idle
ステートから INSERT_COIN
イベントが発行されると、inserting
ステートに遷移する前に、INSERT_COIN
イベントに登録された actions
が実行され、context.amountの数値が加算されます。重要なのは、処理結果によって遷移先のステートが変化することはない、ということです。加算した結果が200円でも500円でも、必ず inserting
ステートに遷移します。また、actions
で実行できるのは同期的な処理のみです。
一方、サービスを実装したリスト9を図示すると、図2のようになります。
idle
ステートから SELECT_ITEM
イベントが発行されると、fetchingItemDetails
ステートに遷移します。fetchingItemDetails
ステートには invoke
プロパティが定義されているので、遷移してきた直後に fetchItemDetails()
が実行されます。その結果を受けて、成功していれば、onDone
に登録した itemDetailsFetched
ステートに遷移しますし、逆に失敗していれば、onError
に登録した error
ステートに遷移します。ここで重要なのは、処理結果によって遷移先のステートが決定される、ということです。処理内容は同期処理でも非同期処理(Promise)でも構いません。
アクションとサービスは、似ているようで違いがあります。
-
イベントに副作用を持たせて
context
などを更新したりconsole.log
でイベントのパラメータを確認したい場合などにはアクションを使う - 通信の結果などに応じて次のステートを決めたい場合などにはサービスを使う
というように、使い分けるとよいでしょう。
まとめ
XStateで定義できるステートマシンの、基本的な構成要素を解説しました。本記事を読んだ後で、前回の記事をあらためて読んでいただけると、実用する際のイメージがつけやすくなるはずです。