javascriptxstate

XState action is not called for `machine.transition()`


I have a simple machine:

import { createMachine } from 'xstate'

export const machine = createMachine(
  {
    predictableActionArguments: true,
    schema: {
      context: {} as { elapsed: number },
      events: {} as { type: 'AT_SAFE_ZONE' } | { type: 'WALKING' },
    },
    initial: 'a',
    states: {
      a: {
        on: {
          WALKING: {
            target: 'b',
          },
        },
      },
      b: {
        on: {
          AT_SAFE_ZONE: {
            target: 'a',
          },
        },
        exit: ['calculateWalk'],
      },
    },
  },
  {
    actions: {
      calculateWalk: async (context, event) => {
        console.log('>>> calculateWalk', context, event)
      },
    },
  }
)

And when I use it:

const { initialState } = machine

const nextState = machine.transition(initialState, { type: 'WALKING' })

console.log(nextState.value)

const nextState2 = machine.transition(nextState, { type: 'AT_SAFE_ZONE' })

I can see the states changing from a to b then to a again, but the action is never called.

Context: I'm testing the machine on vitest, I can see all logs except from the function calculateWalk. I tried many things: with and without array of actions, inside and outside of the "on" block... Nothing worked.


Solution

  • The transition function of a FSM can be understood as a pure function that takes in some state and an event and computes the next state as a return value if the machine defines a transition for this combination of input state and event. It's like a mathematical calculation and truthful to the formal definition of a finite state machine.

    const nextState = machine.transition('some state', { type: 'SOME_EVENT' })
    

    It's the same in XState: the machine's transition function is a pure function that computes the next State object (see State API) without performing any actions[1] of the machine. While the old XState docs show some examples using this function, it's rarely used in practice.

    Instead, in most cases you run the machine as a service which also executes its actions. You can do this with the interpret function or helper functions like useMachine() from one of the framework-specific packages and then sending events to the machine with send().

    import { createMachine, interpret } from 'xstate';
    
    const machine = createMachine({/* your machine definition */});
    
    const actor = interpret(machine).start();
    
    actor.send({ type: 'WALKING' })
    actor.send({ type: 'AT_SAFE_ZONE' })
    

    [1] By the way, createMachine() runs the entry action of the initial state if an entry action is defined.