May 04, 2020

How to Use `useReducer` as a Finite State Machine

edit

I recently attempted to get XState into the Webflow codebase to manage some challenging and complex UIs but was met with resistance. No big deal, this is a part of work, consensus driving, disagreeing and committing, etc. If it hasn’t happened to you yet, it will.

One of the things you need when that happens is a good imagination. How do we make the tools we already have do the things we want in a reasonable way? I’m going to help you with that today by showing you a way to make useReducer behave like a simple finite state machine in React.

I’ve started using the following pattern in some of my work and I think it will help you. If we look at the reducer of useReducer and a state machine, we can see that they are quite similar. They both take a current state and an event (Redux calls this an action, but I will use event throughout this post), and return the next state. The differences are:

  • A reducer does not restrict itself to a set of finite states
  • A reducer has no mechanism for calling side effects from state transitions

We can’t do anything about the second point, and I will cover that in a future post on useEffectReducer, but we can do something about the first. We can create a graph of states that our reducer will use to transition to the next state. I’ll demonstrate how I approach this.

I’m going to use the example of a light bulb like I do in my XState course. A bulb has three states: lit, unlit, and broken. Let’s make a NEXT_STATE_GRAPH object starting with these states.

const NEXT_STATE_GRAPH = {
  lit: {},
  unlit: {},
  broken: {},
}

Next, let’s define the events that will transition each state to their next state.

const NEXT_STATE_GRAPH = {
  lit: {
    TOGGLE: 'unlit',
    BREAK: 'broken',
  },
  unlit: {
    TOGGLE: 'lit',
    BREAK: 'broken',
  },
  broken: {},
}

Now, we’re going to use this in the reducer we will pass to useReducer to get our next state.

const reducer = (state, event) => {
  const nextState = NEXT_STATE_GRAPH[state][event]
  return nextState !== undefined ? nextState : state
}

That’s it! A reducer doesn’t need to be complicated or use a switch statement. It just has to deterministically give you the next state. Let’s apply this to a simple component.

function LightBulb() {
  // we can change `dispatch` to `send` to look more like using XState
  const [state, send] = useReducer(reducer, 'unlit')

  return (
    <div>
      State: {state}
      <button type="button" onClick={() => send('TOGGLE')}>
        Toggle
      </button>
      <button type="button" onClick={() => send('BREAK')}>
        Break
      </button>
    </div>
  )
}

You can see it in action right here. Be sure to click buttons a state shouldn’t respond to as well!

State: unlit

Types

One small issue I ran into while implementing this was that our type system, Flow, was not happy that I was using the failure of finding a key on an object to return undefined on purpose. Type systems hate when you use a language feature like that. No problem, I made a simple fix by adjusting our NEXT_STATE_GRAPH slightly.

// Let's make an object of all our events with an undefined next state
const NON_RESPONSIVE_EVENTS = {
  BREAK: undefined,
  TOGGLE: undefined,
}

// Now let's spread that object into all our states
const NEXT_STATE_GRAPH = {
  lit: {
    ...NON_RESPONSIVE_EVENTS,
    BREAK: 'broken',
    TOGGLE: 'unlit',
  },
  unlit: {
    ...NON_RESPONSIVE_EVENTS,
    BREAK: 'broken',
    TOGGLE: 'lit',
  },
  broken: {
    ...NON_RESPONSIVE_EVENTS,
  },
}

Now each state node will have a key for any event and explicitly returns undefined in those situations where we do not want to make a state transition. We simply override the non-responsive events with responsive ones below, taking advantage of how object spreading works. This will appease any exhaustive check a type system demands.

Conclusion

There you have it, a simple finite state machine using useReducer. I’ll make another post soon on how to handle infinite state data alongside finite states with useReducer, but this should be enough to get your brain juices flowing and experimenting with this approach.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Just Enough Functional Programming
Just Enough Functional Programming
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.