TL;DR

  • Zenn の XState の本の第 2 章を Deno で動かし、XState の基本をまとめた。
  • setup で型や Actions、Guards、Actors を定義し、createMachine で状態と遷移を書き、createActor で動かす。JSR には XState がまだ公開されていないので、npm から入れた。

背景

XState の基本を学ぶために、Zenn の本の第 2 章のコードを Deno で動かしてみた。

環境

  • Deno
  • xstate 5.19.2(npm)

検証

事前準備

deno init deno-xstate-toggle && cd deno-xstate-toggle でプロジェクトを作った。JSR で xstate を検索すると @statelyai/xstate が見つかったが、「This package has not published any versions yet.」と表示されていて、追加しても見つからなかった。

$ deno add jsr:@statelyai/xstate
error: jsr:@statelyai/xstate was not found

仕方がないので npm から入れた。

$ deno add npm:xstate
Add npm:[email protected]

ステートマシンの作成

setup(v5 から)には、ステートマシンの設定を書く。Event、Context、Actions、Guards などの型を明示できるので、型エラーを実行前に見つけられる。Event には { type: 'PLAY'; value: number } のように値を持たせることもできる。

createMachine でステートマシンを作る。id は識別子、initial は初期状態、states は各状態で、on にはその状態で受け取る Event と遷移先を書く。

import { setup } from "xstate"
 
type ToggleEvent = { type: "toggle" }
 
export const toggleMachine = setup({
  types: {} as { events: ToggleEvent },
}).createMachine({
  id: "toggle",
  initial: "inactive",
  states: {
    inactive: { on: { toggle: "active" } },
    active: { on: { toggle: "inactive" } },
  },
})

ESLint のプラグイン(eslint-plugin-xstate)はあるが、メンテナンスされていないようだった。

Visual Studio Code に Stately の拡張機能を入れると、ステートマシンの定義箇所に「Open Visual Editor」が表示される。クリックすると XState Editor が開き、Edit モードでは GUI からステートマシンを編集でき、Simulate モードでは作ったステートマシンをシミュレートできる。

Actor

Actor はアクターモデルのアクターと同じで、実行中のステートマシンを表すインスタンスである。Event を送受信し、受け取った Event に応じて状態を変える、実行中のプロセスと考えればよい。

createActor にステートマシンを渡して作り、start と stop で開始・停止する。Event は send で送る。subscribe の next には状態が変わるたびに呼ぶ処理を、error にはエラー時の処理を書く。snapshot は現在の状態を表す。

const actor = createActor(toggleMachine)
actor.subscribe({
  next: (snapshot: SnapshotFrom<typeof actor>) => console.log(snapshot.value),
  error: (error) => console.error(error),
})
actor.start()
actor.send({ type: "toggle" })

Context

Context は、Actor にデータを持たせるときに使う。型は setup で定義し、初期値は createMachine の context で指定する。snapshot.context.count のように読める。Context はイミュータブルなので、Actions の中で assign を使って更新する。

Actions

Actions は、状態遷移のときに実行される同期処理である。非同期処理には後述の Invoke を使う。Actions は createMachine の中にも書けるが、ステートマシンの記述が簡潔になるので setup の actions に定義することが推奨されている。

const toggleMachine = setup({
  actions: {
    incrementCount: assign({ count: ({ context }) => context.count + 1 }),
  },
})

定義した Actions は、createMachine の中の次のキーに指定する。キーによって実行のタイミングが変わる。

  • entry: その状態に遷移したとき
  • exit: その状態から別の状態に遷移したとき
  • on: Event を受け取ったとき({ actions: 'incrementCount', target: 'active' } のように書く)

配列を渡せば複数の処理を呼べる。entry、exit、on をマシンの直下に書くと、どの状態にいても発火する。

createMachine({
  states: { stopped: {}, running: {} },
  on: {
    RESET: { actions: assign({ elapsed: 0 }), target: ".stopped" },
  },
})

Input

Input は、ステートマシンの振る舞いを変えるために Actor に渡すデータである。型は setup で定義し、createActor の第 2 引数で渡す。渡した Input は、context に関数を指定すると引数で受け取れる。Invoke でも、定義の仕方は違うが使える。

const actor = createActor(toggleMachine, { input: { initialCount: 10 } })
 
createMachine({
  context: ({ input }) => ({ count: input.initialCount ?? 0 }),
})

Guards

Guards は、ガード関数(述語関数)が true を返したときだけ状態遷移を許す。setup の guards に定義し、createMachine の guard で指定する。処理は guard、actions、target の順に実行され、ガード関数が false を返すと actions も target も実行されない。

createMachine({
  on: {
    feedback: [
      { guard: "isGood", target: "thanks" },
      { guard: "isBad", target: "askMore" },
      { target: "form" },
    ],
  },
})

Invoke

Invoke は、状態遷移のときに子 Actor を作って非同期処理を行う機能である。setup の actors に定義し、createMachine の invoke.src で指定する。invoke.input は Context を受け取って Actor に渡す値を返し、invoke.onDone と invoke.onError で成功時と失敗時の遷移先を決める。fromPromise は Promise ベースの Actor を作る。

const toggleMachine = setup({
  actors: {
    resetCount: fromPromise(async ({ input }: { input: { maxCount: number } }) => {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      if (Math.random() < 0.5) throw new Error("リセットエラー")
      return { count: input.maxCount / 2 }
    }),
  },
}).createMachine({
  states: {
    resetting: {
      invoke: {
        input: ({ context: { maxCount } }) => ({ maxCount }),
        src: "resetCount",
        onDone: {
          target: "inactive",
          actions: assign({ count: ({ event }) => event.output.count }),
        },
        onError: "inactive",
      },
    },
  },
})

fromCallback を使うと、コールバック関数の中から sendBack で親 Actor に Event を送る Actor を作れる。戻り値の関数はクリーンアップ関数で、Actor が終了したときに呼ばれる。

fromCallback(({ sendBack }) => {
  const interval = setInterval(() => sendBack({ type: "TICK" }), 10)
  return () => clearInterval(interval)
})

遅延遷移

遅延遷移は、指定した時間が経つと自動で起きる遷移である。after で時間を指定する。次の例では、active に遷移してから 2 秒後に inactive に戻る。

createMachine({
  states: {
    active: { on: { toggle: "inactive" }, after: { 2000: "inactive" } },
  },
})

キーワード

参考