Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v5] implement action groups #3997

Closed
wants to merge 6 commits into from

Conversation

with-heart
Copy link
Contributor

@with-heart with-heart commented May 3, 2023

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-bot
Copy link

changeset-bot bot commented May 3, 2023

🦋 Changeset detected

Latest commit: 8a38c23

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

@codesandbox-ci
Copy link

codesandbox-ci bot commented May 3, 2023

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 8a38c23:

Sandbox Source
XState Example Template Configuration
XState React Template Configuration

@ghost
Copy link

ghost commented May 3, 2023

👇 Click on the image for a new way to code review

Review these changes using an interactive CodeSee Map

Legend

CodeSee Map legend

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was worried this might be too long but after thinking about it, I think this is probably the minimum explanation I can come up with to really get to the heart of what's cool about this feature

packages/core/src/actions.ts Outdated Show resolved Hide resolved
packages/core/src/types.ts Show resolved Hide resolved
@with-heart with-heart changed the title implement action groups [v5] implement action groups May 3, 2023
Comment on lines +244 to +265
export type ActionGroup<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
TEvent extends EventObject = TExpressionEvent,
TActionTypes = string
> = Array<
| TActionTypes
| ParameterizedObject
| BaseDynamicActionObject<
TContext,
TExpressionEvent,
TEvent,
any, // TODO: this should receive something like `Cast<Prop<TIndexedActions, K>, ParameterizedObject>`, but at the moment builtin actions expect Resolved*Action here and this should be simplified somehow
any
>
| ActionFunction<
TContext,
TExpressionEvent,
ParameterizedObject, // TODO: when bringing back parametrized actions this should accept something like `Cast<Prop<TIndexedActions, K>, ParameterizedObject>`. At the moment we need to keep this type argument consistent with what is provided to the fake callable signature within `BaseDynamicActionObject`
TEvent
>
>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding action groups to MachineImplementationsActions was getting pretty messy so I pulled this out as a separate type.

Comment on lines +79 to +87
params: {
actions: toActionObjects(dereferencedAction).map((object) => ({
...object,
params: {
...object.params,
...params
}
}))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since each action needs to receive params, I just resolved the action objects for each here and then mapped them to include the params.

Is that fine?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine to me 👍

packages/core/src/actions.ts Show resolved Hide resolved
Comment on lines +3811 to +3837
entry: {
type: 'messageHandlers',
params: {
message: 'Hello world!'
}
}
},
{
actions: {
messageHandlers: [
'handler1',
{ type: 'otherHandlers', params: { level: 'info' } }
],
otherHandlers: ['handler2'],
handler1: ({ action }) => handler1(action.params),
handler2: ({ action }) => handler2(action.params)
}
}
);

interpret(machine).start();

expect(handler1).toHaveBeenCalledWith({ message: 'Hello world!' });
expect(handler2).toHaveBeenCalledWith({
message: 'Hello world!',
level: 'info'
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has an implicit test for params overriding. Should we be more explicit with a separate test?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might not be a need because if the behavior changes, this test will fail.

cc. @Andarist for his thoughts

@Andarist
Copy link
Member

I didn't take a thorough look through the implementation but here are my thoughts:

  1. we'd like to support this
  2. 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
  3. 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)

@with-heart
Copy link
Contributor Author

  1. 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!

  1. 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.

@davidkpiano
Copy link
Member

🧹 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'
});

@with-heart with-heart deleted the action-groups-next branch May 19, 2024 17:22
@with-heart
Copy link
Contributor Author

@davidkpiano that sounds lovely! Do you have v6 ideas documented anywhere publicly? I'd love to see what you're thinking

@davidkpiano
Copy link
Member

@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.

@with-heart
Copy link
Contributor Author

Thank you! ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants