Statecharts: Updating UI state

Luca Matteis
5 min readApr 14, 2018

--

State, or simply data, is a crucial ingredient for User Interface (UI) development. Your data is a 1-to-1 mapping to what the user is actually seeing. Finding ways to structure this data, and more importantly, ways to update it, is a very relevant issue during the development of software.

One prominent way of updating state that was discussed in my other article “On User Interface Development: appending to the event log” is by using a so called event log. This idea comes from Event Sourcing where one saves the events that led to that state rather than the state itself. Updating such state would simply constitute an append of a new event to the log.

Deriving data from the event log is a reduce operation

Another way of updating state can be achieved with the help of a state machine, or more practically, a Statechart (aka state machine with hierarchy).

With a Statechart, we still have an event log, but instead of deriving the data immediately, as is depicted in the earlier image, we pass the events to a Statechart before deriving the data, which then outputs a new event log.

The Statechart has memory, it remembers where you left off from the last event you gave it. Passing events through it allows us to control whether certain events should or shouldn’t be considered for the reduce operation of deriving our UI data.

The way we control “which events to output” is done by defining them directly in the Statechart definition via entry, exit and transition outputs. Here’s an example:

{
Init: {
on: { BUTTON_CLICKED: 'RequestRunning' }
},
RequestRunning: {
onEntry: START_HTTP_REQUEST,
on: { HTTP_REQUEST_RESOLVED: 'Resolved' }
},
Resolved: {
onEntry: [SHOW_USER_INFO, HIDE_NAVIGATION]
}
}

When we pass as input BUTTON_CLICKED to the aforementioned statechart, it will internally know it’s in the Init state and therefore transition to the RequestRunning — please note that this is the statechart’s state, different in context from our UI state.

Because RequestRunning has onEntry defined, START_HTTP_REQUEST will be the event that is output from the statechart for this particular transition.

statechart('BUTTON_CLICKED') // output: START_HTTP_REQUEST

Again, this is only possible because the statechart remembers the last state it was in. In fact if we currently pass itBUTTON_CLICKED again, no output will be provided because the statechart is in the RequestRunning state where BUTTON_CLICKED isn’t handled.

statechart('BUTTON_CLICKED') // Nothing will output this time

Data as a projection of a Statechart

Intuitively Statecharts take as inputs events, and output other events. The outputs are called actions in the Statechart formalism, but for simplicity’s sake the naming differences are not crucial for this article’s scope.

A Statechart can also output its own state (remember I mentioned that it has its own memory).

statechart('BUTTON_CLICKED') // {
// events: START_HTTP_REQUEST,
// currentState: RequestRunning
// }

With the ability to output its own internal state, we can use a statechart to define several parts of our UI. States in a statechart can essentially map quite naturally to the shape of our UI state.

Taking the earlier form example, we defined it as follows.

{
name: '',
surname: '',
rememberMe: true,
required: []
}

Let’s try to redefine some parts of this data using a Statechart instead.

{
parallel: true,
states: {
RememberMe: {
initial: 'True',
states: {
True: { on: { TOGGLE_REMEMBER_ME: 'False' } },
False: { on: { TOGGLE_REMEMBER_ME: 'True' } },
}
},
Required: {
parallel: true,
states: {
Name: {
initial: 'False',
states: {
True: { on: { VALID_NAME: 'False' } },
False: { on: { EMPTY_NAME: 'True' } },
}
},
Age: {
initial: 'False',
states: {
True: { on: { VALID_AGE: 'False' } },
False: { on: { EMPTY_AGE: 'True' } },
}
},
}
}
}
}

When we initialize the above statechart definition, it will output this data:

{
RememberMe: 'True',
Required: {
Name: 'False',
Age: 'False',
}
}

Looking back at our initial definition, this structure can be used instead of our rememberMe and required attributes.

Here’s a possible final shape of our state projected from the statechart definition above:

{
name: '',
surname: '',
// rememberMe: true, <--\
// required: [] <-- Replaced by the statechart output below
statechart: {
RememberMe: 'True',
Required: {
Name: 'False',
Age: 'False',
}
}
}

Let’s see what happens when we trigger an event. As stated earlier the statechart will now produce two outputs, the new event long, along with it’s new internal state.

Booleans, loading states and more

For things like booleans and loading states, projecting data using a statechart is quite natural and makes lots of sense. However, for anything else that is of dynamic nature such as “list of comments” or “response from a web server” cannot be easily defined using a statechart.

For these other types of data, letting the statechart output appropriate events such as COMMENTS or HTTP_REQUEST_RESOLVED makes perfect sense. Then one can use the usual reduce operation from event sourcing to build the next state.

Although the example of the statechart provided above is quite convoluted, one cannot hide the fact that such definition can also be easily visualized. The flat JSON structure we started with doesn’t tell us anything about how those values will change. On the other hand in the statechart definition we can see exactly which event causes the data to change.

Furthermore one can easily construct the final statechart structure using functions to avoid repetition of states in the chart.

Required: {
parallel: true,
states: {
Name: {
initial: 'False',
states: boolean(VALID_NAME, EMPTY_NAME)
},
Age: {
initial: 'False',
states: boolean(VALID_AGE, EMPTY_AGE)
},
}
}

And since it’s just JavaScript, why not hide the implementation of substates and make them easily reusable.

{
parallel: true,
states: {
RememberMe: {
initial: 'True',
states: boolean(TOGGLE_REMEMBER_ME)
},
Required: ...required()
}
}

Conclusion

If you’re interested in learning more on the subject of Statecharts I’ve written some posts about them:

And several other important resources such as:

--

--