Xstate: Is sending event from within an action considered anti-pattern?

Created on 22 Apr 2019  路  10Comments  路  Source: davidkpiano/xstate

See code sample below, three questions:

  1. Is sending event from within an action an anti-pattern?

  2. If yes, any recommended approach to trigger a state switch from within an action?

  3. If not, is there better way to pass in the send function, or make it available from within the action? Maybe by embedding it inside the context?

states: {
  a1: {...},
  a2: {
    on: { 
      'CLICK': {
        actions: assign((ctx, e) => {
          // using useMachine() outside and pass in `send` function via the click event
          const { send } = e
          // send another event from with-in the action
          send({type: 'SCROLL', value: 123})
        })  
      },
      'SCROLL': 'a1'
    }
  },
}
documentation question

Most helpful comment

Invoked services are indeed all I needed to implement optimistic update, thanks for the examples!! 馃憤

All 10 comments

Is sending event from within an action an anti-pattern?

Yes - actions should be fire-and-forget side-effects.

If yes, any recommended approach to trigger a state switch from within an action?

Do you have a real-life example of what you're trying to do? Why does it have to be from within an action in your case?

Yes, I do have a real use case for that, in short I'm trying to implement optimistic update where I need to update the context multiple times, as show in the code sample below (as you can see I'm kind of using send as dispatch in a redux manner).

and here's my updated two questions:

1) Why actions need to be fire and forget? What if it's not?

2) What's the downside of sending multiple events as demoed below? Is there better approach to do that?

// let's say we are in a `create new item` window
// user just filled the form and hit `send` button
// and while we are persisting that new item via an API, 
// we also want to switch from `newItem` state back to `master` state
// here's how it goes:
actions: assign((ctx, e) => {
  const { payload, send } = e
  const newItem = { ...payload, id: randomId() }

  // drive fsm into 'master' state, along with the newly-created item
  // so that we can display this new item on screen, 
  // even though it's still on the wire back to the server
  send({
    type: 'GOTO_MASTER',
    payload: newItem,
  })

  // invoke API
  fetch(...)

  // and if the invokation was successful and data persisted
  // we want to display some notifications on the screen
  // we may also wanto to persist the result returned from the API in the context
  .then( result => {
    send({
      type: 'NEW_ITEM_SUCCESS',
      result
    })
  })

  // of course if the invokation failed, we need to show an alert
  // which brings us into another state
  .catch( error => {
    send({
      type: 'NEW_ITEM_FAIL',
      error,
    })
  })

})

Side-effects that communicate back with the machines are invoked services (e.g., callbacks, Promises, etc.) Because it's an optimistic UI, it becomes a little tricky, as you are in some 'newItem.updating' as well as some 'view.master' state at the same time, so if you want to explicitly model this in the same state machine, you'd need parallel states.

However, using a service might be easier:

const appMachine = Machine({
  id: 'app',
  initial: 'master',
  invoke: {
    id: 'itemsService',
    src: () => (cb, onReceive) => {
      onReceive(event => {
        if (event.type === 'NEW_ITEM') {
          fetch(...)
            .then(result => cb({ type: 'NEW_ITEM_SUCCESS', result }))
            .catch(error => cb({ type: 'NEW_ITEM_FAIL', error }));
        }
      }
    }
  }
  states: {
    master: { ... },
    newItem: {
      on: {
        SUBMIT_NEW_ITEM: {
          // go back to master
          target: 'master',
          // send intent to service
          actions: send((_, e) => ({
            type: 'NEW_ITEM', 
            data: e.data
          }, { to: 'itemsService' })
        }
      }
    }
  }
});

The difference between this and an "action that can send back events" is that 'itemsService' is a single instance of a managed service, and it lets you implement more advanced logic like throttling, keeping track of the items, adding more features (updating and deleting items), teardown (abort in-flight Promises), etc.

It's basically a mini API / "microservice" that you can even swap out for testing.

The spawning Actors proposal #428 can also be used for this, once it's in:

const createItem = (data) => Machine({
  id: 'item',
  context: data,
  invoke: {
    src: (ctx) => fetch(...),
    onDone: 'created',
    onError: 'error'
  },
  states: {
    // ...
  }
});

const appMachine = Machine({
  // ...
  on: {
    SUBMIT_NEW_ITEM: { actions: spawn(createItem) },
    NEW_ITEM_SUCCESS: { ... },
    NEW_ITEM_FAIL: { ... }
  }
});

Invoked services are indeed all I needed to implement optimistic update, thanks for the examples!! 馃憤

@davidkpiano This pattern is exactly what I'm trying to implement; however, I'm struggling to figure out how to use the send function the way you wrote in your example:

          actions: send((_, e) => ({
            type: 'NEW_ITEM', 
            data: e.data
          }, { to: 'itemsService' })

This sample doesn't appear to be valid JS because you're implicitly returning two separate values. Is there another way to use the send function in this manner (i.e., explicitly specifying the receiver and including the event data in the action)?

Side-effects that communicate back with the machines are invoked services (e.g., callbacks, Promises, etc.) Because it's an optimistic UI, it becomes a little tricky, as you are in some 'newItem.updating' as well as some 'view.master' state at the same time, so if you want to explicitly model this in the same state machine, you'd need parallel states.

However, using a service might be easier:

const appMachine = Machine({
  id: 'app',
  initial: 'master',
  invoke: {
    id: 'itemsService',
    src: () => (cb, onReceive) => {
      onReceive(event => {
        if (event.type === 'NEW_ITEM') {
          fetch(...)
            .then(result => cb({ type: 'NEW_ITEM_SUCCESS', result }))
            .catch(error => cb({ type: 'NEW_ITEM_FAIL', error }));
        }
      }
    }
  }
  states: {
    master: { ... },
    newItem: {
      on: {
        SUBMIT_NEW_ITEM: {
          // go back to master
          target: 'master',
          // send intent to service
          actions: send((_, e) => ({
            type: 'NEW_ITEM', 
            data: e.data
          }, { to: 'itemsService' })
        }
      }
    }
  }
});

The difference between this and an "action that can send back events" is that 'itemsService' is a single instance of a managed service, and it lets you implement more advanced logic like throttling, keeping track of the items, adding more features (updating and deleting items), teardown (abort in-flight Promises), etc.

It's basically a mini API / "microservice" that you can even swap out for testing.

@pdcarroll Please put your code in a CodeSandbox. Easier to diagnose the issue there.

@pdcarroll Please put your code in a CodeSandbox. Easier to diagnose the issue there.

Sure, here's a sandbox with a modified version of the sample you posted above:
https://codesandbox.io/s/laughing-margulis-zd4yh

I included your sample implementation of send (line 29), but commented it out because it's invalid syntax. The version of send that I wrote is valid (and works), but it doesn't capture the event data. My question is, is there a way to send the action to the invoked service, including the event data, as you attempted to illustrate in your version of send?

This sample doesn't appear to be valid JS because you're implicitly returning two separate values. Is there another way to use the send function in this manner (i.e., explicitly specifying the receiver and including the event data in the action)?

That's because you have made a syntax mistake, it should be:

          actions: send(
            (_, e) => ({
              type: "NEW_ITEM",
              data: e.data
            }),
            { to: "itemsService" }
          )

https://codesandbox.io/s/delicate-https-ujfyh

@Andarist The invalid syntax was not my code, it was from the sample posted as a response to the original issue. That's why I asked for clarification. The change you made is exactly what I was looking for. Thank you!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

doup picture doup  路  3Comments

bradwoods picture bradwoods  路  3Comments

hnordt picture hnordt  路  3Comments

ifokeev picture ifokeev  路  3Comments

dakom picture dakom  路  3Comments