Think of React Components as State Machines, Not Functions
By: Michael Landis
Tags: React Hook, React, Programming, JavaScript

When React Components moved from classes to functions, all the buzz was about transitioning from Object-Oriented Programming to Functional Programming. Functional programming has two goals in mind:

You get the same results when you provide the same inputs to a function. The function’s only goal is to create an output. It doesn’t try to do things on the side. (Functions that impact something other than their own output are described as functions that create side effects.)

If a function can fulfill these two criteria, it is called a pure function. You can run the same inputs through it all day, and always get the same output, without having to track anything else.

When Functional Components were first introduced to React, they were pure functions. They could only produce outputs based on their inputs.

Then came Hooks.

With Hooks, functional components create side effects right and left. You can save and change state. You can cache data. You can create side effects using a hook literally called useEffect . All of these things impact the output beyond what the inputs tell us. Functional components are now about as far from pure functions as you can get.

So, if functional components no longer follow an object-oriented or functional programming paradigm, what’s left?

Allow me to introduce to you the idea of Finite State Machines.

Finite State Machines: Old State + Input = New State

The concept of Finite State Machines was introduced to the world back in 1943 in a neuroscience paper by Warren McCulloch and Walter Pitts. They were trying to model behavior based on the idea that, if you had a specific set of inputs, you can define a specific set of possible states, based upon the current state of the system. When the state changes due to a change in the inputs, the state machine undergoes what is known as a transition. When a state machine receives a transition message, it will go to one of a limited number of other states, depending on what that message was.

This is kind of a lot to wrap one’s head around in one paragraph, so let’s look at a classic real-world example: a traffic light.

A traffic light has three specific states: Green (go), Yellow (caution), and Red (stop). When a light turns Green, a timer counts down. When the timer runs out, it tells the light to go to the next state: Yellow. It resets, counts down, then tells the light to go to the next state: Red. At that point, it also tells the light on the other road to go to its next state, from Red to Green, and it starts the same cycle for the other street’s traffic lights.

What we can see is that, when a traffic light is told to change state, it does so based upon specific rules. You can create an event table for a stop light, to show what’s going on.

Event table for a traffic light Finite State Machine

At no point does a traffic light, say, go from Green to Red, or Yellow to Green. The states available to a traffic light at any given point in time are finite. The only time a state can change is when it is told to transition, either by a timer or by the other street’s lights.

Hooks as State Machines

So, how does knowing about finite state machines help us better understand Hooks?

If we look at Hooks, we see that they fall into three categories:

Hooks that trigger a rerender when updated ( useState , useContext )

, ) Hooks that update cache when values change ( useMemo , useCallback )

, ) Hooks that trigger effects when values change ( useEffect , useLayoutEffect )

(Yes, useRef doesn’t fall into these categories, but it’s an odd bird.)

All of these take inputs that indicate that they need to perform a transition. You could say that React Hooks are state machines, responding to transition events. They’re not strictly finite state machines, because they can act on specific values to produce different things. But we can make them finite, by defining specific inputs and producing narrowly-defined outputs.

A real world example: Confirmation modals

Consider a component that lets the user update an order, with the option of either appending to the existing order or replacing it entirely with the new information. If they choose to replace it, we want to show a confirmation modal before continuing.

If we look at our own component, we can look at it in terms of the different pieces of state we need to hold:

Did the user want to replace the entire order?

Did the user submit the order change request?

Is the confirmation modal visible?

Did they confirm the replacement if they needed to?

Is the update completed?

By looking at these different pieces of state, we can see which pieces should cause which changes, and define the different states our component can be in:

Update order component states

By looking at how our variables define a state machine, we get three advantages:

We simplify our 32 different possible variable combinations (true or false, 5 different ways) into 7 distinct component states. Rather than thinking in terms of “what’s true” and “what’s false” we can think in terms of “what is the component set up to do right now?” From these 7 states, we can clearly define how one state turns into another. From “Won’t Replace” we can only go to “Will Replace” or “Updating.” From “Will Replace” we can only go to “Won’t Replace” or “Change Requested.” If somehow we end up with some odd combination of variables, we know something went wrong, and debug from there. Perhaps most importantly, state machines let us manage the user flow by inspecting the component’s data, not by tracking function flow. Handlers are focused on changing data, rather than managing flow. This lets us keep our minds in the React, data-centric, model, rather than trying to revert to an imperative, flow-based model. (Notice how we use showConfirmation as a naked value to show our modal, rather than wrapping its setter in showModal() / hideModal() functions, which could be a tempting way to keep in an imperative frame of mind.)

Admittedly, it may not be obvious at first that we need these five boolean variables to drive seven states. But if we look at what we need to account for, what steps need to happen, we can work out the states we need, the variables required to track the state changes, and the triggers that move us from state to state.

So, we’ve got our states, and the variables that drive them. Now what?

Enter useEffect .

useEffect as a Finite State Machine Transition Manager

useEffect can easily be used to drive the transitions in a finite state machine, thanks to the dependency array. Whenever a piece of state in that array changes, the effect is fired, letting us update the variables to define a new state.

If we convert the event table above into event handlers and useEffect Hooks, we get something that looks like this:

It’s a little verbose, and I don’t show the dependencies related to the actual data, or creating callbacks for the functions, but you can see how each handler or Hook is devoted to managing a transition between states. A transition occurs when the user clicks a checkbox or button, when an effect’s dependencies change, or when the server call completes.

There are no attempts to directly tie one handler to another, one state to another, one transition to another. Each code block is devoted to changing state, but it doesn’t care when the state transition occurs.

By tying the changes to state, it becomes clearer how the current data impacts the behavior for the user, rather than trying to define behavior by imitating an imperative programming flow. This helps us stay in the “follow the data” frame of mind that React encourages.

Once we see how the individual states transition, we can combine some of the code to make it more compact:

I won’t argue that this is simple, especially when coming from an imperative programming background. But by thinking in terms of application state instead of points in time, you can create a variety of flows that could otherwise become even more spaghetti when thinking purely in terms of functions.

The complete, fully functional example can be found at https://r4dll.csb.app/.