xstate
xstate copied to clipboard
[v5] implement action groups
This PR implements #3996, enabling actions to be implemented as an action group. (replaces #3561)
An action group is a named array of actions. When the action group is executed, each action in the group is executed in order:
import { createMachine, interpret, log } from 'xstate';
const machine = createMachine(
{
// reference the action group by name (`someGroup`)
entry: 'someGroup'
},
{
actions: {
// executes `action1`, then `action2`
someGroup: ['action1', 'action2'],
action1: log('action1'),
action2: log('action2')
}
}
);
interpret(machine).start(); // logs "action1", then "action2"
Action groups allow us to avoid error-prone repetition of actions, instead defining the group once and reusing it anywhere—like a single source of truth for the algorithm the group represents:
const machine = createMachine(
{
context: { count: 0 },
on: {
- // increment count, then print it
- incrementClick: { actions: ['incrementCount', 'printCount'] },
+ incrementClick: { actions: 'increment' },
- // increment count, then print it
- // oops! we accidentally put the actions in the wrong order
- tick: { actions: ['printCount', 'incrementCount'] },
+ tick: { actions: 'increment' }
}
},
{
actions: {
+ // increment count, then print it. single source of truth for the `increment` algorithm
+ increment: ['incrementCount', 'printCount'],
incrementCount: assign({
count: ({ context }) => context.count + 1
}),
printCount: log(({ context }) => `Count: ${context.count}`)
}
}
);
Action groups can reference other action groups by name. The referenced group's actions will be executed in order from the point of reference—like spreading the referenced group's actions in the group:
const machine = createMachine(
{
entry: 'initialize'
},
{
actions: {
// executes `load` group actions, then `listen` group actions
initialize: ['load', 'listen'],
load: ['loadConfig', 'loadData'],
listen: ['startApp', 'listenOnPort']
}
}
);
interpret(machine).start();
// actions: (load) loadConfig -> loadData -> (listen) startApp -> listenOnPort
With a mix of actions, action groups, and action group references, we can compose our algorithms in flexible and reusable ways:
import { assign, createMachine, log } from 'xstate';
const machine = createMachine(
{
entry: 'initialize',
exit: 'terminate',
on: {
timeout: { actions: 'reconnect' },
reload: { actions: 'reload' }
}
},
{
actions: {
initialize: ['load', 'connect'],
terminate: ['disconnect', 'save', 'exitProgram'],
reconnect: ['disconnect', 'connect'],
reload: ['disconnect', 'save', 'initialize']
}
}
);
I feel like there's something powerful to thinking about action groups as composable algorithms, though I'm not entirely sure what that is yet, so I'm excited for this to land so more people can play with it!
🦋 Changeset detected
Latest commit: 8a38c23d1cd324826fe6b847208a7210b561c275
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 1 package
| Name | Type |
|---|---|
| xstate | Major |
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
This pull request is automatically built and testable in CodeSandbox.
To see build info of the built libraries, click here or the icon next to each commit SHA.
Latest deployment of this branch, based on commit 8a38c23d1cd324826fe6b847208a7210b561c275:
| Sandbox | Source |
|---|---|
| XState Example Template | Configuration |
| XState React Template | Configuration |
I didn't take a thorough look through the implementation but here are my thoughts:
- we'd like to support this
- i will be refactoring how we handle actions soon, so I likely would prefer for this work to land after that and thus this PR will require significant~ changes. OTOH, it's mostly about resolving the actions recursively so it shouldn't take a lot of time to rebase this
- I think that before landing this we might need to add support for this in the typegen (I know there is a PR for that already) and in the Studio. We'd need some designs or at least some basic support for this (this might also require adding support in the bidi editing code)
- i will be refactoring how we handle actions soon, so I likely would prefer for this work to land after that and thus this PR will require significant~ changes. OTOH, it's mostly about resolving the actions recursively so it shouldn't take a lot of time to rebase this
That's fine with me!
- I think that before landing this we might need to add support for this in the typegen (I know there is a PR for that already) and in the Studio. We'd need some designs or at least some basic support for this (this might also require adding support in the bidi editing code)
That makes sense to me. I'll work on getting the typegen PR updated so it can land before this.
🧹 I think that with enqueueActions(…) and v6 ideas for single-function actions (basically enqueueActions(…) but by default), action groups will be made redundant, since it would be easier to use functions as "groups" instead:
// TENTATIVE API
const machine = setup({
actions: {
actionGroup: (_, x) => {
x.action({ type: 'action1' });
x.action({ type: 'action2' });
},
action1: () => { ... },
action2: () => { ... },
}
}).createMachine({
entry: 'someGroup'
});
@davidkpiano that sounds lovely! Do you have v6 ideas documented anywhere publicly? I'd love to see what you're thinking
@davidkpiano that sounds lovely! Do you have v6 ideas documented anywhere publicly? I'd love to see what you're thinking
Not yet, but once I set up a branch/draft PR for it I'll let you know.
Thank you! ❤️