While the order of actions in XState is fairly clear, how
assigns are handled in them is non-obvious.
If you have a scenario where you try and utilize
context in one action, then
assign a new value to
context, and then try and utilize the new context in another action, you'll find that actually both actions received the new
context as their argument. Why does this happen?
This happens because
XState.Machine.transition is a pure function. In order to remain pure, it does not run the actions, it accumulates them into an
actions array that is returned in the next state object. Since
transition does not run any side effects, it must calculate the next
context object and return this on the state object as well. Thus, any action returned is then called in the interpreter with the next context, not the context assumed to exist at the time and order the action was written.
Instructor: [00:00] Here I have a contrived example double counter machine to demonstrate how action order affects assigns in context. This machine only has one state idle, and a response to one event, incrementCountTwice. This event fires off four actions. One console log before to show the count, two calls of increment count, and one console log after to show the count.
[00:25] We scroll down here we can see what the incrementCount function is, it's an assigned function to Context, that takes our Context and gets the current count and adds 1. Let's call this and see what happens. Opening up the console, we see that we actually got before 2 and after 2. That's odd, why did that happen?
[00:44] To understand this we actually have to think about the machine transition method. I'm going to write some pseudocode up here to help it make sense. Machine.transition is a pure function. It's a function of the state and the event. In order for this to be pure function, we have to get back the same object as our next state every time we pass in this particular state and this particular event.
[01:10] The way it does this, is it returns the next context completely, but taking all the actions, the state's exit actions, the transition actions, and the next state's entry actions, and filtering out any assigns that might happen in them, and merging them into the next context object. It looks like this. Since all the assigns are batched together to give us that next context object, all we're left with are any actions that aren't assigns, so it's almost as if these actions are ordered in this way.
[01:39] All the assigns first, and then any of the other actions in the order that they were declared. Knowing this, we can remove the pseudocode and we can make a change to how our double counter machine works to actually work as we expect. What we realize is we actually have two kinds of contexts, we not only have a count, we have a previous count.
[01:59] Since we have a previous count, it makes sense that we make an action that assigns this value during this. We have incrementCount, we can also have setPreviousCount. We can take this, and add this to our actions. Now that I've added that, I can update this to previousCount, save the machine, and call increment twice. I should be able to open the console, and we now see a before of 0and an after of 2.