0
1

More than 3 years have passed since last update.

[xstate] machine間で親子関係(ツリー)によりstate、eventを管理する方法

Last updated at Posted at 2021-08-03

問題

モデルをxstateに落とし込んでいくと、どうしてもmachineを分割して、1つのmachineが他のmachineを管理して全体としてのstateを管理したい事が出てくる。(例:one-to-many、many-to-many)

解決方法

親machineに子machineを持たせる

やり方は

  1. contextに直接保持しておく
  2. 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を送る必要がある。

context内のspawnされたmachineの場合
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 })
    })
  }
}
invokeされたmachineの場合
{
  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があるのですごく参考になる。

超シンプルに作ったサンプル。

0
1
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
0
1