問題
モデルをxstateに落とし込んでいくと、どうしてもmachineを分割して、1つのmachineが他のmachineを管理して全体としてのstateを管理したい事が出てくる。(例:one-to-many、many-to-many)
解決方法
親machineに子machineを持たせる
やり方は
- contextに直接保持しておく
- invokeでsrcにmachineを当てる
だけだと思う。
contextに保持
interface Context{
child: ActorRefFrom<typeof childMachine>
}
...
{
states: {
idle: {
entry: ["setChild"] // どこでもどのタイミングでも問題ない
}
}
},
{
actions: {
setChild: assign((context, event) => ({...context, child: spawn(childMachine)}))
}
})
ポイントはspawn
を使って、contextに置いておくこと。contextに置くので、contextを更新するまでずっと存在する。
注意点としてはassignする際に
assign((context, event) => ({ ...context, child: spawn(...) }))
assign({
child: (context, event) => spawn(...)
})
としなければ正常動作しない。
assign({
child: spawn(...) // ❌
})
こうするとエラーも何も出ず、動かない。
machineをinvokeする
{
states: {
idle: {
invoke: {
id: "childMachineId",
src: childMachine,
onDone: { ... }
}
}
}
}
invokeなので、invokeされたstate内でのみ存在している。用途としては通常のinvoke serviceにしては複雑で、machineに落とし込んでしまったほうが簡潔にまとまるような時に便利。
events
親から子にeventを投げる方法
sendを使う。sendは一般的なinvoke serviceに向けてeventを投げたりするが、親子関係のあるmachineであれば親から子にeventを投げることも出来る。
eventを投げる際には、子がcontext内に入っているのならそのオブジェクト(またはspawn時の名前)、invokeされたmachineならそのIDを指定してやる必要がある。
注意点としてactionの中で
context.child.send(...)
ともやれなくはないが、これは偶然動いてるだけなので作法としてダメ。必ずimport { send } from "xstate"
のsendを通してeventを送る必要がある。
import { actions, ... } from "xstate" // actions.pureを使う場合
{
on: {
"SOME.EVENT": {
actions: [send("EVENT.TO.CHILD", { to: (context, event) => context.child })]
// もしくは
// actions: [send("EVENT.TO.CHILD", { to: ”spawned-child” })]
// またはこうやっても良い
// actions: ["sendToChild"]
}
}
},
{
actions: {
setChild: assign((context, event) => ({
...context,
child: spawn(childMachine, { name: "spawned-child" })
// nameは指定できるが、contextからオブジェクト自体を参照できるのでしなくても問題ない
})),
sendToChild: actions.pure((context, event) => {
// context, eventによって振り分けが必要だったり、
// コードが煩雑になる場合はこういう風にactionに落とし込んだほうがスッキリする
return send("EVENT.TO.CHILD", { to: context.child })
})
}
}
{
on: {
"SOME.EVENT": {
// ここではidを直接指定する以外ない
actions: [send("EVENT.TO.CHILD", { to: "childMachineId" })]
}
},
states: {
idle: {
invoke: {
id: "childMachineId",
src: childMachine,
onDone: { ... }
}
}
}
}
複数の子machineにsendしたい場合
actions: [...]
に書こうと思えば書けなくもないが、コードが煩雑になるのでactions.pure
を使う。
各childに対してsendを生成して、それを配列として返してやる。
import { actions, ... } from "xstate"
{
context: {
childMachines: []
},
on: {
"ADD.CHILD": {
actions: ["addChild"]
}
"EVENT.TO.CHILD_MACHINES": {
actions: ["sendToChildren"]
}
},
},
{
actions: {
sendToChildren: actions.pure((context, event) => {
return context.childMachines.map((child) => send("EVENT.TO.CHILD", { to: child }))
})
}
}
子から親にeventを投げる場合
import { sendParent, ... } from "xstate"
{
on: {
"SOME.EVENT": {
actions: [sendParent("EVENT.TO.PARENT")]
}
}
}
spawnのsyncやautoForward
sync
親から子をspawnする際に、子のstateが変更する度に親に通知をするかどうか。子のstateがAからBに変わると親のparentService.onTransition((state) => ...)
が反応するようになる。(多分const [state, send] = useMachine(parentMachine)
も)
これだと反応が多すぎるって言う場合は、syncは使わずsendUpdateを子の要所要所で使えば、的確に親に通知が出来る。
{
...
states: {
idle: {},
stateA: {
entry: [sendUpdate()]
},
}
}
autoForward
は親で受けたeventをそのまま子にも流すかどうか。clickイベントのイベントバブリング的な。
親子関係がよっぽど蜜で無い限りは使わなそう。
参考
invokeしたmachineでの親子関係 https://github.com/msurdi/videittous/tree/main/src/machines にmachineがあるのですごく参考になる。
超シンプルに作ったサンプル。