Your App As One Big State Machine
State machine all the things
January 20, 2018
Back in college, and in my early years of work, there was a meme about the approach computer engineering students took when solving problems. The meme states that whatever the problem, they would always resort to using state machines as their solution of choice. It's like the hammer-nail situation where a state machine approach is their hammer, and every problem basically becomes nail-like. But did you know Redux, one of the famous design patterns for the web, essentially turns your app into a big state machine?
It came as a surprise one day while looking for an easy way to explain Redux to a person who confuses it with an event system or regular MVC. Presenting a Redux app as a state machine diagram is the one thing I've never seen online blogs and tutorials do. When I tried to describe it that way, everything just made sense. With this approach, one can easily summarize the Redux architecture as: Nothing will happen until something will make it happen.
// I wish one day JS will have real enums
const LockStatus = {LOCKED: 0, UNLOCKED: 1}
const Action = {PUSH: 0, COIN: 1}
const reducer = (previousState, action) => {
switch (action.type) {
// A push will lock the system, regardless of what state you're in.
case Action.PUSH:
return {
...previousState,
lockStatus: LockStatus.LOCKED
}
// A push will unlock the system, regardless of what state you're in.
case Action.COIN:
return {
...previousState,
lockStatus: LockStatus.UNLOCKED
}
// Do nothing.
default:
return previousState
}
}
// Following from left to right in a clockwise manner starting from the entry point.
const state0 = {lockStatus: LockStatus.LOCKED} // locked
const state1 = reducer(state0, {type: Action.PUSH}) // locked
const state2 = reducer(state1, {type: Action.COIN}) // unlocked
const state3 = reducer(state2, {type: Action.COIN}) // unlocked
const state4 = reducer(state3, {type: Action.PUSH}) // locked
// Unit testing the reducer
test('push when locked', assert => {
const initial = {lockStatus: LockStatus.LOCKED}
const expected = {lockStatus: LockStatus.LOCKED}
assert.deepEqual(reducer(initial, {type: Action.PUSH}), expected)
})
test('push when unlocked', assert => {
const initial = {lockStatus: LockStatus.UNLOCKED}
const expected = {lockStatus: LockStatus.LOCKED}
assert.deepEqual(reducer(initial, {type: Action.PUSH}), expected)
})
test('coin when locked', assert => {
const initial = {lockStatus: LockStatus.LOCKED}
const expected = {lockStatus: LockStatus.UNLOCKED}
assert.deepEqual(reducer(initial, {type: Action.COIN}), expected)
})
test('coin when unlocked', assert => {
const initial = {lockStatus: LockStatus.UNLOCKED}
const expected = {lockStatus: LockStatus.UNLOCKED}
assert.deepEqual(reducer(initial, {type: Action.COIN}), expected)
})
Conclusion
And there you have it. A simple, barebones Redux-like flow based on a state machine diagram. If you can describe your app as a state machine, everything else will fall into place. Take the idea further, where states are "views" in the app and where actions and reducers are the "view transitions", then you've made yourself a simple storyboard. Add urls in the mix, where url changes fire actions and where actions can cause urls to change, you've got yourself a routing setup. The possibilities are endless with state machines.