Stately
Get Started

Migrating from XState v4 to v5

The guide below explains how to migrate from XState version 4 to version 5. Migrating from XState v4 to v5 should be a straightforward process. If you get stuck or have any questions, please reach out to the Stately team on our Discord.

This guide is for developers who want to update their codebase from v4 to v5 and should also be valuable for any developers wanting to know the differences between v4 and v5.

XState v5 and TypeScript

XState v5 and its related libraries are written in TypeScript, and utilize complex types to provide the best type safety and inference possible for you. XState v5 requires TypeScript version 5.0 or greater. For best results, use the latest TypeScript version.

Follow these guidelines to ensure that your TypeScript project is ready to use XState v5:

  • Use the latest version of TypeScript, version 5.0 or greater (required)

    npm install typescript@latest --save-dev
  • Set strictNullChecks to true in your tsconfig.json file. This will ensure that our types work correctly and will also help catch errors in your code (strongly recommended)

    // tsconfig.json
    {
      compilerOptions: {
        // ...
        strictNullChecks: true,
        // or set `strict` to true, which includes `strictNullChecks`
        // "strict": true
      },
    }
  • Set skipLibCheck to true in your tsconfig.json file (recommended)

Creating machines and actors

Use createMachine(), not Machine()

Breaking change

The Machine(config) function is now called createMachine(config):

import {  } from 'xstate';

const  = ({
  // ...
});
// ❌ DEPRECATED
import { Machine } from 'xstate';

const machine = Machine({
  // ...
});

Use createActor(), not interpret()

Breaking change

The interpret() function has been renamed to createActor():

import { createMachine, createActor } from 'xstate';

const machine = createMachine(/* ... */);

// ✅
const actor = createActor(machine, {
  // actor options
});
import { createMachine, interpret } from 'xstate';

const machine = createMachine(/* ... */);

// ❌ DEPRECATED
const actor = interpret(machine, {
  // actor options
});

Use machine.provide(), not machine.withConfig()

Breaking change

The machine.withConfig() method has been renamed to machine.provide():

// ✅
const specificMachine = machine.provide({
  actions: {
    /* ... */
  },
  guards: {
    /* ... */
  },
  actors: {
    /* ... */
  },
  // ...
});
// ❌ DEPRECATED
const specificMachine = machine.withConfig({
  actions: {
    /* ... */
  },
  guards: {
    /* ... */
  },
  services: {
    /* ... */
  },
  // ...
});

Set context with input, not machine.withContext()

Breaking change

The machine.withContext(...) method can no longer be used, as context can no longer be overridden directly. Use input instead:

// ✅
const machine = createMachine({
  context: ({ input }) => ({
    actualMoney: Math.min(input.money, 42),
  }),
});

const actor = createActor(machine, {
  input: {
    money: 1000,
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  context: {
    actualMoney: 0,
  },
});

const moneyMachine = machine.withContext({
  actualMoney: 1000,
});

Actions ordered by default, predictableActionArguments no longer needed

Breaking change

Actions are now in predictable order by default, so the predictableActionArguments flag is no longer required. Assign actions will always run in the order they are defined.

// ✅
const machine = createMachine({
  entry: [
    ({ context }) => {
      console.log(context.count); // 0
    },
    assign({ count: 1 }),
    ({ context }) => {
      console.log(context.count); // 1
    },
    assign({ count: 2 }),
    ({ context }) => {
      console.log(context.count); // 2
    },
  ],
});
// ❌ DEPRECATED
const machine = createMachine({
  predictableActionArguments: true,
  entry: [
    (context) => {
      console.log(context.count); // 0
    },
    assign({ count: 1 }),
    (context) => {
      console.log(context.count); // 1
    },
    assign({ count: 2 }),
    (context) => {
      console.log(context.count); // 2
    },
  ],
});

The spawn() function has been removed

Instead of using the imported spawn() function to create actors inside assign(...) actions:

  • Use the spawnChild(...) action creator (preferred)
  • Or use the spawn(...) method from the first argument passed to the assigner function inside of assign(...) actions (useful if you need the actor ref in context)

Read the documentation on spawning actors for more information.

// ✅
import { spawnChild, assign } from 'xstate';

// Spawning a direct child:
const machine1 = createMachine({
  // ...
  entry: spawnChild('someChildLogic', {
    id: 'someChild',
  }),
});

// Spawning a child with the actor ref in `context`:
const machine2 = createMachine({
  // ...
  entry: assign({
    child: ({ spawn }) => spawn('someChildLogic'),
  }),
});
// ❌
import { assign, spawn } from 'xstate';

const machine = createMachine({
  // ...
  entry: assign({
    child: () => spawn('someChildLogic'),
  }),
});

Use getNextSnapshot(…) instead of machine.transition(…)

The machine.transition(…) method now requires an "actor scope" for the 3rd argument, which is internally created by createActor(…). Instead, use getNextSnapshot(…) to get the next snapshot from some actor logic based on the current snapshot and event:

// ✅
import {
  createMachine,
  getNextSnapshot,
} from 'xstate';

const machine = createMachine({
  // ...
});

const nextState = getNextSnapshot(
  machine,
  machine.resolveState({ value: 'green' }),
  { type: 'timer' },
);

nextState.value; // yellow
// ❌
import { createMachine } from 'xstate';

const machine = createMachine({
  // ...
});

const nextState = machine.transition('green', { type: 'timer' });

nextState.value; // yellow

Send events explictly instead of using autoForward

The autoForward property on invoke configs has been removed. Instead, send events explicitly.

In general, it's not recommended to forward all events to an actor. Instead, only forward the specific events that the actor needs.

// ✅
const machine = createMachine({
  // ...
  invoke: {
    src: 'someSource',
    id: 'someId',
  },
  always: {
    // Forward events to the invoked actor
    // This will not cause an infinite loop in XState v5
    actions: sendTo('someId', ({ event }) => event),
  },
});
// ❌
const machine = createMachine({
  // ...
  invoke: {
    src: 'someSource',
    id: 'someId'
    autoForward: true // deprecated
  }
});

States

Use state.getMeta() instead of state.meta

Breaking change

The state.meta property has been renamed to state.getMeta():

// ✅
state.getMeta();
// ❌ DEPRECATED
state.meta;

The state.toStrings() method has been removed

Breaking change

import { type StateValue } from 'xstate';

export function getStateValueStrings(stateValue: StateValue): string[] {
  if (typeof stateValue === 'string') {
    return [stateValue];
  }
  const valueKeys = Object.keys(stateValue);

  return valueKeys.concat(
    ...valueKeys.map((key) =>
      getStateValueStrings(stateValue[key]!).map((s) => key + '.' + s),
    ),
  );
}

// ...

const stateValueStrings = getStateValueStrings(stateValue);
// e.g. ['green', 'yellow', 'red', 'red.walk', 'red.wait', …]

Use state._nodes instead of state.configuration

Breaking change

The state.configuration property has been renamed to state._nodes:

// ✅
state._nodes;
// ❌ DEPRECATED
state.configuration;

Read events from inspection API instead of state.events

The state.events property has been removed, because events are not part of state, unless you explicitly add them to the state's context. Use the inspection API to observe events instead, or add the event explicitly to the state's context:

// ✅
import { createActor } from 'xstate';
import { someMachine } from './someMachine';

const actor = createActor(someMachine, {
inspect: (inspEvent) => {
if (inspEvent.type === '@xstate.event') {
console.log(inspEvent.event);
}
}
});
// ✅
import { setup, createActor } from 'xstate';

const someMachine = setup({
  // ...
  actions: {
    recordEvent: assign({
      event: ({ event }) => event
    })
  }
}).createMachine({
  context: { event: undefined },
  on: {
    someEvent: {
      // ...
      actions: ['recordEvent']
    }
  }
});

const someActor = createActor(someMachine);
someActor.subscribe(snapshot => {
  console.log(snapshot.context.event);
});
// ❌ DEPRECATED
import { interpret } from 'xstate';
import { someMachine } from './someMachine';

const actor = interpret(someMachine);
actor.subscribe((state) => {
  console.log(state.event); // Removed
});

Events and transitions

Implementation functions receive a single argument

Breaking change

Implementation functions now take in a single argument: an object with context, event, and other properties.

// ✅
const machine = createMachine({
  entry: ({ context, event }) => {
    // ...
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  entry: (context, event) => {
    // ...
  },
});

send() is removed; use raise() or sendTo()

Breaking change

The send(...) action creator is removed. Use raise(...) for sending events to self or sendTo(...) for sending events to other actors instead.

Read the documentation on the sendTo action and raise action for more information.

// ✅
const machine = createMachine({
  // ...
  entry: [
    // Send an event to self
    raise({ type: 'someEvent' }),

    // Send an event to another actor
    sendTo('someActor', { type: 'someEvent' }),
  ],
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  entry: [
    // Send an event to self
    send({ type: 'someEvent' }),

    // Send an event to another actor
    send({ type: 'someEvent' }, { to: 'someActor' }),
  ],
});

Pre-migration tip: Update v4 projects to use sendTo or raise instead of send.

Use enqueueActions() instead of pure() and choose()

The pure() and choose() methods have been removed. Use enqueueActions() instead.

For pure() actions:

// ✅
entry: [
  enqueueActions(({ context, event, enqueue }) => {
    enqueue('action1');
    enqueue('action2');
  }),
];
// ❌ DEPRECATED
entry: [
  pure(() => {
    return ['action1', 'action2'];
  }),
];

For choose() actions:

// ✅
entry: [
  enqueueActions(({ enqueue, check }) => {
    if (check('someGuard')) {
      enqueue('action1');
      enqueue('action2');
    }
  }),
];
// ❌ DEPRECATED
entry: [
  choose([
    {
      guard: 'someGuard',
      actions: ['action1', 'action2'],
    },
  ]),
];

actor.send() no longer accepts string types

Breaking change

String event types can no longer be sent to, e.g., actor.send(event); you must send an event object instead:

// ✅
actor.send({ type: 'someEvent' });
// ❌ DEPRECATED
actor.send('someEvent');

Pre-migration tip: Update v4 projects to pass an object to .send().

state.can() no longer accepts string types

Breaking change

String event types can no longer be sent to, e.g., state.can(event); you must send an event object instead:

// ✅
state.can({ type: 'someEvent' });
// ❌ DEPRECATED
state.can('someEvent');

Guarded transitions use guard, not cond

Breaking change

The cond transition property for guarded transitions is now called guard:

// ✅
const machine = createMachine({
  on: {
    someEvent: {
      guard: 'someGuard',
      target: 'someState',
    },
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  on: {
    someEvent: {
      // renamed to `guard` in v5
      cond: 'someGuard',
      target: 'someState',
    },
  },
});

Use params to pass params to actions & guards

Breaking change

Properties other than type on action objects and guard objects should be nested under a params property; { type: 'someType', message: 'hello' } becomes { type: 'someType', params: { message: 'hello' }}. These params are then passed to the 2nd argument of the action or guard implementation:

// ✅
const machine = createMachine({
  entry: {
    type: 'greet',
    params: {
      message: 'Hello world',
    },
  },
  on: {
    someEvent: {
      guard: { type: 'isGreaterThan', params: { value: 42 } },
    },
  },
}).provide({
  actions: {
    greet: ({ context, event }, params) => {
      console.log(params.message); // 'Hello world'
    },
  },
  guards: {
    isGreaterThan: ({ context, event }, params) => {
      return event.value > params.value;
    },
  },
});
// ❌ DEPRECATED
const machine = createMachine(
  {
    entry: {
      type: 'greet',
      message: 'Hello world',
    },
    on: {
      someEvent: {
        cond: { type: 'isGreaterThan', value: 42 },
      },
    },
  },
  {
    actions: {
      greet: (context, event, { action }) => {
        console.log(action.message); // 'Hello world'
      },
    },
    guards: {
      isGreaterThan: (context, event, { guard }) => {
        return event.value > guard.value;
      },
    },
  },
);

Pre-migration tip: Update action and guard objects on v4 projects to move properties (other than type) to a params object.

Use wildcard * transitions, not strict mode

Breaking change

Strict mode is removed. If you want to throw on unhandled events, you should use a wildcard transition:

// ✅
const machine = createMachine({
  on: {
    knownEvent: {
      // ...
    },
    '*': {
      // unknown event
      actions: ({ event }) => {
        throw new Error(`Unknown event: ${event.type}`);
      },
    },
  },
});

const actor = createActor(machine);

actor.subscribe({
  error: (err) => {
    console.error(err);
  },
});

actor.start();

actor.send({ type: 'unknownEvent' });
// ❌ DEPRECATED
const machine = createMachine({
  strict: true,
  on: {
    knownEvent: {
      // ...
    },
  },
});

const service = interpret(machine);

service.send({ type: 'unknownEvent' });

Use explicit eventless (always) transitions

Breaking change

Eventless (“always”) transitions must now be defined through the always: { ... } property of a state node; they can no longer be defined via an empty string:

// ✅
const machine = createMachine({
  // ...
  states: {
    someState: {
      always: {
        target: 'anotherState',
      },
    },
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  states: {
    someState: {
      on: {
        '': {
          target: 'anotherState',
        },
      },
    },
  },
});

Pre-migration tip: Update v4 projects to use always for eventless transitions.

Use reenter: true, not internal: false

Breaking change

internal: false is now reenter: true

External transitions previously specified with internal: false are now specified with reenter: true:

// ✅
const machine = createMachine({
  // ...
  on: {
    someEvent: {
      target: 'sameState',
      reenter: true,
    },
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  on: {
    someEvent: {
      target: 'sameState',
      internal: false,
    },
  },
});

Transitions are internal by default, not external

Breaking change

All transitions are internal by default. This change is relevant for transitions defined on state nodes with entry or exit actions, invoked actors, or delayed transitions (after). If you relied on the previous XState v4 behavior where transitions implicitly re-entered a state node, use reenter: true:

// ✅
const machine = createMachine({
  // ...
  states: {
    compoundState: {
      entry: 'someAction',
      on: {
        someEvent: {
          target: 'compoundState.childState',
          // Reenters the `compoundState` state,
          // just like an external transition
          reenter: true,
        },
        selfEvent: {
          target: 'childState',
          reenter: true,
        },
      },
      initial: 'childState',
      states: {
        childState: {},
      },
    },
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  states: {
    compoundState: {
      entry: 'someAction',
      on: {
        someEvent: {
          // implicitly external
          target: 'compoundState.childState', // non-relative target
        },
        selfEvent: {
          target: 'compoundState',
        },
      },
      initial: 'childState',
      states: {
        childState: {},
      },
    },
  },
});
// ✅
const machine = createMachine({
  // ...
  states: {
    compoundState: {
      after: {
        1000: {
          target: 'compoundState.childState',
          reenter: true, // make it external explicitly!
        },
      },
      initial: 'childState',
      states: {
        childState: {},
      },
    },
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  states: {
    compoundState: {
      after: {
        1000: {
          // implicitly external
          target: 'compoundState.childState', // non-relative target
        },
      },
      initial: 'childState',
      states: {
        childState: {},
      },
    },
  },
});

Child state nodes are always re-entered

Breaking change

Child state nodes are always re-entered when they are targeted by transitions (both external and internal) defined on compound state nodes. This change is relevant only if a child state node has entry or exit actions, invoked actors, or delayed transitions (after). Add a stateIn guard to prevent undesirable re-entry of the child state:

// ✅

const machine = createMachine({
  // ...
  states: {
    compoundState: {
      on: {
        someEvent: {
          guard: not(stateIn({ compoundState: 'childState' })),
          target: '.childState',
        },
      },
      initial: 'childState',
      states: {
        childState: {
          entry: 'someAction',
        },
      },
    },
  },
});
// ❌ DEPRECATED

const machine = createMachine({
  // ...
  states: {
    compoundState: {
      on: {
        someEvent: {
          // Implicitly internal; childState not re-entered
          target: '.childState',
        },
      },
      initial: 'childState',
      states: {
        childState: {
          entry: 'someAction',
        },
      },
    },
  },
});

Use stateIn() to validate state transitions, not in

Breaking change

The in: 'someState' transition property is removed. Use guard: stateIn(...) instead:

// ✅
const machine = createMachine({
  on: {
    someEvent: {
      guard: stateIn({ form: 'submitting' }),
      target: 'someState',
    },
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  on: {
    someEvent: {
      in: '#someMachine.form.submitting'
      target: 'someState',
    },
  },
});

Use actor.subscribe() instead of state.history

Breaking change

The state.history property is removed. If you want the previous snapshot, you should maintain that via actor.subscribe(...) instead.

// ✅
let previousSnapshot = actor.getSnapshot();

actor.subscribe((snapshot) => {
  doSomeComparison(previousSnapshot, snapshot);
  previousSnapshot = snapshot;
});
// ❌ DEPRECATED
actor.subscribe((state) => {
  doSomeComparison(state.history, state);
});

Pre-migration tip: Update v4 projects to track history using actor.subscribe().

Actions can throw errors without escalate

Breaking change

The escalate action creator is removed. In XState v5 actions can throw errors, and they will propagate as expected. Errors can be handled using an onError transition.

// ✅
const childMachine = createMachine({
  // This will be sent to the parent machine that invokes this child
  entry: () => {
    throw new Error('This is some error');
  },
});

const parentMachine = createMachine({
  invoke: {
    src: childMachine,
    onError: {
      actions: ({ context, event }) => {
        console.log(event.error);
        //  {
        //    type: ...,
        //    error: {
        //      message: 'This is some error'
        //    }
        //  }
      },
    },
  },
});
// ❌ DEPRECATED
const childMachine = createMachine({
  entry: escalate('This is some error'),
});

/* ... */

Actors

Use actor logic creators for invoke.src instead of functions

Breaking change

The available actor logic creators are:

  • createMachine
  • fromPromise
  • fromObservable
  • fromEventObservable
  • fromTransition
  • fromCallback

See Actors for more information.

// ✅
import { fromPromise, setup } from 'xstate';

const machine = setup({
  actors: {
    getUser: fromPromise(async ({ input }: { input: { userId: string } }) => {
      const data = await getData(input.userId);
      // ...
      return data;
    }),
  },
}).createMachine({
  invoke: {
    src: 'getUser',
    input: ({ context, event }) => ({
      userId: context.userId,
    }),
  },
});
// ❌ DEPRECATED
import { createMachine } from 'xstate';

const machine = createMachine({
  invoke: {
    src: (context) => async () => {
      const data = await getData(context.userId);

      // ...
      return data;
    },
  },
});
// ✅
import { fromCallback, createMachine } from 'xstate';

const machine = createMachine({
  invoke: {
    src: fromCallback(({ sendBack, receive, input }) => {
      // ...
    }),
    input: ({ context, event }) => ({
      userId: context.userId,
    }),
  },
});
// ❌ DEPRECATED
import { createMachine } from 'xstate';

const machine = createMachine({
  invoke: {
    src: (context, event) => (sendBack, receive) => {
      // context.userId
      // ...
    },
  },
});
// ✅
import { fromEventObservable, createMachine } from 'xstate';
import { interval, mapTo } from 'rxjs';

const machine = createMachine({
  invoke: {
    src: fromEventObservable(() =>
      interval(1000).pipe(mapTo({ type: 'tick' })),
    ),
  },
});
// ❌ DEPRECATED
import { createMachine } from 'xstate';
import { interval, mapTo } from 'rxjs';

const machine = createMachine({
  invoke: {
    src: () => interval(1000).pipe(mapTo({ type: 'tick' })),
  },
});

Use invoke.input instead of invoke.data

Breaking change

The invoke.data property is removed. If you want to provide context to invoked actors, use invoke.input:

// ✅
const someActor = createMachine({
  // The input must be consumed by the invoked actor:
  context: ({ input }) => input,
  // ...
});

const machine = createMachine({
  // ...
  invoke: {
    src: 'someActor',
    input: {
      value: 42,
    },
  },
});
// ❌ DEPRECATED
const someActor = createMachine({
  // ...
});

const machine = createMachine({
  // ...
  invoke: {
    src: 'someActor',
    data: {
      value: 42,
    },
  },
});

Use output in final states instead of data

Breaking change

To produce output data from a machine which reached its final state, use the top-level output property instead of data:

// ✅
const machine = createMachine({
  // ...
  states: {
    finished: {
      type: 'final',
    },
  },
  output: {
    answer: 42,
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  states: {
    finished: {
      type: 'final',
      data: {
        answer: 42,
      },
    },
  },
});

To provide a dynamically generated output, replace invoke.data with invoke.output and add a top-level output property:

// ✅
const machine = createMachine({
  // ...
  states: {
    finished: {
      type: 'final',
      output: ({ event }) => ({
        answer: event.someValue,
      }),
    },
  },
  output: ({ event }) => event.output,
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  states: {
    finished: {
      type: 'final',
      data: (context, event) => {
        answer: event.someValue,
      },
    },
  },
});

Don't use property mappers in input or output

Breaking change

If you want to provide dynamic context to invoked actors, or produce dynamic output from final states, use a function instead of an object with property mappers.

// ✅
const machine = createMachine({
  // ...
  invoke: {
    src: 'someActor',
    input: ({ context, event }) => ({
      value: event.value,
    }),
  },
});

// The input must be consumed by the invoked actor:
const someActor = createMachine({
  // ...
  context: ({ input }) => input,
});

// Producing machine output
const machine = createMachine({
  // ...
  states: {
    finished: {
      type: 'final',
    },
  },
  output: ({ context, event }) => ({
    answer: context.value,
  }),
});
// ❌ DEPRECATED
const machine = createMachine({
  // ...
  invoke: {
    src: 'someActor',
    data: {
      value: (context, event) => event.value, // a property mapper
    },
  },
});

// Producing machine output
const machine = createMachine({
  // ...
  states: {
    finished: {
      type: 'final',
      data: {
        answer: (context, event) => context.value, // a property mapper
      },
    },
  },
});

Use actors property on options object instead of services

Breaking change

services have been renamed to actors:

// ✅
const specificMachine = machine.provide({
  actions: {
    /* ... */
  },
  guards: {
    /* ... */
  },
  actors: {
    /* ... */
  },
  // ...
});
// ❌ DEPRECATED
const specificMachine = machine.withConfig({
  actions: {
    /* ... */
  },
  guards: {
    /* ... */
  },
  services: {
    /* ... */
  },
  // ...
});

Use subscribe() for changes, not onTransition()

Breaking change

The actor.onTransition(...) method is removed. Use actor.subscribe(...) instead.

// ✅
const actor = createActor(machine);
actor.subscribe((state) => {
  // ...
});
// ❌ DEPRECATED
const actor = interpret(machine);
actor.onTransition((state) => {
  // ...
});

createActor() (formerly interpret()) accepts a second argument to restore state

Breaking change

interpret(machine).start(state) is now createActor(machine, { snapshot }).start()

To restore an actor at a specific state, you should now pass the state as the snapshot property of the options argument of createActor(logic, options). The actor.start() property no longer takes in a state argument.

// ✅
const actor = createActor(machine, { snapshot: someState });
actor.start();
// ❌ DEPRECATED
const actor = interpret(machine);
actor.start(someState);

Use actor.getSnapshot() to get actor’s state

Breaking change

Subscribing to an actor (actor.subscribe(...)) after the actor has started will no longer emit the current snapshot immediately. Instead, read the current snapshot from actor.getSnapshot():

// ✅
const actor = createActor(machine);
actor.start();

const initialState = actor.getSnapshot();

actor.subscribe((state) => {
  // Snapshots from when the subscription was created
  // Will not emit the current snapshot until a transition happens
});
// ❌ DEPRECATED
const actor = interpret(machine);
actor.start();

actor.subscribe((state) => {
  // Current snapshot immediately emitted
});

Loop over events instead of using actor.batch()

Breaking change

The actor.batch([...]) method for batching events is removed.

// ✅
for (const event of events) {
  actor.send(event);
}
// ❌ DEPRECATED
actor.batch(events);

Pre-migration tip: Update v4 projects to loop over events to send them as a batch.

Use snapshot.status === 'done' instead of snapshot.done

Breaking change

The snapshot.done property, which was previously in the snapshot object of state machine actors, is removed. Use snapshot.status === 'done' instead, which is available to all actors:

// ✅
const actor = createActor(machine);
actor.start();

actor.subscribe((snapshot) => {
  if (snapshot.status === 'done') {
    // ...
  }
});
// ❌ DEPRECATED
const actor = interpret(machine);
actor.start();

actor.subscribe((state) => {
  if (state.done) {
    // ...
  }
});

state.nextEvents has been removed

Breaking change

The state.nextEvents property is removed, since it is not a completely safe/reliable way of determining the next events that can be sent to the actor. If you want to get the next events according to the previous behavior, you can use this helper function:

import type { AnyMachineSnapshot } from 'xstate';

function getNextEvents(snapshot: AnyMachineSnapshot) {
  return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])];
}

// Instead of `state.nextEvents`:
const nextEvents = getNextEvents(state);

TypeScript

Use types instead of schema

Breaking change

The machineConfig.schema property is renamed to machineConfig.types:

// ✅
const machine = createMachine({
  types: {} as {
    context: {
      /* ...*/
    };
    events: {
      /* ...*/
    };
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  schema: {} as {
    context: {
      /* ...*/
    };
    events: {
      /* ...*/
    };
  },
});

Use types.typegen instead of tsTypes

Breaking change

XState Typegen does not fully support XState v5 yet. However, strongly-typed machines can still be achieved without Typegen.

The machineConfig.tsTypes property has been renamed and is now at machineConfig.types.typegen.

// ✅
const machine = createMachine({
  types: {} as {
    typegen: {};
    context: {
      /* ...*/
    };
    events: {
      /* ...*/
    };
  },
});
// ❌ DEPRECATED
const machine = createMachine({
  tsTypes: {};
  schema: {} as {
    context: {
      /* ...*/
    };
    events: {
      /* ...*/
    };
  },
});

@xstate/react

useInterpret() is now useActorRef()

Breaking change

The useInterpret() hook, which is used to return an actorRef ("service" in XState v4), is renamed to useActorRef().

// ✅
import { useActorRef } from '@xstate/react';

const actorRef = useActorRef(machine); // or any other logic
// ❌ DEPRECATED
import { useInterpret } from '@xstate/react';

const service = useInterpret(machine);

useActor(logic) now accepts actor logic, not an actor

Breaking change

The useActor(logic) hook now accepts actor logic (such as fromPromise(...), createMachine(...), etc.) instead of an existing ActorRef.

To use an existing ActorRef, use actor.send(...) to send events and useSelector(actor, ...) to get the snapshot:

// ✅
import { useSelector } from '@xstate/react';

function Component({ someActorRef }) {
  const state = useSelector(someActorRef, (s) => s);

  return <button onClick={() => someActorRef.send({ type: 'someEvent' })} />;
}
// ❌ DEPRECATED
import { useActor } from '@xstate/react';

function Component({ someActorRef }) {
  const [state, send] = useActor(someActorRef);

  return <button onClick={() => send({ type: 'someEvent' })} />;
}

Use machine.provide() to provide implementations in hooks

Breaking change

For dynamically creating machines with provided implementations, the useMachine(...), useActor(...), and useActorRef(...) hooks no longer accept:

  • Lazy machine creators as the 1st argument
  • Implementations passed to the 2nd argument

Instead, machine.provide(...) should be passed directly to the 1st argument.

The @xstate/react package considers machines with the same configuration to be the same machine, so it will minimize rerenders but still keep the provided implementations up-to-date.

// ✅
import { useMachine } from '@xstate/react';
import { someMachine } from './someMachine';

function Component(props) {
  const [state, send] = useMachine(
    someMachine.provide({
      actions: {
        doSomething: () => {
          props.onSomething?.(); // Kept up-to-date
        },
      },
    }),
  );

  // ...
}
// ❌ DEPRECATED
import { useMachine } from '@xstate/react';
import { someMachine } from './someMachine';

function Component(props) {
  const [state, send] = useMachine(someMachine, {
    actions: {
      doSomething: () => {
        props.onSomething?.();
      },
    },
  });

  // ...
}
// ❌ DEPRECATED
import { useMachine } from '@xstate/react';
import { someMachine } from './someMachine';

function Component(props) {
  const [state, send] = useMachine(() =>
    someMachine.withConfig({
      actions: {
        doSomething: () => {
          props.onSomething?.();
        },
      },
    }),
  );

  // ...
}

@xstate/vue

useMachine() now returns snapshot instead of state, and actor instead of service

Breaking change

To keep consistent naming with the rest of XState and related libraries:

  • state is now snapshot
  • service is now actor
// ✅
import { useMachine } from '@xstate/vue';

// ...

const {
  snapshot, // Renamed from `state`
  send,
  actor, // Renamed from `service`
} = useMachine(someMachine);
// ❌ DEPRECATED
import { useMachine } from '@xstate/vue';

// ...

const {
  state, // Renamed to `snapshot` in @xstate/vue 3.0.0
  send,
  service, // Renamed to `actor` in @xstate/vue 3.0.0
} = useMachine(someMachine);

New features

Frequently asked questions

When will Stately Studio be compatible with XState v5?

We are currently working on Stately Studio compatibility with XState v5. Exporting to XState v5 (JavaScript or TypeScript) is already available. We are working on support for new XState v5 features, such as higher-order guards, partial event wildcards, and machine input/output.

Upvote or comment on Stately Studio + XState v5 compatibility in our roadmap to stay updated on our progress.

When will the XState VS Code extension be compatible with XState v5?

The XState VS Code extension is not yet compatible with XState v5. The extension is a priority for us, and work is already underway.

Upvote or comment on XState v5 compatibility for VS Code extension in our roadmap to stay updated on our progress.

When will XState v5 have typegen?

TypeScript inference has been greatly improved in XState v5. Especially with features like the setup() API and dynamic parameters, the main use-cases for typegen are no longer needed.

However, we recognize that there may still be some specific use-cases for typegen. Upvote or comment on Typegen for XState v5 in our roadmap to stay updated on our progress.

How can I use both XState v4 and v5?

You can use both XState v4 and v5 in the same project, which is useful for incrementally migrating to XState v5. To use both, add "xstate5": "npm:xstate@5" to your package.json manually or through the CLI:

npm i xstate5@npm:xstate@5

Then, you can import the v5 version of XState in your code:

import { createMachine } from 'xstate5';
// or { createMachine as createMachine5 } from 'xstate5';

If you need to use different versions of an integration package, such as @xstate/react, you can use a similar strategy as above, but you will need to link to the correct version of XState in the integration package. This can be done by using a script:

npm i xstate5@npm:xstate@5 @xstate5/react@npm:@xstate/react@4 --force
// scripts/xstate5-react-script.js
const fs = require('fs-extra');
const path = require('path');

const rootNodeModules = path.join(__dirname, '..', 'node_modules');

fs.ensureSymlinkSync(
  path.join(rootNodeModules, 'xstate5'),
  path.join(rootNodeModules, '@xstate5', 'react', 'node_modules', 'xstate'),
);
// package.json
"scripts": {
  "postinstall": "node scripts/xstate5-react-script.js"
}

Then, you can use the XState v5 compatible version of @xstate/react in your code:

import { useMachine } from '@xstate5/react';
// or { useMachine as useMachine5 } from '@xstate5/react';
import { createMachine } from 'xstate5';
// or { createMachine as createMachine5 } from 'xstate5';

// ...

On this page

XState v5 and TypeScriptCreating machines and actorsUse createMachine(), not Machine()Use createActor(), not interpret()Use machine.provide(), not machine.withConfig()Set context with input, not machine.withContext()Actions ordered by default, predictableActionArguments no longer neededThe spawn() function has been removedUse getNextSnapshot(…) instead of machine.transition(…)Send events explictly instead of using autoForwardStatesUse state.getMeta() instead of state.metaThe state.toStrings() method has been removedUse state._nodes instead of state.configurationRead events from inspection API instead of state.eventsEvents and transitionsImplementation functions receive a single argumentsend() is removed; use raise() or sendTo()Use enqueueActions() instead of pure() and choose()actor.send() no longer accepts string typesstate.can() no longer accepts string typesGuarded transitions use guard, not condUse params to pass params to actions & guardsUse wildcard * transitions, not strict modeUse explicit eventless (always) transitionsUse reenter: true, not internal: falseTransitions are internal by default, not externalChild state nodes are always re-enteredUse stateIn() to validate state transitions, not inUse actor.subscribe() instead of state.historyActions can throw errors without escalateActorsUse actor logic creators for invoke.src instead of functionsUse invoke.input instead of invoke.dataUse output in final states instead of dataDon't use property mappers in input or outputUse actors property on options object instead of servicesUse subscribe() for changes, not onTransition()createActor() (formerly interpret()) accepts a second argument to restore stateUse actor.getSnapshot() to get actor’s stateLoop over events instead of using actor.batch()Use snapshot.status === 'done' instead of snapshot.donestate.nextEvents has been removedTypeScriptUse types instead of schemaUse types.typegen instead of tsTypes@xstate/reactuseInterpret() is now useActorRef()useActor(logic) now accepts actor logic, not an actorUse machine.provide() to provide implementations in hooks@xstate/vueuseMachine() now returns snapshot instead of state, and actor instead of serviceNew featuresFrequently asked questionsWhen will Stately Studio be compatible with XState v5?When will the XState VS Code extension be compatible with XState v5?When will XState v5 have typegen?How can I use both XState v4 and v5?