4

I'm completely new to Xstate and I'm struggling to find help inside the official documentation.

The problem is pretty easy, I'd like to know if an event is triggered when is not suppose to.

I have a basic workflow that is very strict in terms of transitions, for instance, my state can't go from 'pending' to 'processed' without passing from 'uploaded'.

If I use:

stateService.send('PROCESSED') 

while the state is in 'pending', the state doesn't change ( correct ) but is there any utility or event in Xstate that actually tells me that the transaction was not fired since the event was not allowed/correct?

This is my state

const stateMachine = Machine(
  {
    id: 'vocalTrack',
    initial: 'PENDING',
    context: {},
    states: {
      PENDING: {
        on: {
          UPLOADED: 'UPLOADED',
        },
      },
      UPLOADED: {
        on: {
          PROCESSED: 'PROCESSED',
        },
        entry: 'onUploaded',
      },
      PROCESSED: {
        on: {
          READY: 'READY',
          ERROR: 'ERROR',
        },
        exit: 'onProcessed',
      },
      READY: {
        type: 'final',
      },
      ERROR: {
        on: {
          UPLOADED: 'UPLOADED',
        },
        type: 'final',
      },
    },
  },
  {
    actions: {
      onUploaded: (context, event) => {
        console.log(`++++ onUploaded action: ++++`)
      },
      onProcessed: (context, event) => {
        console.log(`++++ onProcessed action: ++++`)
      },
    },
  },
)

const stateService = interpret(stateMachine)
stateService.init('PENDING')

// I'd like to catch the following workflow errors
stateService.send('UPLOADED')
stateService.send('PROCESSED')
Dario
  • 755
  • 1
  • 7
  • 19

3 Answers3

3

There is no option you can pass to the machine or interpret call that will give you this for "free". You can easily add some logic to the "onTransition" method though:

Option 1, validate on state.changed:

    service.onTransition(state => {
        if (state.changed) {
            console.log(`The state did change, so ${state.event.type} caused a valid transition`);
        } else {
            console.log(`${state.event.type} did not cause a state change`);
        }
    })

Option 2, validate based on state.value & event name:

    service.onTransition(state => {
        if (state.event.type === 'xstate.init'){
            console.log('Init Transition');
            return;
        }
        if(state.value === state.event.type){
            console.log('Transition Valid');
        }else {
            console.log('Transition Invalid');
        }
    })

Option 1 would probably be the preferred solution! You can read more about it here in the documentation. The following SO question is also related in terms of events that don't exist at all.

I've created a small demo application in react to demonstrate this:

Edit cranky-silence-934uu

TameBadger
  • 1,580
  • 11
  • 15
-1

It is a solved issue, but I believe there is also a different one, to handle it on the machine configuration level. So I will post it for comparison.

Taking into consideration that:

you can define PROCESSED event on the whole machine configuration level:

const stateMachine = Machine(
    {
        id: "vocalTrack",
        initial: "PENDING",
        context: {},
        on: {
            PROCESSED: {
                actions: ["invalidCall"]
            }
        },
        states: {
            PENDING: {
                on: {
                    UPLOADED: "UPLOADED"
                }
            },
            UPLOADED: {
                on: {
                    PROCESSED: "PROCESSED"
                },
                entry: "onUploaded"
            },
            ...
        },
    }, {
        actions: {
            ...
            invalidCall: (context, event, meta) => {
                console.log(
                `invalid call of event ${event.type} on the ${meta.state.value} state`
                );
            }
        }
    }
);

and in effect, in every state where the PROCESSED event is not handled, it will propagate up to the main configuration level.

Example on codesandbox

What's more, a wildcard descriptor exists and will capture all events, that is not defined at a given level, so we might as well capture all triggered events that are not supported in a given state with:

on: {
    "*": {
        actions: ["invalidCall"]
    }
},

Of course, it all depends on your particular case, but what I like in that solution, is that it all stays on the machine definition level.

m.cekiera
  • 5,365
  • 5
  • 21
  • 35
-1

I also managed to get this working with the following:

export const stateMachine = createMachine({
  id: 'stateMachine',
  initial: 'created',
  strict: true,
  on: {
    '*': {
      cond: (_context, _event, meta) => {
        throw new Error(`Cannot change from ${meta.state.value}`);
      },
    },
  },
  states: {
    created: {
      on: {
Owen Ben Davies
  • 259
  • 2
  • 8