State Driven Development for User Interfaces (Part 2: Finite State Machines)
Nimmo
Posted on February 4, 2018
Note: This post assumes a basic familiarity with the way Redux works, although the core concept doesn't really lose anything without that understanding. Still, it might be worth checking out Explain Redux like I'm five if you are scratching your head in the second section. I'll also be using React, but the idea being presented here doesn't require React.
In order to implement the technique discussed in my previous post, it's especially helpful to be able to think about our applications in terms of a Finite State Machine
.
For anyone unfamiliar with FSMs, as the name suggests, they can only have a finite number of possible states, but crucially can only be in one of those states at any given time.
Consider for example, a door. How many states could it be in? It probably initially looks something like this:
LOCKED
UNLOCKED
OPENED
CLOSED
That's definitely a finite list of possible states our door can be in, but you may have noticed that we've made a mistake here. Do we really need a separate state for CLOSED
and UNLOCKED
? Well, if we're looking to be able to say that our door can only be in one of a finite number of states, then I'd say we probably don't. We can assume that CLOSED
means UNLOCKED
, since we know our door can't (meaningfully) be LOCKED
and OPENED
at the same time. So perhaps our states should look more like this:
LOCKED
CLOSED
OPENED
Now we've figured out our states, we'd probably like to know how our door will transition from one to another, right?
Here's a very simple state transition diagram for our door:
In this case, the initial state
doesn't matter so much (by which I mean any of these states would have been fine as the initial state), but let's say that the initial state of our door is going to be CLOSED
.
And, you know what, we don't really care about the transitions that just go back to their previous state either, do we? They're all just showing actions that aren't available in the current state, after all:
Now, we don't really spend a lot of time at work building virtual doors, but let's say that we think we've identified a gap in the market, and we were looking to fill it by building our door into a web application.
We've already done the first step: figuring out our states and our transitions. Now it's time for a little bit of code.
Enter Redux
Saying "Redux isn't necessary for this" is, of course, redundant. But since it just happens to be perfect for what we're trying to achieve here, that's what we'll be doing. So, we can take our diagram, and use that to write our store
file:
export
const actionTypes = {
OPEN: 'OPEN',
CLOSE: 'CLOSE',
LOCK: 'LOCK',
UNLOCK: 'UNLOCK',
};
export
const stateTypes = {
OPENED: {
name: 'OPENED',
availableActions: [actionTypes.CLOSE]
},
CLOSED: {
name: 'CLOSED',
availableActions: [actionTypes.OPEN, actionTypes.LOCK]
},
LOCKED: {
name: 'LOCKED',
availableActions: [actionTypes.UNLOCK]
},
};
const initialState = {
_stateType: stateTypes.CLOSED,
};
export
const open =
() => ({
type: actionTypes.OPEN,
});
export
const close =
() => ({
type: actionTypes.CLOSE,
});
export
const lock =
() => ({
type: actionTypes.LOCK,
});
export
const unlock =
() => ({
type: actionTypes.UNLOCK,
});
const door =
(state = initialState, action) => {
const actionIsAllowed =
state._stateType.availableActions.includes(action.type);
if(!actionIsAllowed) return state;
switch(action.type) {
case actionTypes.OPEN:
return { _stateType: stateTypes.OPENED };
case actionTypes.CLOSE:
case actionTypes.UNLOCK:
return { _stateType: stateTypes.CLOSED };
case actionTypes.LOCK:
return { _stateType: stateTypes.LOCKED };
default:
return state;
}
};
export default door;
Now we have our reducer
, which is a coded version of our state transition diagram. Did you notice how easy it was to go from the diagram to the code here? Of course, the level of complexity in this example is very low, but I'm hoping you can see why we're finding this so useful.
The only thing that's in here that's "unusual" is the use of _stateType
, which you can see also contains a list of available actions in a given state. The usefulness of this might be questionable, but I believe that it offers both an extra level of documentation for the reader of this code, as well as a potential safety net against errors when transitioning from one state to another.
Implementation
Wiring this together into a container to hold our door, it looks like this:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
stateTypes,
close as closeFunction,
open as openFunction,
lock as lockFunction,
unlock as unlockFunction,
} from './path/to/store';
import OpenedDoor from './path/to/opened_door';
import ClosedDoor from './path/to/closed_door';
import LockedDoor from './path/to/locked_door';
const Door =
({
_stateType,
open,
close,
lock,
unlock,
}) => {
switch(_stateType) {
case stateTypes.OPENED:
return (
<OpenedDoor
close={close}
/>);
case stateTypes.CLOSED:
return (
<ClosedDoor
open={open}
lock={lock}
/>);
case stateTypes.LOCKED:
return (
<LockedDoor
unlock={unlock}
/>);
default:
return null;
}
};
const mapStateToProps =
({ door }) => ({
_stateType: door._stateType,
});
const mapDispatchToProps =
dispatch =>
bindActionCreators(
{
open: openFunction,
close: closeFunction,
lock: lockFunction,
unlock: unlockFunction,
}, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Door);
Essentially, containers are rendered in exactly the same way as actions
are processed in our reducer
; a switch statement on the stateType
returns the correct child component for a given state.
And from here, we'll have individual stateless components for each of our "door" types (open/closed/locked), which will be rendered to the user depending on the state the door is in, and will only allow for actions that are available based on our original state transition diagram (go and double check; they should match up nicely).
It's worth noting that the fact that the actual rendering of components almost feels like an afterthought isn't a coincidence (so much so that I didn't even feel that showing the code for the components themselves would add any value to this post, but you can view them on Github if you feel otherwise). Thinking about state above all else lends itself to easy planning, to the point where actually putting it together is really simple. This methodology really is all about promoting more thought up-front; although the benefits are more obvious in a more complicated application than our door.
In the next part we'll look at how to expand this to be more usable in a real application, by introducing a methodology for dealing with parallel state machines.
Posted on February 4, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 4, 2018