Xstate: Delayed Events not working

Created on 25 Feb 2020  路  9Comments  路  Source: davidkpiano/xstate

I'm trying to use Delayed Events
https://xstate.js.org/docs/guides/delays.html#delayed-events

I defined actions:

actions: {
        reachedLevel: context => {
          const { level, chosenLevel } = context;
          if (level === chosenLevel) {
            send('REACHED');
          }
        },
        errorPressDirection: _context => {
          //===== working
          // setTimeout(() => {
          //   return send('REACHED');
          // }, 1000);

          //===== working
          //send('REACHED')

          // Not working
          send('REACHED', { delay: 1000 });
        },
      },

If I use either the setTimeout or only send the type the code work well

setTimeout(() => {
     return send('REACHED');
}, 1000);

Or

 send('REACHED');

But send with option delay. This event is not running the REACHED

send('REACHED', { delay: 1000 });

I don't know is this my approach is wrong or delayed-events is not working.
I'm using version 4.7.8

Give me advice.
Thanks so much.

All 9 comments

The send(...) function is an action creator; it is a pure function that only returns an action object and does not imperatively send an event.

https://xstate.js.org/docs/guides/actions.html#send-action

Also can you share the full code? I'm curious as to how it's working, as the send() action creator doesn't do anything, it only returns an object.

Hi @davidkpiano

This is my elevator Machine. Can you help me take a look at actions: errorPressDirection and activities: moving

Thanks so much.

const elevatorMachine = Machine(
    {
      id: 'elevator',
      initial: 'stop',
      context: {
        level: 1,
        chosenLevel: 1,
        direction: '',
        isDisablePressLevelBtn: true,
        doorStatus: 'close',
      },
      states: {
        stop: {
          // current is stop the transition only can up or down
          on: {
            UP: {
              target: 'up',
              actions: assign({
                direction: (context, event) => 'up',
                isDisablePressLevelBtn: false,
                doorStatus: 'open',
              }),
            },
            DOWN: {
              target: 'down',
              actions: assign({
                direction: (context, event) => 'down',
                isDisablePressLevelBtn: false,
                doorStatus: 'open',
              }),
            },
          },
        },
        down: {
          // current is down the transition only can up or stop
          id: 'elevator_down',
          on: {
            PRESS_LEVEL: [
              {
                target: 'moving.go_down',
                cond: 'pressLevelDownValidate',
                actions: assign({
                  chosenLevel: (context, event) => +event.value,
                  doorStatus: 'close',
                }),
              },
              {
                target: 'error.go_down',
                actions: assign({
                  chosenLevel: (context, event) => +event.value,
                  doorStatus: 'close',
                }),
              },
            ],
          },
        },
        up: {
          // current is up the transition only can down or up
          id: 'elevator_up',
          on: {
            PRESS_LEVEL: [
              {
                target: 'moving.go_up',
                cond: 'pressLevelUpValidate',
                actions: assign({
                  chosenLevel: (context, event) => +event.value,
                  doorStatus: 'close',
                }),
              },
              {
                target: 'error.go_up',
                actions: assign({
                  chosenLevel: (context, event) => +event.value,
                  doorStatus: 'close',
                }),
              },
            ],
          },
        },
        error: {
          states: {
            go_up: {
              entry: 'errorPressDirection',

              on: {
                REACHED: {
                  target: '#elevator_moving.go_down',
                  actions: assign({
                    direction: (context, event) => 'down',
                  }),
                },
              },
            },
            go_down: {
              entry: 'errorPressDirection',

              on: {
                REACHED: {
                  target: '#elevator_moving.go_up',
                  actions: assign({
                    direction: (context, event) => 'up',
                  }),
                },
              },
            },
          },
        },
        moving: {
          id: 'elevator_moving',
          initial: 'finished',
          states: {
            go_up: {
              activities: ['moving'],
              entry: 'reachedLevel',
              on: {
                REACHED: 'reached',
                MOVING: {
                  target: 'go_up',
                  actions: assign({
                    level: (context, event) => event.value + 1,
                  }),
                  cond: {
                    type: 'isMoveUp',
                  },
                },
              },
            },
            go_down: {
              activities: ['moving'],
              entry: 'reachedLevel',
              on: {
                REACHED: 'reached',
                MOVING: {
                  target: 'go_down',
                  actions: assign({
                    level: (_context, event) => event.value - 1,
                  }),
                  cond: {
                    type: 'isMoveDown',
                  },
                },
              },
            },
            reached: {
              entry: assign({
                doorStatus: 'open',
              }),
              after: {
                1000: 'finished'
              }
            },
            finished: {
              type: 'final',
            },
          },
          onDone: {
            target: 'stop',
            actions: assign({
              direction: (_context, _event) => '',
              isDisablePressLevelBtn: true,
              doorStatus: 'close',
            }),
          },
        },
      },
    },
    {
      actions: {
        reachedLevel: context => {
          const { level, chosenLevel } = context;
          if (level === chosenLevel) {
            send('REACHED');
          }
        },
        errorPressDirection: context => {
          // setTimeout(() => {
          //   return send('REACHED');
          // }, 1000);
          send('REACHED', {
            delay: 1000
          });
        },
      },
      activities: {
        moving: (context, _event) => {
          const { level } = context;
          send('MOVING', {
            value: level,
            delay: 1000
          });
          // setTimeout(() => {
          //   return send({
          //     type: 'MOVING',
          //     value: level,
          //   });
          // }, 1000);
        },
      },
      guards: {
        pressLevelDownValidate: (context, event, { cond }) => {
          const chosenLevel = event.value;
          const { level } = context;

          return chosenLevel < level;
        },
        pressLevelUpValidate: (context, event, { cond }) => {
          const chosenLevel = event.value;
          const { level } = context;

          return chosenLevel > level;
        },
        isMoveUp: (context, event, { cond }) => {
          const { level, chosenLevel } = context;

          return level < chosenLevel;
        },
        isMoveDown: (context, event, { cond }) => {
          const { level, chosenLevel } = context;

          return chosenLevel < level;
        },
      },
    },
  );

Hi @davidkpiano,
I really want to hear your opinion.

Please be patient - you can't expect to always get a response immediately. I bet David has seen your previous post or will get to it when he can. We have lives and we do this in our free time.

@Andarist yah, I know. I'm asking like a normal way. I don't rush. I just want to let him know that I shared the code and wanted to hear his opinion about my approach. It does not mean I'm asking he have to answer asap and I don't mean disturb his life. The difference timezone is one more reason.
And I apologize if either you or he feel frustrating

@mymai91 Great elevator example, can you put this in a CodeSandbox?

Also, the activity should be considered fire-and-forget, so what you really want here is to invoke a service:

      services: {
        moving: (context, _event) => (callback) => {
          const { level } = context;
          callback('MOVING', {
            value: level,
            delay: 1000
          });
        },
      },

And invoke it this way:

states: {
   go_up: {
     invoke: ['moving'],
// ...

And that reachedLevel should be most likely refactored to a guarded transient transition, smth like:

on: {
  '': { cond: 'reachedLevel', target: 'reached' }
}

@davidkpiano
Thanks for your answer. I think, invoke a service is exactly what I need but the only thing is it might not support delay (I read the document and see that https://xstate.js.org/docs/guides/communication.html#invoking-callbacks)

I still using setTimeout for my purpose because I want the level_value slowdown transition 1 second.
This is my demo: https://codesandbox.io/s/thirsty-rain-gxgo0

Andarist: Thanks for your comment. I tried refactored as you suggest but it's not working.

Thanks so much for helping me solve my problem. ^___^

@mymai91 No problem; feel free to ask in spectrum.chat/statecharts so we can help you further.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bradwoods picture bradwoods  路  3Comments

mattiamanzati picture mattiamanzati  路  3Comments

suku-h picture suku-h  路  3Comments

dakom picture dakom  路  3Comments

hnordt picture hnordt  路  3Comments