[xstate] machine간에 부모 - 자식 관계 (트리)로 state, event를 관리하는 방법

문제



모델을 xstate에 떨어뜨려 가면, 아무래도 machine을 분할해, 1개의 machine가 다른 machine를 관리해 전체로서의 state를 관리하고 싶은 것이 나온다. (예: one-to-many, many-to-many)

해결 방법


  • spawn
  • send
  • sendParent
  • pure
  • sendUpdate

  • 부모 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(...) // ❌
    })
    

    이렇게 하면 에러도 아무것도 나오지 않고, 움직이지 않는다.



    기계를 invoke


    {
      states: {
        idle: {
          invoke: {
            id: "childMachineId",
            src: childMachine,
            onDone: { ... }
          }
        }
      }
    }
    

    invoke이므로, invoke된 state내에서만 존재하고 있다. 용도로는 통상의 invoke service로 해서는 복잡하고, machine에 떨어뜨려 버리는 편이 간결하게 정리되는 때에 편리.

    events



    부모로부터 아이에게 이벤트를 던지는 법



    send를 사용한다. send는 일반적인 invoke service를 향해 event를 던지기도 하지만, 부모와 자식 관계가 있는 machine이면 부모로부터 아이에게 event를 던질 수도 있다.
    event를 던질 때에는 아이가 context내에 들어 있다면 그 객체(또는 spawn시의 이름), invoke된 machine이면 그 ID를 지정해 줄 필요가 있다.

    주의점으로서 action 안에서. 반드시 context.child.send(...)의 send를 통해 이벤트를 보내야합니다.

    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 하고 싶은 경우


    import { send } from "xstate" 에 쓰려고 하면 쓸 수 없지만 코드가 번잡하기 때문에 actions: [...] 를 사용한다.
    각 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 가 변경할 때마다 부모에게 통지를 하는지 어떤지. 아이의 상태가 A에서 B로 바뀌면 부모 actions.pure가 반응합니다. (아마 parentService.onTransition((state) => ...) 음)

    이것이라고 반응이 너무 많다고 하는 경우는, sync는 사용하지 않고 sendUpdate를 아이의 요소요소로 사용하면, 정확하게 부모에게 통지를 할 수 있다.

    아이
    {
      ...
      states: {
        idle: {},
        stateA: {
          entry: [sendUpdate()]
        },
      }
    }
    

    autoForward



    는 부모로 받은 event를 그대로 아이에게도 흘릴지 어떨지. click 이벤트의 이벤트 버블링적인.
    부모와 자식 관계가 좀처럼 꿀이 아닌 한은 사용하지 않는 것.

    참고





    invoke한 machine에서의 부모와 자식 관계 htps : // 기주 b. 코 m / m r r / ゔ ぃ에서 어쨌든 s / t ree / 마이 / src / 마을 s 에 machine이 있습니다.

    매우 간단하게 만든 샘플.

    좋은 웹페이지 즐겨찾기