LoginSignup
4
4

More than 1 year has passed since last update.

xstate眠たくなる問題

Last updated at Posted at 2021-05-24

問題

xstateは使えるとビジネスロジックをかなり綺麗に纏められるようになるし、各state事で起こせるeventは固定なので、組む時のバグを減らせるしユーザーがインスペクターから勝手に関連性のないeventを発火させることも出来ない。
便利なはずだと思ってサンプルを見てみても簡単なトグルスイッチとか赤黄青の信号機のサンプルで、実際に自分で使うとなると、ビジネスロジックをどういう風にxstateに落とし込むの分からなくなる。

更にstateとcontextと状態管理とは?どっちなの?とか、stateとeventで似てるようで違う設定項目だったり、actionするのかinvokeするのか違いが分かりにくかったり、やってる内にとにかく眠くなる。

machine

核となる部分。finite state machine = FSM = 要は決まった状態では決まった状態への移動しか出来ないようになっているもの的な。

state

machineで表現したい状態を指す。

context

最初はstateと混乱してしまうかもしれないが、状態というよりはmachine上で何らかの値を保持しておく場所みたいなもの。

event

各state時に何と言われたらどうするかというイベント。

activity

state時に継続的に実行したい場合(setInterval的な)
assignは出来ない

action

実行して結果は気にしないもの
assignでcontextを更新したい場合

invoke

実行して結果次第で次に行う事柄が違う場合に使う
assignは出来ない

大雑把なmachineのよく使う設定内容

typescriptでの書き方は何種類かあるが、これがシンプルかも。

your-machine.ts
// https://xstate.js.org/docs/packages/xstate-immer/#xstate-immer
// immer版のassignのほうが楽
import { assign } from "@xstate/immer"
import { createMachine } from "xstate"

// 必要に応じてcontextを持てる 無くてもいい
type Context = {
  value: number
  message: string
}
// 別に大文字じゃなくていいが、event名は大文字にしておくことでstate名との混乱を避ける
type Event =
  | { type: "EVENT_NAME" }
  | { type: "HAS_EVENT_PARAM"; param: number }
  | { type: "HAS_EVENT_PARAM_OBJECT"; payload: { a: number; b: string } }
  // またはdot表記ではっきりとstateと区別するとか
  // こうするとどうしても
  // on: { "eventType.eventAction": { target: "..." } }
  // のように文字列で書かなければいけないので、シンタックスハイライトで色分けもされるし
  | { type: "eventType.eventAction" }

export default createMachine<Context, Event>(
  // configオブジェクト -----------------------
  {
    // nestedStateの時に子stateから親stateを呼び出す時に使ったりする
    id: "someId",
    // 必要に応じてContextタイプに沿ったオブジェクトを宣言する
    context: {
      value: 0,
      message: "",
    },
    initial: "stateName", //初期stateの宣言
    on: {
      // グローバルなevent
      // stateがどの状態であっても呼んでいいeventを書く
    },

    states: {
      stateName: {
        entry: ["actionName", "actionName"],
        exit: ["actionName", "actionName"],
        activities: ["activityName", "activityName"],
        always: [
          // 上から順にcondの結果を見てtrueを返すところでtargetへ遷移する
          {
            target: "stateName",
            cond: "guardName",
          },
          {
            target: "stateName",
            cond: "guardName",
          },
          {
            target: "stateName",
          },
        ],
        invoke: [
          {
            // idはsendからinvokeを呼ぶ時に必要
            // https://xstate.js.org/docs/guides/actions.html#send-targets
            id: "invokeId",
            src: "serviceName",
            onDone: {
              actions: ["actionName", "actionName"],
              target: "stateName",
            },
            onError: {
              actions: ["actionName", "actionName"],
              target: "stateName",
            },
          },
        ],
        after: {
          // someDurationはdelaysに宣言できる
          // 直接ミリ秒の数字を書いてもいい
          // 1000: { target: "stateName" }
          // fallback的に一定秒数以上経ったら諦めて別のstateに移動するなどに使える
          someDuration: {
            actions: ["actionName", "actionName"],
            target: "stateName",
          },
        },

        on: {
          "": [
            // 有無を言わさず速攻で実行されるイベント
            // https://xstate.js.org/docs/guides/events.html#null-events
            // v5ではdeprecated -> alwaysを代わりに使う
            {
              cond: "guardName",
              target: "stateName",
            },
            {
              cond: "guardName",
              target: "stateName",
            },
            {
              target: "stateName",
            },
          ],
          "*": [
            // https://xstate.js.org/docs/guides/transitions.html#wildcard-descriptors
            // どんなイベントが起きても実行する
            {
              cond: "guardName",
              target: "stateName",
            },
            {
              cond: "guardName",
              target: "stateName",
            },
            {
              target: "stateName",
            },
          ],
          EVENT_NAME: {
            actions: ["actionName", "actionName"],
            target: "stateName",
          },
        },
      },
      nestedStateName: {
        // stateの中にstatesを持つことで「Aで且つB」というような状態が持てる
        // 子となるstateから上位のstatesを呼ぶにはmachineのidから辿って呼べたりする
        // target: "#someId.stateName"
        // https://xstate.js.org/docs/guides/ids.html#relative-targets
        initial: "stateName",
        states: {
          stateA: {
            on: {
              // ...
            },
          },
          stateB: {
            on: {
              // ...
            },
          },
        },
      },
      parallelStateName: {
        // 「Aで且つBとC」のような状態が持てる
        type: "parallel",
        states: {
          parallelStateA: {
            initial: "stateName",
            states: {
              stateNameA: {
                on: {
                  // ...
                },
              },
              stateNameB: {
                on: {
                  // ...
                },
              },
            },
          },
          parallelStateB: {
            initial: "stateName",
            states: {
              stateNameA: {
                on: {
                  // ...
                },
              },
              stateNameB: {
                on: {
                  // ...
                },
              },
            },
          },
        },
      },
      memoryStateName: {
        initial: "stateName",
        states: {
          stateNameA: {},
          stateNameB: {},
          stateNameC: {},
          historyStateName: {
            // ネストされたstateなどで前回のstateをデフォルトとして遷移したい場合などに便利
            // 呼び出すのはあくまでも外側のstateから
            type: "history",
            history: "shallow",
          },
        },
      },
      someStateName: {
        on: {
          EVENT_NAME: {
            // historyで前回最終stateから呼び出したい場合は
            // type: "history"のstateを指定して呼ぶ
            target: "memoryStateName.historyStateName",
          },
        },
      },
    },
  },
  // configオブジェクト

  // optionsオブジェクト ------------------------
  {
    activities: {
      // !!!activity内でassignは出来ない
      // https://github.com/davidkpiano/xstate/discussions/1278
      // https://github.com/davidkpiano/xstate/discussions/1652
      activityName: (context, event) => {},
    },

    // https://xstate.js.org/docs/guides/effects.html
    // - actionは呼び出した後の** 結果を気にしないもの **
    // - assignする時などに使う
    actions: {
      actionName: (context, event) => {},
      actionName2: assign((context, event) => {
        // import { assign } from "@xstate/immer"
        // immer版assignを使っているので、contextは破壊的に変更して構わない
        // xstate標準のassignだとReact.setStateの時のように
        // オブジェクトを新しく作って返さないといけないので面倒
        context.value += 1
      }),

      //  --------------------------
      // xstate標準だとassignは二通りの書き方がある
      //  --------------------------
      // context eventを取る関数で、次のcontextとなるオブジェクトを返す
      // actionName: assign((context, event) => {
      //   return {
      //     ...
      //   }
      // })

      // 次のcontextとなるオブジェクトをassignへ渡すが
      // 変更したいcontext内容にはcontext eventを取る関数で更新する値を返す
      // actionName: assign({
      //   member: (context, event) => value
      // })
    },

    // https://xstate.js.org/docs/guides/effects.html
    // ================
    // service = invoke
    // ================
    // は呼び出したあとの結果次第で実行するなにかが変わる場合に使う
    //
    // !!!service内でassignは出来ない
    // https://github.com/davidkpiano/xstate/discussions/1278
    // https://github.com/davidkpiano/xstate/discussions/1652
    // service内で同stateが受け取れるeventを起こし、そのevent内でassignをするactionを使うことで
    // invokeしながらassign出来る

    // https://xstate.js.org/docs/guides/communication.html#the-invoke-property
    // invokeするservice
    // - promise
    // - callback
    // - observable
    // - machine
    // のどれかのパターン
    services: {
      serviceName: (context, event) => new Promise((resolve, reject) => {}),
    },

    // contextの内容や
    // send({type: "eventName", payload: "123" })
    // などとしてeventオブジェクトにパラメータを渡してその内容からtrue/falseで結果を返す
    // trueの時のみ他のstateへ遷移させる
    guards: {
      guardName: (context, event) => {
        // ...
        return true
      },
    },

    // 一定の秒数後に他のstateに遷移する場合で、contextやeventのパラメータによって
    // その秒数を変えたい時に使う
    delays: {
      delayName: (context, event) => {
        // ...
        return 1000
      },
    },
  }
  // optionsオブジェクト
)

xstateの使い所

最初はどういう風にビジネスロジックとかをxstateに落とし込めば良いのか分からないが、setState<boolean>(false)とかで何らかの状態を管理する値が2つあった場合、もうそれはxstateで管理したほうが良い。

state A state B
true true
false false

4パターンの組み合わせがあり、多分どれかは絶対にあってはいけない状態だったりする。
(例:loading: true、error: true みたいなloading中なのに既にerror発生してる)
(例:success: true、error: true)
これを自己管理しようとすると4パターンだけでも結構面倒だし、コンポーネント間で参照するとかあればよりコードが分散する。
xstateに落とし込めばロジックが全部machineオブジェクト1つに集約されるので、コードがスッキリする。

とても参考になるサイト

22レッスンがタダで見れる

drag&dropをxstateで

ビデオプレイヤーをxstateで

長い割にかなり雑

xstate作者本人の解説(他にも山ほどあるが最近見た中でxstateがなんで理にかなってるかがよく分かる)

よく使うパターンのmachineが定義されている

上のxstate-catalogueの作業内容(Matt Pocock)

jotaiと組み合わせて使うとコンポーネント間でのやり取りが楽になる

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4