問題
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での書き方は何種類かあるが、これがシンプルかも。
// 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と組み合わせて使うとコンポーネント間でのやり取りが楽になる