Strongly-typed finite-state machines with Redux and TypeScript
Finite-state machines have been all the rage recently. There are many libraries that allow you to work with finite-state machines. However, I wondered: how far can we get with our existing tools — Redux and the reducer pattern?
Note that this article is not concerned with why finite-state machines are useful. If you’re interested in learning about why they are useful, I recommend David Khourshid’s talk “Simplifying Complex UIs with Finite Automata & Statecharts”.
Reducer is a state machine
At the core of state machines is the following function:
If you’re familiar with Redux, that might look familiar to you. A Redux reducer function is a state machine! A reducer function describes how the machine should transition, given the previous state and an action (aka an event in statechart terminology), to the next state.
This article is interested in how we can utilise Redux to write a strongly-typed finite-state machine, in the interests of code correctness and readability. By finite, we mean that the machine may only be in one of a finite number of states at any given time. By strongly-typed, we mean that the states and actions should carry the types of their parameters, and these parameters should only be accessible when we’ve narrowed the union of states or actions to a single variant.
The examples in this article are available on GitHub.
Setting the scene
Throughout this article we will use the example of a simple photo gallery search. The application starts in a
Form state, where the user may enter a query. Upon form submission the
Search event is triggered, and the application should transition into a
Loading state. When request either fails or succeeds, corresponding events
SearchSuccess are triggered the application should transition into the
Success states, respectively. We can represent this as a statechart diagram:
To begin we must define some basic types and helpers which we’ll need to use later on:
State type is a tagged union of all the possible state variants. By expressing our state as a union, we're defining which states are valid. Later we'll use these states to define which transitions are valid.
Note how the
query parameter is only available in the
Loading state, and the
items parameter is only available in the
Gallery state. By restricting these parameters so that they only exist in their corresponding states, we are making impossible states impossible: the type system only allows us to access the parameters when we are in a state where they can exist.
(Note there are cleaner ways to define tagged unions in TypeScript, but I refrained from using them in this example for simplicity. See the note at the end.)
Action type is a tagged union of all the possible action variants.
Earlier we saw how Redux’s reducer functions allow us to describe our state transitions. As well how the transition should be performed, we can define which transitions are valid for given states. For example, a valid transition would be from a
Loading state, given a
SearchFailure event, to the
Failed state—but the
SearchFailure event should not cause a transition when we're in the
Tying it all together
To demonstrate our state machine, we can simulate actions. In a real world application, these would be driven by outside events such as user interactions or HTTP responses.
This produces the following output in the console:
There is much more to state machines than this example demonstrates. However, the simple primitive of a reducer allows for all types of state machines. For example, “nested state machines” can simply be modelled as nested reducers. Likewise, “parallel state machines” are just combined reducers.
State types are known as tagged union types. There are much cleaner ways of defining and using tagged union types in TypeScript, but I refrained from using them in this example for simplicity. At Unsplash we tend to use Unionize. For example, here is how we might define the
Action tagged union type if we were to use Unionize:
A full example using Unionize can be seen at https://github.com/unsplash/ts-redux-finite-state-machine-example/tree/unionize.
If you like how we do things at Unsplash, consider joining us!