State Charts: To Guard, or to Guard Action — Part 1

Matthew Jones
4 min readFeb 20, 2019

--

So, you’ve decided to use xstate (or any other similar library). You’re looking to tie your architecture together with a state graph, nice and neat. You finally finish your graph, and you sit back and marvel how you’ve distilled a universe of complexity down into some javascript objects.

It’s all right there.

You keep thinking about the code, and how the states are going to interact with each other. You maybe start looking at the action docs.

The State instance returned from machine.transition(...) has an .actions property, which is an array of action objects for the interpreter to execute:

Cool — so for each state transition we get an array of actions. Makes sense.

When interpreting statecharts, the order of actions should not necessarily matter (that is, they should not be dependent on each other). However, the order of the actions in the state.actions array is:

1. onExit actions - all the exit actions of the exited states, from the atomic state node up
2. transition actions - all actions defined on the chosen transition
3. onEntry actions - all the entry actions of the entered states, from the parent state down.

The state transition event is received; first, the onExit actions for the current node fire; then, the ‘regular’ transition actions fire off; and then, any onEntry actions for the new state node fire off. Makes sense.

The interesting thing to me is that the onExit, onEntry stuff necessitates some modicum of an order. If a function is set to run while exiting a state node, it really should fire before the entry ones.

Looking at the UML Spec

Let’s pause here and take a look at the UML spec.

In extended state machines, a transition can have a guard, which means that the transition can “fire” only if the guard evaluates to TRUE. A state can have many transitions in response to the same trigger, as long as they have non-overlapping guards; however, this situation could create problems in the sequence of evaluation of the guards when the common trigger occurs.

The UML specification[1] intentionally does not stipulate any particular order; rather, UML puts the burden on the designer to devise guards in such a way that the order of their evaluation does not matter. Practically, this means that guard expressions should have no side effects, at least none that would alter evaluation of other guards having the same trigger.

OK, so we kind of have our answer here: basically, the spec can’t ensure action order because of uncertainty in using guards. So it advises you to architect things in such a way that their order does not matter.

Sounds kind of circular.

What Do We Really Want?

If you’re like me, you’re lazy, and ultimately just looking for simplicity. When I hear talk about guards causing overlapping states whose action orders cannot really be inferred, I get scared. And then there’s Delays… like, hardcoded millisecond delays, which is pretty cringe in anything other than academia. These actions are all going to be promises.

Anyway, the whole thing starts to look like a DSL. That’s probably not what we want.

Ahhh. State Charts — you were supposed to save me. Now I feel like I’m drowning. How we can throw ourselves a life raft? How can we keep this all in check?

Well, going back to the quote above, if the burden is on you, in user land, then one option is to completely forget about guards, delays, and all that stuff, and instead use the much better user-land tools at your disposal with the aim of still keeping the encapsulation and spirit of a state chart.

That’s right, folks. We’re talking about javascript here.

Guard Actions

Say we are on state tired and we want to move to wired . We can’t purchase a coffee without any money. We could use a guard.

const getCoffeeMachine = Machine(
{
id: 'GetCoffee',
initial: 'tired',
states: {
tired: {
on: {
BUY_COFFEE: {
target: 'wired',
cond: (ctx) => {
return ctx.hasCash;
}
}
}
},
wired: {
on: {
SOME_TIME_LATER: 'tired'
}
}
}
}
)

Or, we could use an action. A guard action, if you will.

const getCoffeeMachine = Machine(
{
id: 'GetCoffee',
initial: 'tired',
states: {
tired: {
on: {
BUY_COFFEE: {
target: 'wired',
actions: ['checkHasCash']
}
}
},
wired: {
on: {
SOME_TIME_LATER: 'tired'
}
}
}
}
)

But how would this work? Well — and this is where is gets a little crazy — the action would have the ability to transition back.

In this case, back to the tired state. In effect, that’s what guards do: they prevent you from moving to a state you would otherwise move to. So long as the guard action functionally does that, which it does, then even the UML spec doesn’t seem to care too much about the whole action ordering/guards thing and your implementation thereof.

The State Chart puritans might be thinking something along the lines of…

Dude, the whole reason for state charts is to organize all the state logic in one place. You’ve moved the guard logic into actions, effectively breaking the encapsulation — all that logic is somewhere else now.

And that isn’t wholly incorrect, but let’s be real here:

return ctx.canSearch && event.query && event.query.length > 0;

It probably doesn’t matter where stuff like this lives. Be it guards acting on context, or actions acting off of your app’s actual state.

Tying it All Together

There is fairly ideal abstraction that we can use to make all of this work. Where we can start to view the state chart as an “engine” of sorts that can self-regulate itself at runtime.

That’s what we’re going to look at in Part 2.

--

--