アクションとサービス
次は、ステートマシンにおけるアクションとサービスについて解説します。アクションは、ステート遷移時に実行される副作用を持つ処理です。サービスは、非同期処理や長時間実行されるタスクを扱うための機能です。
アクション
アクションは、状態遷移時に副作用として実行される処理です。アクションは、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で定義できるステートマシンの、基本的な構成要素を解説しました。本記事を読んだ後で、前回の記事をあらためて読んでいただけると、実用する際のイメージがつけやすくなるはずです。
