Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Created September 10, 2018 16:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OliverJAsh/5115e5eb6a82395d980f9f6e844dd923 to your computer and use it in GitHub Desktop.
Save OliverJAsh/5115e5eb6a82395d980f9f6e844dd923 to your computer and use it in GitHub Desktop.

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:

https://gist.github.com/2de84cbeb7bd25c2c725d2831ea7b7b3

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 SearchFailure and SearchSuccess are triggered the application should transition into the Failed or Success states, respectively. We can represent this as a statechart diagram:

(Generated via https://musing-rosalind-2ce8e7.netlify.com/?machine=%7B%22initial%22%3A%22Form%22%2C%22states%22%3A%7B%22Form%22%3A%7B%22on%22%3A%7B%22Search%22%3A%22Loading%22%7D%7D%2C%22Loading%22%3A%7B%22on%22%3A%7B%22SearchSuccess%22%3A%7B%22Gallery%22%3A%7B%7D%7D%2C%22SearchFailure%22%3A%22Failed%22%7D%7D%2C%22Failed%22%3A%7B%22on%22%3A%7B%22Search%22%3A%22Loading%22%7D%7D%2C%22Gallery%22%3A%7B%22on%22%3A%7B%22Search%22%3A%22Loading%22%7D%7D%7D%7D.)

Prerequisites

To begin we must define some basic types and helpers which we'll need to use later on:

https://gist.github.com/77aae60d652a13aa8c60b77ec9590efd

Defining states

Our 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.)

https://gist.github.com/86ed88d55204a87818241d52837986a5

Defining actions

Our Action type is a tagged union of all the possible action variants.

https://gist.github.com/a635f633178a5cde17200ab2f762416e

Defining transitions

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 Form state.

https://gist.github.com/21200a8880e6230dfbec03b62402b09b

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.

https://gist.github.com/bed05c84fdf1d057ab5fc29033e81d4f

https://gist.github.com/079c47d10e3c720b768865f89666ddb8

This produces the following output in the console:

https://gist.github.com/4307fad5eb6af0f6d277db600dd37ebe

Going further

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.

Our Action and 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:

https://gist.github.com/713183a57a6e70f06f41bfb0943d1615

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment