Guards (Conditional Transitions)

Many times, you'll want a transition between states to only take place if certain conditions on the state (finite or extended) or the event are met. For instance, let's say you're creating a statechart for a search form, and you only want search to be allowed if:

  • the user is allowed to search (.canSearch in this example)
  • the search event query is not empty.

This is a good use case for a "transition guard", which determines if a transition can occur given the state and the event. A guard is a function defined on the cond property that takes 2 arguments:

and returns either true or false, which signifies whether the transition should be allowed to take place:

import { Machine } from 'xstate';

const searchMachine = Machine({
  id: 'search',
  initial: 'idle',
  context: {
    canSearch: true
  },
  states: {
    idle: {
      on: {
        SEARCH: {
          target: 'searching',
          // only transition to 'searching' if cond is true
          cond: (ctx, event) => {
            return ctx.canSearch && event.query && event.query.length > 0;
          }
        }
      }
    },
    searching: {
      onEntry: 'executeSearch'
      // ...
    },
    searchError: {
      // ...
    }
  }
});

If the cond guard returns false, then the transition will not be selected, and no transition will take place from that state node.

Example of usage with context:

import { interpret } from 'xstate/lib/interpreter';

const searchService = interpret(searchMachine)
  .onTransition(state => console.log(state.value))
  .start();

searchService.send({ type: 'SEARCH', query: '' });
// => 'idle'

searchService.send({ type: 'SEARCH', query: 'something' });
// => 'searching'

Multiple Guards

If you want to have a single event transition to different states in certain situations you can supply an array of conditional transitions.

For example, you can model a door that listens for an OPEN event, and opens if you are an admin and errors if you are not:

import { Machine, actions } from 'xstate';
import { interpret } from 'xstate/lib/interpreter';
const { assign } = actions;

const doorMachine = Machine({
  id: 'door',
  initial: 'closed',
  context: {
    isAdmin: false
  },
  states: {
    closed: {
      initial: 'idle',
      states: {
        idle: {},
        error: {}
      },
      on: {
        SET_ADMIN: assign({ isAdmin: true }),
        OPEN: [
          { target: 'opened', cond: ctx => ctx.isAdmin },
          { target: 'closed.error' }
        ]
      }
    },
    opened: {
      on: {
        CLOSE: 'closed'
      }
    }
  }
});

const doorService = interpret(doorMachine)
  .onTransition(state => console.log(state.value))
  .start();
// => { closed: 'idle' }

doorService.send('OPEN');
// => { closed: 'error' }

doorService.send('SET_ADMIN');
// => { closed: 'error' }
// (state does not change, but context changes)

doorService.send('OPEN');
// => 'opened'
// (since ctx.isAdmin === true)

Notes:

  • The cond function must always be a pure function that only references the context and event arguments.
  • ⚠️ Warning: do not overuse guard conditions. If something can be represented discretely as two or more separate events instead of multiple conds on a single event, it is preferable to avoid cond and use multiple types of events instead.