Skip to content
Version: XState v5

Setup

In XState version 5, you can now use the setup({ ... }) function to setup types and sources for your machines. This has many benefits:

  • Reduced boilerplate for strongly typing and providing named sources
  • More robust machine logic, as named sources are guaranteed to exist
  • Better type inference for actions, actors, guards, delays, context, events, etc.
  • Strongly-typed snapshot and done events for actors
  • Strongly-typed state values
  • Reusability of source logic

Example usage:

import { setup, assign } from 'xstate';

const machine = setup({
types: {
context: {} as { count: number },
events: {} as { type: 'inc' } | { type: 'dec' },
},
actions: {
increment: assign({
count: ({ context }) => context.count + 1,
}),
decrement: assign({
count: ({ context }) => context.count - 1,
}),
},
}).createMachine({
context: { count: 0 },
on: {
inc: { actions: 'increment' },
dec: { actions: 'decrement' },
},
});

Setting up types

Machine types should be setup in the types property of setup({ types }). This is where you can setup the types for your machine, including:

  • Types for context
  • Types for events, including event payloads
  • Types for input
  • Types for actions, including action params
  • Types for guards, including guard params
  • Types for actors

Migrating from createMachine

Migrating from bare createMachine({ ... }) to setup({ ... }).createMachine({ ... }) to create a machine is simple.

  1. Import setup instead of createMachine from 'xstate'
  2. Move types from createMachine(...) to setup(...)
  3. Move action, actor, guard, etc. sources from the 2nd argument of createMachine(config, sources) to setup({ ... })
import {
// createMachine
setup
} from 'xstate';

const machine =
setup({
types: { ... },
actions: { ... },
guards: { ... }
})
.createMachine({
// types: { ... }
}, /* { actions, guards, ... } */);

Extending setups

_ Since XState version 5.24.0_

The .extend() method allows you to progressively build up your setup by extending actions, guards, and delays. This enables patterns that weren't possible before with full type safety.

Why use .extend()?

Before .extend(), all actions, guards, and delays had to be defined in a single setup() call. With .extend(), you can:

  • Compose setups from multiple sources: Create base setups that can be extended for different domains or modules
  • Reference base sources in extended sources: Use base actions, guards, and delays within extended ones with full type safety
  • Chain extensions: Build up complex setups incrementally while maintaining type safety throughout

Basic usage

You can extend any setup by calling .extend() with additional actions, guards, or delays:

import { setup, enqueueActions } from 'xstate';

const baseSetup = setup({
actions: {
doSomething: () => {
console.log('Doing something');
},
},
guards: {
truthy: () => true,
},
});

const extendedSetup = baseSetup.extend({
actions: {
doSomethingElse: () => {
console.log('Doing something else');
},
},
guards: {
falsy: () => false,
},
});

const machine = extendedSetup.createMachine({
entry: [{ type: 'doSomething' }, { type: 'doSomethingElse' }], // ✅ Both actions are available
on: {
EV: {
guard: { type: 'truthy' }, // ✅ Base guard is available
actions: { type: 'doSomethingElse' },
},
},
});

Referencing base sources in extended sources

One of the powerful features of .extend() is the ability to reference base actions, guards, and delays within extended ones, with full type safety:

import { setup, enqueueActions, not, and, or, assign } from 'xstate';

const baseSetup = setup({
types: {
context: {} as { count: number },
},
actions: {
increment: assign({
count: ({ context }) => context.count + 1,
}),
},
guards: {
truthy: () => true,
isPositive: ({ context }) => context.count > 0,
},
});

const extendedSetup = baseSetup.extend({
guards: {
// ✅ Can reference base guard with not()
falsy: not('truthy'),

// ✅ Can combine base guards with and()
isPositiveAndTruthy: and(['isPositive', 'truthy']),

// ❌ TypeScript error: 'nonexistent' doesn't exist
// nonexistent: not('nonexistent'),
},
actions: {
// ✅ Can reference base actions in enqueueActions
incrementAndLog: enqueueActions(({ enqueue, check }) => {
enqueue('increment'); // ✅ Base action is available

if (check('isPositive')) {
// ✅ Extended guard is available
enqueue.raise({ type: 'POSITIVE' });
}

// ❌ TypeScript error: 'nonexistent' doesn't exist
// enqueue('nonexistent');
}),
},
});

const machine = extendedSetup.createMachine({
on: {
EV: {
guard: 'isPositiveAndTruthy', // ✅ Extended guard is available
actions: 'incrementAndLog',
},
},
});

Delays can also be extended and referenced in actions:

import { setup, raise } from 'xstate';

const baseSetup = setup({
delays: {
short: 10,
},
});

const extendedSetup = baseSetup.extend({
delays: {
medium: 100,
},
});

const machine = extendedSetup.createMachine({
initial: 'a',
states: {
a: {
entry: [
raise({ type: 'GO' }, { delay: 'short' }), // ✅ Base delay
raise({ type: 'GO' }, { delay: 'medium' }), // ✅ Extended delay
// ❌ TypeScript error: 'long' doesn't exist
// raise({ type: 'GO' }, { delay: 'long' }),
],
on: {
GO: 'b',
},
},
b: {
after: {
medium: 'c', // ✅ Extended delay available in after
},
},
c: {},
},
});