Xstate: Resolve/document inconsistencies between @xstate/fsm and xstate

Created on 20 Jul 2020  路  10Comments  路  Source: davidkpiano/xstate

Description
I use both xstate and @xstate/fsm depending on the complexity of the project and sometimes start with @xstate/fsm and move to xstate if needed. There are, however, some undocumented differences that have tripped me up a few times.

  1. In xstate, the object returned by a function assigner is merged with the current context. In @xstate/fsm it replaces the current context.

  2. In xstate, transitions without a target are internal. In @xstate/fsm these transitions will exit and reenter the state node. I often have both entry actions and actions that respond to events but shouldn't cause transitions. In @xstate/fsm the entry actions are executed on every event, making them unusable.

Expected Result
I would expect the behaviour to be the same for the features shared by the two libraries.

Actual Result
The libraries differ in subtle ways, leading to unexpected behaviour.

bug 馃 @xstatfsm

Most helpful comment

Few things to add here:

  • I see from #1367 that an assigner function will now completely replace the context in xstate so this should no longer be changed in @xstate/fsm.
  • As guards can be used on transitions in @xstate/fsm it seems strange that they can't be included in the machine options. It would be good if the machine options could include both actions and guards.
  • The StateMachine.Machine returned by createMachine in @xstate/fsm doesn't have a withConfig method so integrations have to handle the merging manually. It would be preferable for this to be implemented in @xstate/fsm so that there is no need to access _options on the returned machine. This would also prevent integration code breaking if there are changes to the machine structure.
  • In @xstate/fsm, machines are created with createMachine whereas in xstate they can be created with either Machine or createMachine. The difference seems to be the way they handle generics. Will one of these be converged upon in V5 or is it necessary to have both? Personally, I think createMachine is more explicit and that using PascalCase for a function is a little confusing.
  • In @xstate/fsm, a service is subscribed to using the subscribe method whereas in xstate you can use subscribe or onTransition. Are both necessary or could subscribe be used in all cases?
  • It would be good to introduce some tests to ensure that @xstate/fsm uses a functionally equivalent subset of xstate's features i.e. a user should be able to replace @xstate/fsm with xstate in a project without any changes to their code.

All 10 comments

So, seems like two things to fix in @xstate/fsm:

  • assign(() => ...) should merge, not replace
  • undefined targets should be considered internal, whereas explicit target: 'sameNode' should be considered external transitions.

Sounds about right?

Yep. That would help a lot. On a related note, in both libraries it would be useful if a function assigner could return undefined for no changes to the context. This would save having to return an empty object and make conditional logic a bit leaner.

it would be useful if a function assigner could return undefined for no changes to the context

@jamesopstad while I agree that some syntactic sugar would be nice, why not guard your assign action? I believe the moment you execute the assign action, state.changed will be set to true even if the context wasn't changed.

The extra guard will also improve visualization.

I use if statements inside function assigners as type guards to narrow the event type e.g.

assign((context, event) => {
 if (event.type === 'someType') {
  return {
   // context changes
  }
 } else {
   return {}
 }
}

I don't think there's another way of correctly inferring the event type in the actions object but correct if I'm wrong. If the function assigner could return undefined then the else statement would be unnecessary. Not a big deal but they add up and I think it would only require a very small change to xstate.

@jamesopstad that's great. I was talking about cond: 'isEventOfMyType', actions: 'assignContextBasedOnMyType'. That way your action is only executed when the guard evaluates to true.

A more flexible solution for this would be using a choose action over guarded transition:

choose([ cond: 'isEventOfMyType', actions: 'assignContextBasedOnMyType' ])

True, but you still need a condition in the assign function to narrow the event type.

Right, that's true - you could also do this:

pure((ctx, event) => {
  if (event.type === 'someType') {
    return assign({ /* context changes */ })
  }
})

I'm not sure if allowing undefined as a return type of Assigner would be preferable as usually not returning something is an oversight on a developer's side.

Few things to add here:

  • I see from #1367 that an assigner function will now completely replace the context in xstate so this should no longer be changed in @xstate/fsm.
  • As guards can be used on transitions in @xstate/fsm it seems strange that they can't be included in the machine options. It would be good if the machine options could include both actions and guards.
  • The StateMachine.Machine returned by createMachine in @xstate/fsm doesn't have a withConfig method so integrations have to handle the merging manually. It would be preferable for this to be implemented in @xstate/fsm so that there is no need to access _options on the returned machine. This would also prevent integration code breaking if there are changes to the machine structure.
  • In @xstate/fsm, machines are created with createMachine whereas in xstate they can be created with either Machine or createMachine. The difference seems to be the way they handle generics. Will one of these be converged upon in V5 or is it necessary to have both? Personally, I think createMachine is more explicit and that using PascalCase for a function is a little confusing.
  • In @xstate/fsm, a service is subscribed to using the subscribe method whereas in xstate you can use subscribe or onTransition. Are both necessary or could subscribe be used in all cases?
  • It would be good to introduce some tests to ensure that @xstate/fsm uses a functionally equivalent subset of xstate's features i.e. a user should be able to replace @xstate/fsm with xstate in a project without any changes to their code.

As guards can be used on transitions in @xstate/fsm it seems strange that they can't be included in the machine options. It would be good if the machine options could include both actions and guards.

This one tripped me up

Was this page helpful?
0 / 5 - 0 ratings

Related issues

carlbarrdahl picture carlbarrdahl  路  3Comments

3plusalpha picture 3plusalpha  路  3Comments

hnordt picture hnordt  路  3Comments

carloslfu picture carloslfu  路  3Comments

ifokeev picture ifokeev  路  3Comments