keajs / kea

Batteries Included State Management for React
https://keajs.org/
MIT License
1.94k stars 51 forks source link

[Question] Chain of Responsibility Pattern using custom Logic Builders? #160

Open Tbhesswebber opened 8 months ago

Tbhesswebber commented 8 months ago

I'm doing something admittedly weird in a side project and I ran into an issue that I managed to get around, but I don't understand the intent of custom LogicBuilders given the issue.

I have a multi-page, branching workflow that collects data in different ways depending on the path that you're on. You can think of the workflow as a directed, acyclic graph.

In order to support this, I have a top-level logic (let's call it SuperLogic) and then a couple of lower-level logics that are for a diverged path of the workflow (let's call them LogicA-LogicZ). I then wrote a function that returns a LogicBuilder calling other "core" builders (connect, listeners, etc) so that I could wire SuperLogic into each of LogicA-LogicZ via kea([connectSuperLogic({listenTo: "myAction", transformValues: transform})]) without SuperLogic needing to know anything about the dependent logics.

All of this seemed pretty straight-forward until I checked the UI and it seemed that my SuperLogic was never being connected to or updated. With only a little debugging, I realized that I could just directly invoke the core builders with the logic passed into the logic builder. Is this the expected way to handle this? Alternatively, I could do something like return an array of core builder calls instead of returning a LogicBuilder, but that seemed like it "isn't the kea way".

Example (simplified):

export function connectSuperLogic<T extends Logic = Logic>({listenTo, transformValues}: ConnectionConfig) {
  return (logic) => {
    connect(superLogic)(logic);
    listeners(({values}) => (
      {[listenTo]: superLogic.actions.grabSubForm(transformValues(values))}
    ))(logic);
  };
}
mariusandra commented 7 months ago

Hey, one thing I immediately see that in the provided code, the listener should be a function, not calling the action directly:

export function connectSuperLogic<T extends Logic = Logic>({listenTo, transformValues}: ConnectionConfig) {
  return (logic) => {
    connect(superLogic)(logic);
    listeners(({values}) => (
      {[listenTo]: () => superLogic.actions.grabSubForm(transformValues(values))}
      // added "() =>" above 👈 
    ))(logic);
  };
}

However since this is a contrived example, I'll assume it's a typo in simplification.

Regarding:

Is this the expected way to handle this?

Yeah, I'd say so. The code looks good to me. Each standard function like actions({}) just returns a logic => {} function, which gets executed when the logic is built. You're also returning a builder with the same shape, and using it to modify the logic in question, which is correct.

The pattern of listeners(...)(logic) is also correct, and documented here: https://keajs.org/docs/meta/kea#logic-builders

Tbhesswebber commented 7 months ago

Awesome, thanks for verifying! And yes, that was a typo in my simplification of things.

Re: listeners(...)(logic) - It might be useful to make the documentation a bit more explicit there rather than having it hidden at the end of the code block - I missed it entirely and ended up debugging to figure out how things are called. Happy to make a PR with the changes I would make if you want to see an example