Built-in actions

Along with assign, XState has several other built-in actions which can do different things in a state machine. We’ll introduce a couple of built-in actions for now and learn about the others later.

Send action

XState’s built-in send action is useful for when statecharts need to send events back to themselves.

When the send action is executed, it sends an event back to the machine as if it were from an external source.

This pattern can help compose different flows together. In the example below, the user can either press the copy button or press ctrl + c to fire a COPY event to the machine. Using the send action to fire the same event from both actions reduces duplication.

ts
import { createMachine, send } from "xstate";
 
const keyboardShortcutMachine = createMachine({
on: {
PRESS_COPY_BUTTON: {
actions: send({ type: "COPY" }),
},
PRESS_CTRL_C: {
actions: send({ type: "COPY" }),
},
COPY: {
actions: "copyToClipboard",
},
},
});
ts
import { createMachine, send } from "xstate";
 
const keyboardShortcutMachine = createMachine({
on: {
PRESS_COPY_BUTTON: {
actions: send({ type: "COPY" }),
},
PRESS_CTRL_C: {
actions: send({ type: "COPY" }),
},
COPY: {
actions: "copyToClipboard",
},
},
});

You can also dynamically specify the event to send by passing a function to send:

ts
send((context, event) => {
return {
type: "SOME_EVENT",
};
});
ts
send((context, event) => {
return {
type: "SOME_EVENT",
};
});

Pure action

The pure action is useful when you need to run a dynamic number of actions depending on the current machine’s state.

pure lets you pass a function to the machine, which calculates the type and number of actions to be executed.

In the example below, we check context to find which actions the machine should run.

ts
import { actions, createMachine } from "xstate";
 
const { pure } = actions;
 
createMachine({
context: {
runBothActions: false,
},
entry: pure((context) => {
if (context.runBothActions) {
// You can return an array of actions
return ["action1", "action2"];
}
// Or a single action
return "action1";
}),
});
ts
import { actions, createMachine } from "xstate";
 
const { pure } = actions;
 
createMachine({
context: {
runBothActions: false,
},
entry: pure((context) => {
if (context.runBothActions) {
// You can return an array of actions
return ["action1", "action2"];
}
// Or a single action
return "action1";
}),
});

Rules of built-in actions

Built-in actions are pure functions. Pure functions don’t execute anything themselves but instead return instructions that tells XState what to do.

For example, the assign function returns an object containing type: 'xstate.assign' and an assigner function.

ts
import { assign } from "xstate";
 
const assignResult = assign((context, event) => ({
newValue: true,
}));
 
assignResult.type; // 'xstate.assign'
assignResult.assigner; // (context, event) => ({ newValue: true })
ts
import { assign } from "xstate";
 
const assignResult = assign((context, event) => ({
newValue: true,
}));
 
assignResult.type; // 'xstate.assign'
assignResult.assigner; // (context, event) => ({ newValue: true })

The instruction set above is interpreted by XState, which executes the code. The result of assign must be passed directly to actions, entry or exit.

For example, the following code won’t work correctly because the result of the assign isn’t being passed into assignToContext.

ts
import { createMachine, assign } from "xstate";
 
const machine = createMachine(
{
// ...config
},
{
actions: {
assignToContext: (context, event) => {
// 🚫 This won’t work!
// The result of the assign isn’t being passed
// into assignToContext
assign({
message: "Hello!",
});
},
},
},
);
ts
import { createMachine, assign } from "xstate";
 
const machine = createMachine(
{
// ...config
},
{
actions: {
assignToContext: (context, event) => {
// 🚫 This won’t work!
// The result of the assign isn’t being passed
// into assignToContext
assign({
message: "Hello!",
});
},
},
},
);

The following example works correctly because the result of the assign is passed into assignToContext:

ts
import { createMachine, assign } from "xstate";
 
const machine = createMachine(
{
// ...config
},
{
actions: {
assignToContext: assign((context) => ({
message: "Hello!",
})),
},
},
);
ts
import { createMachine, assign } from "xstate";
 
const machine = createMachine(
{
// ...config
},
{
actions: {
assignToContext: assign((context) => ({
message: "Hello!",
})),
},
},
);

Summary

send can be used to send events back to your machine. pure can be used to dynamically return different actions. Built-in actions must be passed directly to the machine or returned from pure.