xstate icon indicating copy to clipboard operation
xstate copied to clipboard

Delay function can't access current state where it is referenced

Open ilmarmors opened this issue 3 years ago • 10 comments

Description There is option to specify delay in state after section as function. In my use case it makes sense to use one function that calculates delay based on current state and optionally context variable value. It looks like currently delay function is called first, even before interpreter changes state.value and is calling onTransition listener.

Probably most general solution would be adding additional parameter (state or meta object that includes state like in actionFunction(context, event, actionMeta)) to delay function, which currently has (context, event) as parameters.

Less preferable solutions:

  • using service (interpreter).state, which would be updated before (now - after) delay function is called.
  • onTransition listener is called before (now - after) delay function is called. Then I set current state as variable to use in delay function

P.S. In reproduction I have added also changing context variable value, which happens after state is transitioned, just to understand better xstate inside. I guess it would be wrong idea to call delay function after entry actions are executed.

Expected Result See reproduction. service.state is 'process' (state where delay function resides)

Actual Result See reproduction. service.state is 'start' (previous state)

Reproduction Simplest reproduction - https://codesandbox.io/s/xstate-example-template-vanilla-forked-wdzg3

Additional context XState version 4.22.00

ilmarmors avatar Jul 16 '21 01:07 ilmarmors

@ilmarmors Could you fill in a bit more detail on your use case? What is it that you're trying to achieve? Are you trying to have one delay length while in one state, and another delay length while in another?

We might be able to model this more idiomatically using current tools.

mattpocock avatar Jul 21 '21 09:07 mattpocock

I have set of exercises user is going through one by one. All the exercises have the same flow - start, intro, position check, feedback and more. So basically all the exercises go through the same states and I'm using the same state machine. But there are differences among exercises for the same states - different duration, different parameters etc, which are stored in json configuration file (exercise is variable to access config for current exercise)

For example, I have state

body_orientation_instructions: {
  entry: [
    'showTextMessage'
  ],
  after: {
    // messageDuration: { target: 'body_orientation_wait' } // doesn't work as I wanted
    bodyOrientationInstructionsDuration: { target: 'body_orientation_wait' }
  },
  meta: {
    // wanted to avoid referencing machine interpreter in showTextMessage
    // when looking up message for current exercise, so I can use lookup id
    // which I choose to be the same as state id in this case
    state: 'body_orientation_instructions'
  }
}

and in machineOptions I have functions:

actions: {
  showTextMessage: function(context, event, actionMeta) {
    console.log('showTextMessage for '+ actionMeta.state.value);
    context.text_message = exercise.messages[actionMeta.state.value].text;
    // context.text_message is used in web app to display to user
  }
}

But delay function doesn't work like action - no access to current state meta data (3rd parameter exists but it is some kind of internal stuff), and interpreter.state has previous state, not current state. I wanted to be able to use one fuction

delays: {
  messageDuration: function(context, event) {
    return exercise.durations[delayMeta.state.value]; // can't work, no third parameter like in action
    return exercise.durations[machineService.state.value]; // don't work, contains previous state
  }
}

Workaround - use seprate state specific function for every delay, so no need to access current state

delays: {
  bodyOrientationInstructionsDuration: function(context, event) {
    return exercise.durations.body_orientation_instructions;
  }
}

ilmarmors avatar Jul 21 '21 10:07 ilmarmors

It seems fairly reasonable that since we have meta for actions, we should do something similar for delay calculation. @Andarist, @davidkpiano thoughts?

mattpocock avatar Jul 21 '21 11:07 mattpocock

Note that actionMeta.state might not always be what you think it is. For assign actions it's the "source state" (the state before transition) and for custom actions (functions) it's the state in which the machine lands after resolving all microsteps (after resolving all raised and null events) in a transition. This is not necessarily how it should work but it is how it currently works. We plan to refactor this for it to be more predictable.

Since the value of meta.state will always be somewhat misleading due to how our internal algorithm works... I would like to explore removing meta.state entirely. For that, I need to understand how people are using it because we should provide a better way to achieve the goals which have been enabled by it.

In this instance - you seem to want to provide per state (or rather maybe - per delay) configuration for a particular "executable content" (the delay "resolver" in this case). Would allowing the pattern presented below solve your problems?

// in the machine config
after: [{ type: 'calculateDelay', extraProp: 100 }]

// in the implementations object
calculateDelay: (ctx, event, { delay }) => {
  // do smth with `delay.extraProp`
}

Andarist avatar Jul 22 '21 12:07 Andarist

Is there any info in documentation, which describes what is the sequence of microsteps during state transition? Or it is too implementation specific so it shouldn't be specified and people wouldn't rely on that information? Although I would say that clear logical sequence is good thing to be described.

For my case possibility to pass additional function argument (object that can contain many properties) is fine. That would be something like you wrote (and could be applicable not only to delay but also to actions, guards and any other function):

body_orientation_instructions: {
  entry: [
    'showTextMessage': { messageId: 'body_orientation_instructions' }
  ],
  after: {
    messageDuration: { target: 'body_orientation_wait', { stateId: 'body_orientation_instructions' } }
  }
}

and

actions: {
  showTextMessage: function(context, event, params) {
    context.text_message = exercise.messages[params.messageId].text;
    // context.text_message is used in web app to display to user
    // this example is simplified as probably the same effect can be achieved with assign
    // but you might want to do more complex calculations to calculate context var value
  }
},
delays: {
  messageDuration: function(context, event, params) {
    return exercise.durations[params.stateId];
  }
}

Regarding state and actionMeta. I see that there is good reason to know the state from which function is called, and no real reason not to provide that. You can call it somehow different than state, for example,callState so its value is clearly defined and won't depend on microstep sequence of state change

ilmarmors avatar Jul 22 '21 12:07 ilmarmors

That would be something like you wrote (and could be applicable not only to delay but also to actions, guards and any other function):

Yes, this is already supported for some things (mainly for actions) - we should look into supporting this pattern for all the implementation "types" (guards, services, etc)

Regarding state and actionMeta. I see that there is good reason to know the state from which function is called, and no real reason not to provide that. You can call it somehow different than state, for example,callState so its value is clearly defined and won't depend on microstep sequence of state change

Right - I think what usually people want here is some kind of representation of the state node from which the thing is called (exactly like you have described it). The current meta.state doesn't deliver that (it is sometimes that - depending on the taken transitions, but that's mostly coincidental).

Is there any info in documentation, which describes what is the sequence of microsteps during state transition? Or it is too implementation specific so it shouldn't be specified and people wouldn't rely on that information? Although I would say that clear logical sequence is good thing to be described.

Quite good documentation on this can be found in the SCXML spec here. There are some subtle differences between their implementation and ours but conceptually we aim to follow that closely (while reserving the right to diverge from it where it makes sense to us, for a variety of reasons). We might want to document our exact algorithm in the future but that's probably something that we should revisit after releasing the next major version - v5.

Andarist avatar Jul 22 '21 19:07 Andarist

is meta available somewhere in v5?

ciekawy avatar Aug 28 '23 11:08 ciekawy

All arguments can be destructured from the arguments object that we call your functions with in v5. state (or anything like it) is not part of this object though.

I firmly believe that people, usually, don't want the current state there. I'd love to hear more about your use case to suggest an alternative. We are also constantly reevaluating this decision - so who knows, maybe something like it will get added there in the future.

Andarist avatar Aug 28 '23 12:08 Andarist

I'm very new to xstate and I'm only trying to design machine to have better control over app navigation than what typical router (e.g. Angular) offers. I find xstate very powerful and flexible, probably offering much more than I actually need so still not sure whether this may be an overkill in my case.

In particular let's say an app has reading feature, comprehension quiz, jigsaw puzzle. These are sll states and there is also start state in which first some particular kind of puzzle is presented and after that there is some guide message presented - as action - the guides are presented in many places of the app - via events - and often ideally I'd just use SHOW_GUIDE event and the action could know what message to present based on the state in the first place. Repeating the state when sending/raising the event adds extra verbosity to the already quite verbose machine config.

ciekawy avatar Aug 28 '23 16:08 ciekawy

Made a pull request to add .stateNode for this and many other use-cases @ciekawy: https://github.com/statelyai/xstate/pull/4211

davidkpiano avatar Aug 28 '23 19:08 davidkpiano