mattpocock / xstate-stream-demo-app

A full-stack app built with Next.js, XState, Typescript and GraphQL.
11 stars 2 forks source link

Parallel or Promise.all that's the question #1

Open RainerAtSpirit opened 3 years ago

RainerAtSpirit commented 3 years ago

https://github.com/mattpocock/xstate-stream-demo-app/blob/cb6d8d9ddceeb1e6e33522e0310b69b53974ff18/src/machines/paymentWizard.machine.ts#L106

I was pretty sure that I was running into this before, see https://github.com/davidkpiano/xstate/issues/359. I'd say it's a great educational example of how to use parallel states correctly. However, in the real world, the question would be if we gain any benefits or if an invoke Promise.all could be used instead. In this case, I'm leaning to the second as it makes the machine and its visualization easier.

mattpocock commented 3 years ago

@RainerAtSpirit Sorry dude, missed this!

How about this - what if you needed to represent the loading states of the two promises differently? I.e. what if you needed to show in your UI which one failed/which succeeded? Or show how long each one had taken?

Promise.all could cover quite a few cases where these requirements weren't needed, but a parallel machine would let you handle it all.

Agree in general though, if I were implementing it myself and I wasn't going to need the UI to show the state of the promises, I'd just use Promise.all

RainerAtSpirit commented 3 years ago

@mattpocock No worries.

Just for the sake of argument. If the use case requires independent handling, then most probably I'd reach out for the actor model. e.g. master gets config data and based on the that spawns multiple actors that gets additional data. That way each DetailView component would become independent.

// contrived partial example
...
context: {
     ...
       parentId: '123',
      configs: [],
      actorRefs: [],
      },
states: {
        init: {
          invoke: {
            src: 'getConfigs',
            onDone: {
              target: 'done',
              actions: assign({
                configs: (context, event) => {
                  return event.data
                },
              }),
            },
            onError: {
              target: 'error',
            },
          },
        },
        done: {
          entry: assign((context: any, event) => {
            const refs = context.configs.map((config: any) => {
              return spawn(createListViewMachine(config, context.projectId))
            })

            return {
              actorRefs: refs,
            }
          }),
        },

// usage with react

export default function App() {
  const [current, send] = useMachine(myMachine, { devTools: true })

  return (
    <div className="App">
      <h1>ProjectId {current.context.projectId}</h1>
      <h2>{current.value}</h2>

      {current.context.actorRefs.map((ref: any) => (
        <DetailView key={ref.id} service={ref}></DetailView>
      ))}
    </div>
  )
}