If you work in software long enough, you'll have to deal with legacy codebases. But you'll also have had the chance to start projects from scratch. And eventually you'll see your project become a legacy codebase itself.
A big challenge in software engineering is: "how do we keep applications maintainable?" Because if software isn't maintainable, it can be hard to change and get other people involved. This is not great for many reasons, but most importantly: it makes working with the code less fun!
So we try and keep code maintainable. Like with gardening, this is an active effort. Because if we don't pay attention, weeds will sprout. So we document things. Plan things. Refactor things. All so we can find the right abstractions to keep our code maintainable.
Today we'll be talking about an abstraction that works particularly well for User Interfaces: State Machines.
You can think of a state machine as a data structure that's used to express states and the relationships between them. A state machine can only be in a single state at any given time, and from that state it can only progress to particular states.
Let's make this a little more practical. Take for example a (Dutch) traffic light. In the Netherlands, traffic lights start out as "green". From "green" they can transition to "orange". And from "orange", go to "red". And from "red", back to "green". We could write this down as:
green => { orange }
orange => { red }
red => { green }
From the chart above, we can see that "green" can go to "orange". But green can never directly transition to "red". So we've now successfully expressed relations between code. Neat!
Let's take this a step further though. What if we wanted to have different transitions available? We'd need to start by giving our transitions names. Let's name the existing transition "timer", because transitions of the current state are all based on a timer.
green => { timer: orange }
orange => { timer: red }
red => { timer: green }
And that's the basics. From here we could go on to add more states and transitions, expanding the graph.
Let's implement the traffic light example in JavaScript. In order for this to work, we'll need to implement:
Let's dig in!
The first step is to write down our states and transitions:
var transitions = {
green: { timer: 'orange' },
orange: { timer: 'red' },
red: { timer: 'green' }
}
There's only one piece of information missing now: our initial state. Let's define it.
var initialState = 'green'
Each state links to several other states, which are referenced by transition names. The base algorithm for state machines is:
function stateMachine (transitions, currentState, transitionName) {
var newState = transitions[currentState][transitionName]
return newState
}
Or in a more compact notation:
var stateMachine = (t, c, n) => t[c][n]
While the state machine algorithm is quite simple, it requires us to keep track of what the current state is. This means that state machines themselves are stateful. Luckily we can create a simple interface for this using classes.
Let's create a class that takes an initial state + state map as the initial
arguments, and implements one method called .transition(transitionName)
.
note: the class
notation here is for brevity. It's the idea that matters more
than the implementation. So feel free to write this down however you prefer!
class StateMachine {
constructor (initialState, transitions) {
this.state = initialState
this.transitions = transitions
}
transition (transitionName) {
var nextState = this.transitions[this.state][transitionName]
if (!nextState) throw new Error(`invalid: ${this.state} -> ${transitionName}`)
this.state = nextState
}
}
The value of .state
is the current state we're in. If an invalid transition
occurs, the state machine throws an error explaining which transition was
invalid.
Now that we have all of our individual bits, let's combine it all together:
var machine = new StateMachine('green', {
green: { timer: 'orange' },
orange: { timer: 'red' },
red: { timer: 'green' }
})
machine.transition('timer')
console.log(machine.state) // => 'orange'
machine.transition('timer')
console.log(machine.state) // => 'red'
machine.transition('timer')
console.log(machine.state) // => 'green'
And that's all it takes to implement a fully functional state machine. As you can see there's not much code to it.
From here there are a few more features that could be added, such as:
We'll leave this as an exercise up to the reader.
If you're looking for a solid state machine implementation, check out choojs/nanostate. It's similar to the state machine we implemented in the section above, but adds event hooks, clean error messages, and more.
Websites generally consist of 3 main elements: paragraph text, lists and forms. While paragraph text is generally straightforward to place on a page, lists and forms require some more work. This section explains everything you need to know to work with forms in Choo.
Connecting to the network is essential for applications. This section is all about the browser's network APIs, and how to use them in Choo.
Choo is built up out of two parts: stores and views. In order to render a view,
it must be added to the application through app.route()
. This is the router.
Server rendering is an excellent way to speed up the load time of your pages. This section shows how to effectively render Choo apps in Node.
State machines are a great way to manage different states in your application. In this section we'll learn how to use and implement state machines.
Stores are Choo's data abstraction. They're meant to both hold application data, and listen for events to change it. In traditional systems this is sometimes also known as "models".
Views are Choo's rendering abstraction. It's the part that takes the internal state and renders elements to the DOM.