menu

Statecharts

The Statecharts community on Spectrum is (along with spectrum) MOVING TO OTHER PLATFORMS: For statecharts discussions in general go to Statecharts Discussion on GitHub or Gitter(https://gitter.im/statecharts/statecharts). For XState-specific questions, go to the XState discussion forum for Q&A or the Stately discord chat to chat.

Channels
Team

Coupling between view and statechart

May 25, 2019 at 8:34am

Coupling between view and statechart

May 25, 2019 at 8:34am
This comes up from time to time, so I thought I'd just make a post where we can explore different opinions. I have a preference when it comes to the level of coupling between the statechart and the user of the statechart (often a UI). I find that if there is too much coupling, it limits my creative freedom when designing the statechart. So by default, I generally avoid any mention of the names of the states themselves outside the statechart. This means that my view code never knows which state we're in.
The common practice is to—in your view code—introduce a tight coupling to the structure of the statechart. Here, a "state.matches" call decides which view to use:
// view
state.matches("foo.bar.coo") ? <Coo/> : <NotCoo>
// js
const machine = { states: {
foo: { states: {
bar: { states: {
coo: {}
}}
}}
}}
offered an improvement: Co-locate the coupled parts, at least. Introduce a level of indirection between the view and the statechart. Here, "cooing" is true if we're in the "foo.bar.coo" state:
// view
viewflags.cooing(state) ? <Coo/> : <NotCoo>
// js
const viewflags = {
cooing: state => state.matches("foo.bar.coo");
};
const machine = { states: {
foo: { states: {
bar: { states: {
coo: {}
}}
}}
}}
This solves the coupling problem, but still requires you to keep the "viewflags" up-to-date, which means every time you want to make a structural change to the statechart, you have this extra mental effort of figuring out if all the viewflags are correct.
I prefer to put this information directly in the statechart, and (since the original "viewflags" is essentially a set of booleans) we can use an activity:
// view
state.activities.cooing ? <Coo/> : <NotCoo>
// js
const machine = { states: {
foo: { states: {
bar: { states: {
coo: {
activities: "cooing"
}
}}
}}
}}
In this last example, if I decide that the "bar" state need to be wrapped into a parallel state to model some other behaviour, I can do that at a whim.
  • It might mean that since it's so easy to restructure the statechart, I'm more likely to do so.
  • I can decide to rename a state without consequence (all references to any state are contained within the statechart
  • If test code follows the same model, rearranging the statechart also doesn't affect test code.

May 25, 2019 at 10:05am
I like that approach (with activities). My personal approach is that when starting out (or prototyping), I will use state.matches(...), and then when the statechart gets more stable, I can switch to state.activities.something.
This is also helpful when using non-declarative UI frameworks (or no frameworks at all):
activities: {
cooing: () => {
const cooEl = document.querySelector('.coo')
cooEl.dataset.visible = true;
// Remember to add clean-up!
return () => delete cooEl.dataset.visible;
}
}
Edited
like-fill
2
  • reply
  • like
Not to nitpick but ain’t ‘cooing’ still leaked outside of the statecharts? Although it does look better than the full ‘foo.bar.coo’ version.
Edited
like-fill
1
  • reply
  • like
Maybe its the "cooing" that is leaking from the view into the statechart.
Edited
  • reply
  • like
Hi! 👋
That's such an interesting topic!
In the end, I believe that the machine and the view will always be somehow coupled. Otherwise, they wouldn't be part of the same application. For example, if the machine is either in the "cooing" or "not cooing" state, it would make no sense for the view to be completely decoupled from these states and display whether we're biking, swimming or sitting. The view is meant to reflect the state of the state machine, and the state machine drives the behavior of the view. The most interesting question is: How to achieve that in a way that in a way that's robust and easy to maintain?
When we look at the first example, we can notice that the view depends on the current state of the state machine (foo.bar.coo) and uses a boolean value (the result of state.matches) to selectively display one or another variant (state.matches("foo.bar.coo") ? <Coo/> : <NotCoo>).
The view from the second example also uses a boolean value (the result of viewflags.cooing) to selectively display some of its variants. This boolean value is calculated by a function that depends on the current state of the state machine (cooing: state => state.matches("foo.bar.coo")).
In the third example, the view depends on the currently running activity of the state machine (state.activities.cooing ? <Coo/> : <NotCoo>).
All three of them share a very similar approach, where the view is driven by conditional statements (boolean ? onTrue : onFalse) derived from the current state of the state machine, which also reflects the same domain (.coo in the machine, <Coo/> in the view). It could be called subtractive behavior control, because it's up to the view function to perform state inspections with conditional statements (or expressions) and carefully disable some parts of its code based on its knowledge about the possible states of the state machine and their meaning. For the view function, the state machine is nothing but a suggestion. We can still easily forget about an if and accidentally display both views simultaneously (<Coo/> <NotCoo/>) or never really interpret the change (<Coo/> without an if).
A different approach would be to flip this architecture upside down to get an additive behavior control, where instead of disabling existing code based on the state we don't associate it with that state at all. That way the whole codebase may have no boolean flags at all and there's no risk we will accidentally render many states simultaneously. In this technique the dependencies are also inverted. Some parts of the state machine, especially the leaves, will depend on the API of the view function. Usually it's not a problem, because a single machine will rarely be driving very different APIs in the same time, like React and the low-level, mutable DOM API. But when it needs to do it, the higher level parent nodes may be extracted into reusable state machines and then composed with two different sets of sub-machines, minimizing the dependency on the view functions to the same level as in the examples above.
Here's an example, where React view functions look like this:
RENDER: ({ action: { dispatch } }) => (
<Coo onClick={() => dispatch({ type: "TOGGLE" })} />
),
They are driven by a React-agnostic, separated machine with a Coo <-toggle-> NotCoo graph where Coo and NotCoo are external, reusable nodes. What's insteresting about this implementation is that:
  • There's not a single boolean checking like something ? Coo : NotCoo.
  • The main machine is unaware that we're using React to render the view
Cheers! 🙂
like-fill
3
  • reply
  • like
Look at it this way: What do you need to do if you want to change the behaviour so that you delay the display of coo by e.g. 1 second? In my case, I just move the 'cooing' activity to a substate of 'coo':
// view
state.activities.cooing ? <Coo/> : <NotCoo>
// js
const machine = { states: {
foo: { states: {
bar: { states: {
coo: { states: {
soonCooing: {
after: { 1000: "reallyCooing" }
},
reallyCooing: {
activities: "cooing"
}
}}
}}
}}
}}
Only the statechart needed to change. In the other two examples I'd have to update the call to state.matches too.
And the reason I used a boolean as an example was because of the booleanness of state.matches() which is often used as glue between statecharts and states. My goal is neither to introduce nor to get rid of booleans. Activities are just one of many ways for a statechart to communicate to the world what should be shown. Activities are the simplest way, but you could use extended state, or even actions. But I do give you that using activities might result in a dense set of booleans, so maybe use an "extended state" that describes the "desired view"?
Edited
like-fill
1
  • reply
  • like
I've gone pretty much completely in the opposite direction, and built the state machine right into the component's class. I even wrote a library to make it fairly easy. The library is here: https://github.com/jrobinson01/sm-element and here's an example component to fiddle with:
As noted here in the chat, it didn't feel quite right to couple the machine with the UI at first, but I'm finding it pretty decent in practice. The SMElement base class is flexible in that you can either use this.currentStateRender(this.data), or this.isState('name', this.currentState). I have tended to use the former more often. Also relevant to the conversation, my machine implementation doesn't support nesting states. But, since each component is it's own machine, I haven't found much of a need to support nesting states when you can just nest components instead.
I really like the idea of state-machine driven UI, as well as web components. I don't think my solution is perfect by any means. It feels more like a good first stab. I'm absolutely open to criticism and ideas to make it more useful or easier to use in general.
Edited
like-fill
2
  • reply
  • like
Hi , that's some interesting example here! I like this approach where it's the state that selects the proper render function instead of the render function interpreting the state.
like-fill
1
  • reply
  • like
I like it too. It's not as pure as I'd like in that, in practice you usually end up with some sort of 'global' markup that gets rendered (like the button in my example). What I was really going for was to have a) the data for the component (a reflection of the properties' values for a given state) the result of a pure function (transition effects) and b) the ui a result of a pure function (the current state's render function). What makes that hard to achieve is that data can and will be set from the outside. For example, the common architecture of data down, events up. You have a parent component that might get data from a redux store or fetch response, and pass it down through children components. What I did to sort of work with that, was to have each property change send a <property>-changed event to the state machine.
like-fill
1
  • reply
  • like
Good question !
Activities seem to be an interesting way how we can build another set of states on top of the states defined by the state machine. It may be a valuable escape hatch from the automata-based programming paradigm, but it may also cause a bit of confusion. Rendering <NotCoo/> when we're in foo.bar.notCoo feels totally right. However, rendering the same <NotCoo/> when we are in foo.bar.coo.soonCooing is a bit odd, because we have already left foo.bar.notCoo and now we are in the foo.bar.coo state.
If the desired behavior is to stay a bit longer in the previous state before displaying <Coo/>, then I believe each of these previous states should handle this delay. Depending on their complexity and desired behavior while waiting it may be as simple as dispatching an action with a delay, or a bit more complex with an extra node like 'AboutToLeaveNotCoo' when we want to take care of the user input while the timer is running (like what happens when something is clicked or edited within that 1 second).
  • reply
  • like
what I found to be a convenient and in the same time coherent solution were lenses. In JS we have this wonderful FP library https://ramdajs.com and it comes with support for lenses. They allow us to adapt the size and shape of the state almost any way we want. A child component may consume and update what from its perspective looks just like a very simple, small, flat object, while in reality it's part of a big, global, nested state object. It's like having the possibility to store both the application-wide state (for example, the currently logged in user, like in Redux or React Context) and simple local component state (which in reality is just a projection of the global state through a telescope, that is a set of lenses).
I wrote a bit more about lenses here - https://lukaszmakuch.pl/post/lenses
I used lenses to implement todo items in the TodoMVC app. A single item, when edited, simply reads and alters the value of the context.content property. This value is then applied to the proper item in the todos array that holds all the entries in the global state.
  • reply
  • like
The UI and the behaviour of the UI don't need to converge. Consider a simple text editor that saves text to a server as it is being typed.
  • When the user types a character, you might want to immediately save it. However UX doesn't want to show that in the UI immediately but after a short delay.
  • After perhaps 100ms UX wants the UI to start showing "Saving..." for a minimum of 1 second, even though the save has already completed.
  • Then, 1 second after the text is saved, UX wants the UI to say "All changes saved".
  • If the user continues typing while "Saving..." then the "Saving..." stays up, and the text is saved continuously, possibly with some debouncing.
Here the UI has two important "states": saving and saved. But the behaviour is easily much more complicated, and probably has more than two top level states.
Then imagine that the tech team says that they can't handle saving on every keystroke, so they want to delay the saving to at the earliest, 200ms after a keystroke, and at most 2 seconds after a keystroke, i.e. every two seconds when a user types continuously. UX decides that the UI should not change its behaviour of showing "Saving..." after 100ms. This new requirement would probably change the statechart quite a bit. The ability to sprinkle which UI to show on top of the statechart is quite powerful.
Statecharts are primarily about behaviour. The typical definition of behaviour is how a thing reacts to events. Which view to show is but one thing controlled by the statechart.
So I disagree. I think it's not odd at all to render the NotCoo UI when you're in the foo.bar.coo.soonCooing state. I could argue the converse that _not leaving the notCoo state when the COO event happened is just as odd. It depends. It depends on the other behavioural aspects of coo and the other states.
like-fill
4
  • reply
  • like
that's an interesting example (and pretty common). I'm thinking you'd use some sub charts. Your top level chart might have states like clean,dirty, saving and saved. I can picture then a child chart that's only concerned with whether the user is typing or not. When the child is not typing, the parent decides to save or not. What I think I'm getting at, is that maybe the solution isn't so much trying to fit everything into one chart and UI, but to break it into smaller pieces, so UI bits can overlap based on more discreet states.
Thanks for the links! Lenses look really cool, as does Ramda as a whole. I've played with functional stuff a bit but have never done anything serious with it. I do really like the concepts though.
I was thinking all day about my component lib and my issue with data changing from the outside. Currently, when a property is changed on the component (generally from the outside, via lit-html bindings), I call a setter for that property and update the component's "internal" data property. In the same process I send <property>-changed event to the state machine that it can act upon. What I'm considering doing, is following that same pattern except with one key difference. When a properties setter is called, I'd just send the <property>-changed event to the machine. If the machine's current state handles that event, then it can act upon it by using an effect function that returns either the entire current state, or a delta (most likely, the desired property's new value). I think this will have some useful benefits. Namely that a components data can only be changed if the current state allows it.
Edited
like-fill
3
  • reply
  • like

May 26, 2019 at 5:59am
Splitting it up is definitely a way to go. The way I've framed the problem, you could go with two regions, one for the UI ("saving", "saved") and one for the saving behaviour, (probably more complex) with the UI region's states guarded by timers and "in saving.whatever". It illustrates my point nicely, thought, that the UI "state" and other behaviours of the component don't always align nicely along state boundaries.
While an initial statechart might be drafted from UI mockups, I think that you pretty quickly want to model the behaviour slightly differently. At least, I think it's better if doing so is easy, and decoupling the UI from the statechart (using activities, actions, external state) is a good way of lowering the friction to make changes to a statechart.
like-fill
1
  • reply
  • like
When there's some background activity I want to control, I like to place it in an orthogonal region that reacts to the events emitted by the state machine controlling the UI.
With reusable state machines it's even possible to extract the part of the behavior (statechart) common for both the server and the client and reuse it in both places. We just need to provide the more detailed states with logic specific to the environment.
It all boils down to when do we want to use a state machine (boxes and arrows), and when do we want to use data (strings, booleans, maps etc.).
I believe that the reason why do we often opt out from the automata-based programming paradigm is that we still don't have extremely flexible and powerful tools to model some of the behaviors.
like-fill
2
  • reply
  • like

May 27, 2019 at 12:53pm
I may have missed the boat with activities, but I took your decoupling comments to heart early on and used context/actions to solve the decoupling. I am now in the process of splitting my prototype into lots of reusable Vue plugins, and had I not decoupled as you recommended I would have had a mess. I am now able to rapidly break my project into reusable plugins without worry, so I totally owe you a huge thank you.
like-fill
5
  • reply
  • like

June 10, 2019 at 6:15am
I've been thinking, extended state can also result in coupling between the statechart and the UI. There are any best practices you follow to avoid coupling them too much? I believe one good advise is to keep the extended state as flat as possible, e.g., context.foo instead of context.foo.bar.baz.
  • reply
  • like
The extended state will very likely be a part of the contract between the statechart and its user. As with any API, it's a good idea to keep it well defined. I don't see a problem with nesting as such, though.
  • reply
  • like
I think it might be possible to hide the extended state behind the actions, though, i.e. that an action is called with arguments from the extended state.
  • reply
  • like

June 14, 2019 at 12:37am
instead of state.activitities.cooing why not state.meta.coo? benefits:
  • meta is an object, accepts any static value
  • if you need a boolean, you don't need to pass an empty implementation like with activities
  • meta is bit more semantic than activities for viewflags, although you can use both
Edited
like-fill
2
  • reply
  • like

August 11, 2020 at 6:06pm
I like that approach (with activities). My personal approach is that when starting out (or prototyping), I will use state.matches(...), and then when the statechart gets more stable, I can switch to state.activities.something.
This is also helpful when using non-declarative UI frameworks (or no frameworks at all):
activities: {
cooing: () => {
const cooEl = document.querySelector('.coo')
cooEl.dataset.visible = true;
// Remember to add clean-up!
return () => delete cooEl.dataset.visible;
}
}
I like this approach , however you mentioned activities are on their way out (in V5). Would you do something similar using invoke and if so: how? If not, then how would you do this in V5?
  • reply
  • like
Yes; the above can be done in V4 using invoke like this:
invoke: {
id: 'cooing',
src: (ctx, e) => () => {
// ...
return () => delete cooEl.dataset.visible;
}
}
like-fill
1
  • reply
  • like

August 12, 2020 at 12:30pm
Thanks ! However, if you (are dealing with a framework and) really only need a binary toggle to trigger the rendering (elsewhere) of a custom piece of HTML, then this feels a little bloated compared to what activities let us do previously 🤔
Instead of:
activities: 'cooing'
You would now get something like the following (I guess):
invoke: {
id: 'cooing',
src: () => () => {}
}
Or will there be a nicer way to accomplish this in V5?
  • reply
  • like
Yes, (tentatively) it will look like this:
invoke: invokeActivity('cooing')
// or inline...
invoke: invokeActivity((ctx, e) => {
// ...
return () => delete cooEl.dataset.visible;
})
like-fill
2
  • reply
  • like
That looks perfect to me! I can't wait :-)
  • reply
  • like

January 2, 2021 at 8:12am
Using activities or meta is a far much better approach because it makes it possible for different states to render the same thing without having to do multiple checks. Which also makes refactoring really easy. And for a large project, things can get crazy really fast.
  • reply
  • like
Show more messages