DEV Community

Mikey Stengel
Mikey Stengel

Posted on • Updated on

State machine advent: From implicit to explicit state machine (6/24)

In my research about state machines, I heard @davidkpiano talk about the fact that all of us are already using state machines. Most of them are just implicit. In today's post, I'll cover implicit state machines and showcase why you should reach for an explicit state machine instead.

Implicit state machines do not enforce the state + event => newState formula that should drive our UI. They oftentimes change state within event handlers and are usually plagued by a lot of conditional logic. The simplest version of such an implicit state machine written purely in React can be seen below or in this codesandbox.

import React, { useState } from "react";
import Switch from "react-switch";

const LightSwitch = () => {
  const [active, setActive] = useState(false);

  return (
    <Switch
      onChange={() => setActive(!active)}
      checked={active}
      aria-label='Toggle me'
    />
  );
}

This is one of the simplest stateful React components and it's working great. What could possibly be wrong with the implementation above?

It starts with useState. We pass an initial state and then perform a state update using setState in event handlers. Whenever this occurs, we have created a component whose behavior can't easily be reused. The initial state of our component is tightly coupled and encapsulated to our React component. If we want to reuse our logic (including the initial state), we might want to reach for a custom hook instead.

const useSwitch = () => {
  const [active, setActive] = useState(false);

  return [active, setActive]; 
}

This custom hook allows us to share the initial state of false for any component that'd like to implement a stateful switch component. However, each component implementing this hook will have to directly call setActive(!active) as soon as someone clicks on the switch. We can fix this by making a minor change to our custom hook.

const useSwitch = () => {
  const [active, setActive] = useState(false);

  const toggle = () => void setActive(!active); 
  return [active, toggle]; 
}

const LightSwitch = () => {
  const [active, toggle] = useSwitch();

  return (
    <Switch
      onChange={toggle}
      checked={active}
      aria-label='Toggle me'
    />
  )
}

Instead of exposing the setActive hook directly, we expose a function that acts as an event that ultimately drives our state changes.

Sweet. We've abstracted our implicit state machine into a custom hook that encapsulates the exact behavior as our explicitly defined state machine from yesterday.
This worked pretty well but it's worrisome just how easy it is to forget that events should drive state changes. Needless to say, as your application logic and state architecture grows beyond two possible boolean values and one event, you'll undoubtedly introduce a lot of bugs by using implicit machines and miss out on all the benefits of explicit state machines such as visualization.

In summary:

  • It's very easy to forget state + event => newState because React encourages to perform state updates in event handlers. Send events in event handlers and your code will improve. The only way to enforce this is by strictly modeling your application with state machines.
  • If you want to fully separate behavior from your component, reach for explicit state machines immediately.
  • useState(boolean) especially if you have two or more interdependent local states is a huge red flag and indicates that you should probably go with an explicit defined state machine.

Codesandbox explicit state machine in xstate

Codesandbox implicit state machine

Codesandbox a better event-driven implicit state machine using custom hooks

About this series

Throughout the first 24 days of December, I'll publish a small blog post each day teaching you about the ins and outs of state machines and statecharts.

The first couple of days will be spent on the fundamentals before we'll progress to more advanced concepts.

Discussion (0)