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

Actions inside state? #93

Closed
arggh opened this issue Feb 25, 2020 · 9 comments
Closed

Actions inside state? #93

arggh opened this issue Feb 25, 2020 · 9 comments

Comments

@arggh
Copy link

arggh commented Feb 25, 2020

I'll start with the disclaimer that I'm just dipping my toes with robot, but here goes.

Currently the documentation for actions states that:

action takes a function that will be run during a transition. The primary purpose of using action is to perform side-effects.

Question

Why are actions allowed only within transitions? What if I want to invoke an action every time the machine enters a specific state, regardless of the previous state? With the current API, I have to define that action in all the transitions that lead to that specific state.

Suggestion

Allow actions to be defined also as children of state.

const machine = createMachine({
  idle: state(transition("submit", "validate")),
  validate: state(
    action(ctx => log('Validation attempt', ctx))
  ),
  // ...gazillion other states that might lead to 'validate'
});

Now the action get's invoked every time the machine enters validate-state.

To take it one step further, there could also be an option to define whether the action should be triggered upon entry or exit of state.

@matthewp
Copy link
Owner

Thanks for starting the discussion! There are three design considerations that heavily drive Robot's API:

  1. Simplicity and having 1 way of doing things.
  2. Keeping the size small
  3. Composability

Having a second way to perform actions would violate (1) and (2).

You correctly point out that having many transitions that all want to perform an action could result in verbose code. There's sort of 2 ways to solve this:

  1. Use function composition. This is the general solution to code reuse in Robot. You could create a special transition function that always calls this action.
const validatingTransition = event => transition("submit", "validate",
  action(ctx => log('Validation attempt', ctx)))
  1. In this case you can use an intermediate state:
const machine = createMachine({
  idle: state(transition("submit", "log_validate")),
  log_validate: state(
    immediate("validate", action(ctx => log('Validation attempt', ctx)))
  ),
  validate: state()
});

@arggh
Copy link
Author

arggh commented Feb 26, 2020

Thanks for the suggestions!

To keep the discussion going, for a while at least, I'll throw in a few counterarguments:

  1. The function composition -approach might become slightly cumbersome, if all transitions cannot be the same transition, eg. one of them has to call a bunch of other actions on the way to the validate state.

  2. Intermediate state -approach feels like it's making the machine logic harder to follow and reason about due to extra hoops and camouflaged actual target state, but this is mostly a matter of taste, I guess?

  3. Allowing the action() as an argument of state would kill two birds with one stone (...would kill 1.5 birds with one stone and provide some flexibility to the timing of actions, but a true exit-action would definitely need another API change, so that it's not tied to transitions either): actions upon entry (action as argument to state) and actions upon exit (action as argument to transition)

Regarding violating the design principles 1 & 2

  1. I'd argue that having an action take place during transition and when entering a state are two different things, therefore the suggested API wouldn't violate the "1 way of doing things" - you would still be using the one and only action(), but it's placement determines when the action should take place.

  2. I've no idea on the amount of work or code that Robot would require to facilitate this change, but I'd be surprised if it increased the size by any significant margin?

@HipsterBrown
Copy link
Contributor

For the project I've been working on, which is building an API on top of robot to build "wizard" logic using React component, I've created helper functions around the core utilities:

const createStep = (stepName: string, ...transitions: Transition[]) => {
  return {
    [stepName]: [transition('updateValues', stepName', updateValuesReducer), ...transitions] 
  };
}

This is similar to the field example in the "Composition" guide in the docs.

A composable function could be created that includes whatever action is required when transitioning to a certain state. 🤷‍♂

@matthewp
Copy link
Owner

matthewp commented Mar 3, 2020

Yeah @HipsterBrown, I would do it that way as well. Very cool to see Robot being used in a wizard! I'd love to hear more about it if you can.


@arggh One thing to keep in mind is that, as implemented, an action is just a reducer that has no return value. https://github.com/matthewp/robot/blob/master/machine.js#L31 . I just say this to say that an action inside of state would be a new feature.

I'm curious, what is the usecase for enter/exit actions? Or actions in general? I've really only used them for debugging.

@HipsterBrown
Copy link
Contributor

I actually just found a great use for actions today. In the wizard tool I'm building, I was originally using the service onChange callback to call history.push during a change in state, i.e. progression forward or backwards. That led to some weird issues when other events triggered that onChange callback when I didn't want to call history.push. I fixed this issue by using an action for that history.push call as a side-effect for the next and previous events.

Once I get approval to open-source this stuff, it will help illustrate what I'm talking about.

@kybarg
Copy link
Contributor

kybarg commented Mar 25, 2020

@arggh haven't read the whole thread, but what about using immediate

const machine = createMachine({
    initial: state(
        transition('START', 'start'),
    ),
    start: state(
        immediate('start',
            guard((ctx) => !ctx),
            reduce(() => true),
            action(() => {
                console.log('I run once on start!')
            })
        ),
        transition('DONE', 'done'),
    ),
    done: state(),
}),

@matthewp
Copy link
Owner

I'm going to close because we aren't going to do this right now. Happy to continue discussion and would be happy to come up with patterns to document on the site to achieve this sort of thing through composition.

@frederikhors
Copy link

frederikhors commented Aug 15, 2021

but what about using immediate

Is this still working today? I think it's not. At least I'm not able to. Can you help me, @kybarg?

@frederikhors
Copy link

@HipsterBrown can you publish some code for that wizard with robot?

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

No branches or pull requests

5 participants