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

# 問題

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

# 解決方法

- [spawn][1]
- [send][2]
- [sendParent][3]
- [pure][4]
- [sendUpdate][5]

## 親machineに子machineを持たせる

やり方は

1. contextに直接保持しておく
1. invokeでsrcにmachineを当てる

だけだと思う。

### contextに保持

```ts
interface Context{
  child: ActorRefFrom<typeof childMachine>
}

...
{
  states: {
    idle: {
      entry: ["setChild"] // どこでもどのタイミングでも問題ない
    }
  }
},
{
  actions: {
    setChild: assign((context, event) => ({...context, child: spawn(childMachine)}))
  }
})
``` 

ポイントは`spawn`を使って、contextに置いておくこと。contextに置くので、contextを更新するまでずっと存在する。
注意点としてはassignする際に

```ts
assign((context, event) => ({ ...context, child: spawn(...) }))
```

```ts
assign({
  child: (context, event) => spawn(...)
})
```

としなければ正常動作しない。

```ts

assign({
  child: spawn(...) // ❌
})
```

こうするとエラーも何も出ず、動かない。

[![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16757/e6a02ea5-2b38-5c34-0220-3e63bdadd100.png)][1]

### machineをinvokeする

```ts
{
  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を送る必要がある。

```ts: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 })
    })
  }
}
```

```ts: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を生成して、それを配列として返してやる。

```ts
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を投げる場合

```ts
import { sendParent, ... } from "xstate"

{
  on: {
    "SOME.EVENT": {
      actions: [sendParent("EVENT.TO.PARENT")]
    }
  }
}
```

## spawnのsyncやautoForward

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16757/86c9bc65-1ad0-4b58-f3de-149b295bc07b.png)

### sync

親から子をspawnする際に、子のstateが変更する度に親に通知をするかどうか。子のstateがAからBに変わると親の`parentService.onTransition((state) => ...)`が反応するようになる。（多分`const [state, send] = useMachine(parentMachine)`も）

これだと反応が多すぎるって言う場合は、syncは使わずsendUpdateを子の要所要所で使えば、的確に親に通知が出来る。

```ts:子
{
  ...
  states: {
    idle: {},
    stateA: {
      entry: [sendUpdate()]
    },
  }
}
```

### autoForward

は親で受けたeventをそのまま子にも流すかどうか。clickイベントのイベントバブリング的な。
親子関係がよっぽど蜜で無い限りは使わなそう。

# 参考

https://www.youtube.com/watch?v=NTfPtYJORck

https://www.youtube.com/watch?v=7erJ1DV_Tlo

https://github.com/msurdi/videittous/

invokeしたmachineでの親子関係 https://github.com/msurdi/videittous/tree/main/src/machines にmachineがあるのですごく参考になる。

https://codesandbox.io/s/machinenoqinziguanxisanpuru-zzjd8

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


[1]: https://xstate.js.org/docs/guides/actors.html#spawning-actors
[2]: https://xstate.js.org/docs/guides/actions.html#send-action
[3]: https://xstate.js.org/docs/guides/communication.html#invoking-machines
[4]: https://xstate.js.org/docs/guides/actions.html#pure-action
[5]: https://xstate.js.org/docs/guides/actors.html#sending-updates