🐕

XState のステートマシンのコンセプト

2024/05/02に公開

はじめに

XState は、JavaScript および TypeScript アプリの状態管理およびオーケストレーションソリューションです。

この記事では XState をより理解するために、以下の 2 点について解説します。

  1. 従来のステートマシン
  2. State 上でのステートマシンのコアコンセプト

ステートマシンとはなにか

まずはじめに従来のステートマシンについて説明します。

ステートマシンとは、イベントの発生時にどのような状態からどのような状態に遷移するかを表現するモデルです。状態(ステート)を表現する機械(マシン)なのでステートマシンと呼ばれます。

ステートマシンは、状態、イベント、遷移の集合で構成されます。ソフトウェア開発において、ステートマシンを利用することでアプリケーションの振る舞いをモデル化できます。不可能な状態に回避したり望ましくない遷移を特定することが容易になります。

状態が有限であるステートマシンを有限ステートマシンと呼びます。本記事で扱うステートマシンは全て有限ステートマシンです。

具体的な例を見てみましょう。

【 例1 】ボールペンの例

例えばボールペンをイメージしてみましょう。ボールペンのペンはボタンを押すことでペン先が出るようになっています。ボールペンの状態をステートマシンで表現すると以下のようになります。

状態
状態とは、ボールペンがどのような状態にあるかを表します。ボールペンの状態は「ペン先が出ている」か「ペン先が出ていない」の 2 つの状態があります。

イベント
イベントとは、ボールペンの状態を変化させるためのトリガーです。ボールペンの状態を変化させるためには、ボタンをクリックするというイベントが必要です。

遷移
遷移とは、ボールペンの状態がどのように変化するかを表します。ボールペンのペン先が出ている状態でボタンをクリックすると、ペン先が出ていない状態に遷移します。逆に、ペン先が出ていない状態でボタンをクリックすると、ペン先が出ている状態に遷移します。

状態遷移図
図で表現するとこのようになります。なお、ステートマシンの状態を示す図を状態遷移図と呼びます。

Alt text

状態遷移表
表を用いて状態遷移を表現できます。この表を状態遷移表と呼びます。

現在の状態 入力 次の状態
ペン先が出ている ボタンをクリック ペン先が出ていない
ペン先が出ていない ボタンをクリック ペン先が出ている

【 例2 】正規表現の例

ここでは正規表現 abc* を記述するステートマシンを作成します。abc*a の後に b が続き、その後に c が 0 回以上続く文字列を表します。

文字列 受け入れ可否
a
ab
abc
abcc
b
abb
c

状態遷移図
正規表現 abc* の条件を満たす状態、つまり「受容状態」は二重丸で表現されます。今回はそれぞれの状態に ID を振っています。見て分かる通り、状態 0 から状態 1 に遷移するときには a が入力され、状態 1 から状態 2 に遷移するときには b が入力されます。abc* の条件を満たす状態を「受容状態」と呼びます。

状態遷移表

現在の状態 入力 次の状態
0 a 1
0 b 3
0 c 3
1 a 3
1 b 2
2 a 3
2 b 3
2 c 2
3 a 3
3 b 3
3 c 3

【 例3 】Toggle ボタンの例

Toggle ボタンをステートマシンで表現すると以下のようになります。

状態遷移図
ボールペンと異なり初期状態を追加しています。初期状態とはステートマシンが開始されたとき、最初に入る状態です。今回、初期状態はオフだとしています。

状態遷移表
「状態遷移表」を用いることで、状態遷移の規則を表に表す事ができます。

現在の状態 入力 次の状態
オフ ボタンをクリック オン
オン ボタンをクリック オフ

なぜステートマシンを利用するのか

ステートマシンを利用することで複雑なアプリケーションの状態を管理が簡素化されます。具体的には、ステートマシンによって、状態遷移とイベントが構造的に管理されます。また、アプリケーションの状態管理とそれ以外の部分を明確に分離されます。以上により、複雑さが軽減することでバグが防止し、コード品質を向上させることで保守性が向上されます。

ステートマシンをソフトウェア開発に利用する主なメリットはこちらです。

  • 【わかりやすい】 ステートマシンは視覚的で理解しやすいため、チームや関係者間でのコミュニケーションコストが下がります。
  • 【テストが簡単】 ステートマシンは決定論的であるためテストが簡単です。考えられるすべての状態とそれらの間の遷移をテストできます。(決定的とは、ある状態で特定のイベントが発生した場合、次の状態が一意に決まることを意味します。詳しくの後ほど説明します。)
  • 【実装が簡単】 ステートマシンはどのプログラミング言語でも簡単に実装できます。ステートマシンのライブラリを使用するか、あるいは独自でステートマシンのライブラリを作成できます。
  • 【メンテナンスが簡単】 ステートマシンは理解、テスト、実装が簡単なため、保守が簡単です。

XState 上でのステートマシンとはなにか

XState では従来のステートマシンの機能を実現しつつ、更に機能を追加することでより複雑な状態を表現できます。XState のステートマシンでは階層化された状態、並列状態による同時実行、など通常のステートマシンでは利用できない機能を提供しています。

ここでは公式ページの内容にそって、XState 上でのステートマシンについて説明します。

https://stately.ai/docs/state-machines-and-statecharts

はじめに作業用のコードとして Next.js のプロジェクトを作成しておきます。

新規プロジェクト作成と初期環境構築の手順詳細

作業環境を構築

Next.js で作業環境を構築します。

プロジェクトの作成

create next-app@latestでプロジェクトを作成します。

$ pnpm create next-app@latest next-xstate-statemachinetutorial --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd next-xstate-statemachinetutorial

Peer Dependenciesの警告を解消

Peer dependenciesの警告が出ている場合は、pnpm installを実行し、警告を解消します。

 WARN  Issues with peer dependencies found
.
├─┬ eslint-config-next 14.2.1
│ └─┬ @typescript-eslint/parser 7.2.0
│   └── ✕ unmet peer eslint@^8.56.0: found 8.0.0
└─┬ next 14.2.1
  ├── ✕ unmet peer react@^18.2.0: found 18.0.0
  └── ✕ unmet peer react-dom@^18.2.0: found 18.0.0

以下を実行することで警告が解消されます。

$ pnpm i -D eslint@^8.56.0
$ pnpm i react@^18.2.0 react-dom@^18.2.0

不要な設定を削除し、プロジェクトを初期化します。

styles

CSSなどを管理するstylesディレクトリを作成します。globals.cssを移動します。

$ mkdir -p src/styles
$ mv src/app/globals.css src/styles/globals.css

globals.cssの内容を以下のように上書きします。

src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

初期ページ

app/page.tsxを上書きします。

src/app/page.tsx
import { type FC } from "react";

const Home: FC = () => {
  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
    </div>
  );
};

export default Home;

レイアウト

app/layout.tsxを上書きします。

src/app/layout.tsx
import "@/styles/globals.css";
import { type FC } from "react";
type RootLayoutProps = {
  children: React.ReactNode;
};

export const metadata = {
  title: "Sample",
  description: "Generated by create next app",
};

const RootLayout: FC<RootLayoutProps> = (props) => {
  return (
    <html lang="ja">
      <body className="">{props.children}</body>
    </html>
  );
};

export default RootLayout;

TailwindCSSの設定

TailwindCSSの設定を上書きします。

tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  plugins: [],
}
export default config

TypeScriptの設定

TypeScriptの設定を上書きします。

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

スクリプトを追加

型チェックのスクリプトを追加します。

package.json
{
  "name": "next-tsconfig-resolvejsonmodule",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
+   "typecheck": "tsc"
  },
  "dependencies": {
    "next": "14.1.4"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8",
    "eslint-config-next": "14.1.4",
    "postcss": "^8.4.38",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "tailwindcss": "^3.3.0",
    "typescript": "^5"
  }
}

動作確認

ローカルで動作確認します。

$ pnpm run dev

コミットして作業結果を保存しておきます。

$ git add .
$ git commit -m "feat:新規にプロジェクトを作成し, 作業環境を構築"

XState をインストール

XState をインストールします。

$ pnpm install xstate @xstate/react

コミットします。

$ git add .
$ git commit -m "feat:add xstate"

ステートマシンを作成

ステートマシンを追加します。

$ mkdir -p src/machines
$ touch src/machines/demo-1.ts
$ touch src/machines/demo-2.ts
$ touch src/machines/demo-3.ts
$ touch src/machines/demo-4.ts
$ touch src/machines/demo-5.ts
$ touch src/machines/demo-6.ts
src/machines/demo-1.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-1',
  initial: '寝ている',
  description: "状態のみです。遷移はないため`寝ている`状態から`起きている`状態に遷移することはありません。",
  states: {
    "寝ている": {
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    },
    "起きている": {
      description:
      "![happy awake puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",

    },
  },
});
src/machines/demo-2.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-2',
  initial: '寝ている',
  description: "`起きる`イベントをトリガーに`寝ている`状態から`起きている`状態へ、`寝る`イベントをトリガーに`起きている`状態から`寝ている`状態へ遷移します。",
  states: {
    "寝ている": {
      on: {
        "起きる": {
          target: "起きている",
        },
      },
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    },
    "起きている": {
      on: {
        "寝る": {
          target: "寝ている",
        },
      },
      description:
      "![happy awake puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",

    },
  },
});
src/machines/demo-3.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-3',
  initial: '散歩待ち',
  description: "多くのステートマシンには最終状態があります。この図では`散歩終了`状態が最終状態となります。",
  states: {
    "散歩待ち": {
      on: {
        "家を出る": {
          target: "散歩中",
        },
      },
      description:
      "![puppy patiently waiting](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/waiting.svg)",
    },
    "散歩中": {
      on: {
        "家に着く": {
          target: "散歩終了",
        },
      },
      description:
      "![happy walking puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",
    },
    "散歩終了": {
      type: "final",
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    }
  },
});
src/machines/demo-4.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-4',
  initial: '散歩待ち',
  description: "複数の状態を一つにまとめて階層化された状態を表現することが出来ます。階層化された状態では、親状態と子状態があります。散歩中状態に属する子状態として、`歩く`状態、`走る`状態の2つの子状態を作ることができます。",
  states: {
    "散歩待ち": {
      on: {
        "家を出る": {
          target: "散歩中",
        },
      },
      description:
      "![puppy patiently waiting](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/waiting.svg)",
    },
    "散歩中": {
      on: {
        "家に着く": {
          target: "散歩終了",
        },
      },
      initial: "歩く",
      states: {
        "歩く": {
          on: {
            "スピードアップ": {
              target: "走る",
            },
          },
          description:
            "![happy walking puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",
        },
        "走る": {
          on: {
            "スピードダウン": {
              target: "歩く",
            },
          },
          description:
            "![puppy running bouncily](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/running.svg)",
        },
      },
    },
    "散歩終了": {
      type: "final",
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    }
  },
});
src/machines/demo-5.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: "demo-5",
  initial: '散歩待ち',
  description: "多くのステートマシンには最終状態があります。この図では`散歩終了`状態が最終状態となります。",
  states: {
    "散歩待ち": {
      on: {
        "家を出る": {
          target: "散歩中",
        },
      },
      description:
      "![puppy patiently waiting](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/waiting.svg)",
    },
    "散歩中": {
      type: "parallel",
      on: {
        "家に着く": {
          target: "散歩終了",
        },
      },
      states: {
        "足の動き": {
          initial: "歩く",
          states: {
            "歩く": {
              on: {
                "スピードアップ": {
                  target: "走る",
                },
              },
              description:
                "![happy walking puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",
            },
            "走る": {
              on: {
                "スピードダウン": {
                  target: "歩く",
                },
              },
              description:
                "![puppy running bouncily](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/running.svg)",
            },
          },
        },
        "尻尾の動き": {
          initial: "尻尾を振らない",
          states: {
            "尻尾を振らない": {
              on: {
                "楽しい": {
                  target: "尻尾を振る",
                },
              },
              description:
              "![puppy sniffing the ground with a still tail](https://raw.githubusercontent.com/statelyai/assets/d236419f16a3b51a653e401084e9e9be8cfdbaa3/example-images/dogs/sniffing.svg)",
            },
            "尻尾を振る": {
              on: {
                "落ち着く": {
                  target: "尻尾を振らない",
                },
              },
              description:
              "![puppy sniffing the ground with a wagging tail](https://raw.githubusercontent.com/statelyai/assets/d236419f16a3b51a653e401084e9e9be8cfdbaa3/example-images/dogs/wagging.svg)",
            },
          },
        },
      },
    },
    "散歩終了": {
      type: "final",
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    }
  },
});
src/machines/demo-6.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-6',
  initial: 'おやつ待ち',
  description: "自己遷移で`おやつ待ち`状態に戻ります。",
  states: {
    "おやつ待ち": {
      on: {
        "おやつをあげる": {
          target: "おやつ待ち",
        },
      },
      description:
      "![happy walking puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/begging.svg)",
    },
  },
});

コミットします。

$ git add .
$  git commit -m "マシンを追加"

状態遷移図

あらためて状態遷移図は、ステートマシンの状態、イベント、遷移を視覚的に表現したものです。

XState の状態遷移図は以下の特徴を持ちます。これらは従来の状態遷移図と同様の特徴を持ちます。

  • 状態を四角で表現
  • 遷移を矢印で表現

XState の状態遷移図は、従来の状態遷移図に加えて全てではありませんが以下の特徴を持ちます。

  • 階層化された状態を表現
  • 状態の同時実行を表現

状態

あらためて状態とは、ステートマシンが取り得る状態を表します。ステートマシンは、状態を遷移させることで、アプリケーションの振る舞いをモデル化します。ステートマシンは一度に 1 つの状態にしか存在できません。例えば、寝ている状態起きている状態の 2 つがあったとします。犬は寝ることと起きていることを同時に行うことは出来ませんので、どちらか 1 つの状態を取ります。

ステートマシンに初期状態を与えることでどの状態から開始するか明確に指定することが出来ます。この状態遷移図では寝ている状態が処置値となっています。

alt text

XState のコードで表現するとこうなります。

src/machines/demo-1.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-1',
  initial: '寝ている',
  description: "状態のみです。遷移はないため`寝ている`状態から`起きている`状態に遷移することはありません。",
  states: {
    "寝ている": {
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    },
    "起きている": {
      description:
      "![happy awake puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",

    },
  },
});

状態遷移図で表示されたステートマシンの 1 つ課題があります。状態遷移がないため寝ている状態から起きている状態に遷移することが出来ません。

遷移とイベント

ある状態から別の状態に移動するためには遷移が必要であり、遷移を引き起こすにはイベントが必要です。犬が寝ている状態から起きている状態に遷移するためには、犬が起きるというイベントが必要です。また、犬が起きている状態から寝ている状態に遷移するためには、犬が寝るというイベントが必要です。

遷移は決定的です。決定的とはある状態で特定のイベントが発生した場合、次の状態が一意に決まることを意味します。例えば、犬が寝ている状態で起きるイベントが発生した場合、必ず起きている状態に遷移します。

alt text

XState のコードで表現するとこうなります。

src/machines/demo-2.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-2',
  initial: '寝ている',
  description: "`起きる`イベントをトリガーに`寝ている`状態から`起きている`状態へ、`寝る`イベントをトリガーに`起きている`状態から`寝ている`状態へ遷移します。",
  states: {
    "寝ている": {
      on: {
        "起きる": {
          target: "起きている",
        },
      },
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    },
    "起きている": {
      on: {
        "寝る": {
          target: "寝ている",
        },
      },
      description:
      "![happy awake puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",

    },
  },
});

最終状態

ステートマシンに最終状態を指定できます。最終状態は、ステートマシンが終了したことを示します。最終状態は、二重のボーダー囲み表現します。

散歩の状態遷移図を例に取ると、散歩待ち状態から散歩中状態に遷移し、散歩中状態から散歩終了状態に遷移します。散歩終了状態は最終状態であり、ステートマシンが終了したことを示します。

alt text

XState のコードで表現するとこうなります。

src/machines/demo-3.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-2',
  initial: '寝ている',
  description: "`起きる`イベントをトリガーに`寝ている`状態から`起きている`状態へ、`寝る`イベントをトリガーに`起きている`状態から`寝ている`状態へ遷移します。",
  states: {
    "寝ている": {
      on: {
        "起きる": {
          target: "起きている",
        },
      },
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    },
    "起きている": {
      on: {
        "寝る": {
          target: "寝ている",
        },
      },
      description:
      "![happy awake puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",

    },
  },
});

階層化された状態

複数の状態を 1 つにまとめて階層化された状態を表現することが出来ます。階層化された状態には、親状態子状態があります。親状態は子状態を持ち、子状態は親状態に属します。XState では、親状態と子状態を見分けるために、四角のボックスで子状態を表現します。親状態と子状態は、より複雑な状態を表現できる XState 特有の機能です。子状態においてどの状態が初期状態か指定する必要があります。

例えば、親状態では、散歩待ち状態、散歩中状態、散歩終了状態の 3 つの状態があったとします。散歩中状態に属する子状態として、歩く状態、走る状態の 2 つの子状態を作ることができます。図では歩く状態が初期状態です。

alt text

XState のコードで表現するとこうなります。

src/machines/demo-4.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: 'demo-4',
  initial: '散歩待ち',
  description: `複数の状態を一つにまとめて階層化された状態を表現することが出来ます。階層化された状態では、親状態と子状態があります。散歩中状態の`,
  states: {
    "散歩待ち": {
      on: {
        "家を出る": {
          target: "散歩中",
        },
      },
      description:
      "![puppy patiently waiting](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/waiting.svg)",
    },
    "散歩中": {
      on: {
        "家に着く": {
          target: "散歩終了",
        },
      },
      initial: "歩く",
      states: {
        "歩く": {
          on: {
            "スピードアップ": {
              target: "走る",
            },
          },
          description:
            "![happy walking puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",
        },
        "走る": {
          on: {
            "スピードダウン": {
              target: "歩く",
            },
          },
          description:
            "![puppy running bouncily](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/running.svg)",
        },
      },
    },
    "散歩終了": {
      type: "final",
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    }
  },
});

原子状態

原子状態とは子状態を持たない状態です。散歩待ち状態、歩く状態、走る状態、散歩終了のいずれも原子状態です。

alt text

並列状態

並列状態とは全ての子状態が同時に実行されている状態です。

例えば、親状態の散歩中の状態において、2 つの領域が存在するとします。1 つめの領域は歩く状態と走る状態を含む足の動きに関連する領域、2 つ目は尻尾を振る状態と振らない状態に関連する領域です。犬は尻尾を振りながら歩くことができるので、これら 2 つの領域は同時に実行可能です。こういった複数の状態を同時に実行できるのが並列状態です。

XState では、並列状態を表現するために、四角のボックスを点線で囲みます。

alt text

XState のコードで表現するとこうなります。

src/machines/demo-5.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  id: "demo-5",
  initial: '散歩待ち',
  description: "多くのステートマシンには最終状態があります。この図では`散歩終了`状態が最終状態となります。",
  states: {
    "散歩待ち": {
      on: {
        "家を出る": {
          target: "散歩中",
        },
      },
      description:
      "![puppy patiently waiting](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/waiting.svg)",
    },
    "散歩中": {
      type: "parallel",
      on: {
        "家に着く": {
          target: "散歩終了",
        },
      },
      states: {
        "足の動き": {
          initial: "歩く",
          states: {
            "歩く": {
              on: {
                "スピードアップ": {
                  target: "走る",
                },
              },
              description:
                "![happy walking puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/walking.svg)",
            },
            "走る": {
              on: {
                "スピードダウン": {
                  target: "歩く",
                },
              },
              description:
                "![puppy running bouncily](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/running.svg)",
            },
          },
        },
        "尻尾の動き": {
          initial: "尻尾を振らない",
          states: {
            "尻尾を振らない": {
              on: {
                "楽しい": {
                  target: "尻尾を振る",
                },
              },
              description:
              "![puppy sniffing the ground with a still tail](https://raw.githubusercontent.com/statelyai/assets/d236419f16a3b51a653e401084e9e9be8cfdbaa3/example-images/dogs/sniffing.svg)",
            },
            "尻尾を振る": {
              on: {
                "落ち着く": {
                  target: "尻尾を振らない",
                },
              },
              description:
              "![puppy sniffing the ground with a wagging tail](https://raw.githubusercontent.com/statelyai/assets/d236419f16a3b51a653e401084e9e9be8cfdbaa3/example-images/dogs/wagging.svg)",
            },
          },
        },
      },
    },
    "散歩終了": {
      type: "final",
      description:
      "![sleeping puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/asleep.svg)",
    }
  },
});

自己遷移

自己遷移とは、イベントが発生しても、同じ状態に遷移することです。

例えば、犬のおねだりプロセスでは、おやつをもらうイベントを伴うおねだり状態が存在します。そして、おやつが大好きな犬の場合、おやつをもらうイベントを何度経験しても、犬はおやつのおねだり状態に戻ります。

alt text

XState のコードで表現するとこうなります。

src/machines/demo-6.ts
import { createMachine } from 'xstate';

export const demo1Machine = createMachine({
  /** @xstate-layout N4IgpgJg5mDOIC5QTAWwPYFoBsA6QUgyAhDICYMgofqCGDAMRHGBJDIEIMgSgyDRDANoAMAuoqAA7qwAlgBdB6AHY8QAD0QAOAKy4ALAsUBGOQCYAnNmUB2HcoDMAGhABPROoPtc2ddhMbTJ9WoUBfHxfHoUeCQQFAwcKX4hUQkpWQRMDxNcE1MtA1tVbAUTAwMLa3jsPB12F3VlOXUTaoV1LV8QtCw8GgoIgRExSWC4hPYlFJM0jIUsnLyrRCzcUa0h5R1c9gM1ep8vIA */
  id: 'demo-6',
  initial: 'おやつ待ち',
  description: "自己遷移で`おやつ待ち`状態に戻ります。",
  states: {
    "おやつ待ち": {
      on: {
        "おやつをあげる": {
          target: "おやつ待ち",
        },
      },
      description:
      "![happy walking puppy](https://raw.githubusercontent.com/statelyai/assets/main/example-images/dogs/begging.svg)",
    },
  },
});

言葉の整理

最後に、参考までにステートマシンに関連する言葉を整理します。

言葉 英語
状態 ステートマシンが取り得る状態を表します。
状態遷移図 ステートマシンの状態、イベント、遷移を視覚的に表現した図
遷移 状態から別の状態にいどうすること
有限ステートマシン 有限個の状態を持つステートマシン
イベント 状態遷移を引き起こすトリガー
決定的 ある状態で特定のイベントが発生した場合、次の状態が一意に決まること
初期状態 ステートマシンが開始されたとき、最初に入る状態
最終状態 ステートマシンが終了したことを示す状態
親状態 子状態を持つ状態
子状態 親状態をもつ状態
原子状態 子状態を持たない状態
並列状態 全ての子状態が同時に実行されている状態
自己遷移 イベントが発生しても、同じ状態に遷移すること

まとめ

この記事では以下の 2 点について解説しました。

  1. 従来のステートマシン
  2. State 上でのステートマシンのコアコンセプト

作業リポジトリはこちらです。

https://github.com/hayato94087/next-xstate-statemachinetutorial

Discussion