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.
Top comments (6)
Can the "LOCKED" be considered a "sub" state of "CLOSED" ?
like
Good question! :-) It all comes down to how you decide to model your states really, but I imagine that you'd end up with the same thing. If "CLOSED" is a state and "UNLOCKED / LOCKED" are sub-states of "CLOSED", then essentially "CLOSED + LOCKED" and "CLOSED + UNLOCKED" both become composite states themselves, which kind of puts you back where we started with three states (since "CLOSED" must be either locked or unlocked).
Do you see what I mean? :-)
yes, thanks i guess that in this situation "LOCKED" is a shortcut for "CLOSED & LOCKED"
What i was thinking out loud with that question was about the intricacy of composite states as you call them and if any guidance is known to deal with them, theorytically and codewise :)
Not to push you to have an answer, just if there's some reflections on how to process : setting a hierarchy of states, linking them, etc
Some methods to approach it smartly like you did :)
i assumed that enters the realm of FSM but correct me if i'm wrong ;)
Now that you mention it, perhaps my example is actually just too simple to address your question properly - my apologies, let's have a look at this in a little more depth. :)
In the third part of this series (which I realise you've already read), I talk about parallel state machines. That hinted at hierarchy but didn't explicitly discuss it, perhaps I should extend that post to include a better example. But did you notice how in that post, we moved from the concept of a
door
as a container, to aroom
instead, which contained adoor
and analarm
? This is showing a hierarchy like the one you're asking about (although again in a more simple manner really). Extrapolate this to to be say, ahouse
that has multiplerooms
each with their owndoors
, and you have parallelism and hierarchy being dealt with at the same time, but where each room is independent of one another, whilst the whole system is still only able to be in one state (made up of the combination of the states of all therooms
) at any given point.In terms of implementing these in code, my take on this so far with React has been that each
container
is itself a representation of a finite state machine. The outermostApp
being the entire thing, and every container below that being an FSM that either has "sub" FSMs or parallel FSMs. If you match that to the diagram in part 3, which shows theroom
, then theroom
is a container in React that contains both thedoor
and thealarm
. Diagram and code for reference. :)The key thing to think about here is that even just by asking the question, you're thinking about planning the architecture of your application at the right time - i.e. before you've started writing it! :) Changing your mind on how a system is put together is a lot easier when it's still just a diagram isn't it. :D
I agree : thinking in terms of FSM it 's an approach that's not only easier but more related to reality of systems, if i could say so.
From your answer i understand that, in fine, a FSM can also be looked as a group of states which implies that the "root" state are defined by the states of its "children" without having to care -i assume- about all the possible states of the entire application.
thank you for these articles and answers :)
You're welcome, thank you for reading them! :-)
And yes, that's exactly it. Basically this whole thought process isn't really about building applications with FSMs, it's about recognising that your system is an FSM already (even before you've built it!) and using that realisation to allow you to better plan out your application from the start.