A Custom useStateMachine React Hook
In his post Stop using isLoading booleans, Kent C. Dodds tells us to… well, to stop using booleans to keep track of binary states 😅. The problem with using a boolean flag to model our component’s state is that, usually, the flag is just part of a bigger, more complex state.
When you encode your component’s state in a bunch of binary flags, you end up rendering with a lot of conditional checking. That leads to code that is really difficult to maintain and extend.
Instead of using boolean values, Kent recommends using a state machine. In his post he uses xstate, a fully-fledged state machine library.
In this story I will offer a simpler alternative implemented as a Custom Hook using useReducer
.
Of course, because it is simpler, this alternative is also less powerful, so if you are building a complex component, give xstate a try!
Defining useStateMachine
Quoting the Wikipedia, a state machine (or finite state machine, FSM) is
[…] an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some inputs; the change from one state to another is called a transition. An FSM is defined by a list of its states, its initial state, and the inputs that trigger each transition
So, in order to build our own state machine we need:
- A list of states.
- A list of events (inputs).
- A list of transitions (current state + event => next state. 🤔 this one looks familiar…).
We are going to start designing the API for our useStateMachine
custom Hook. What we want is something like this:
const [currentState, sendEvent] = useStateMachine(machineSpec);
The Hook accepts a machine specification object defining the states, inputs and transitions, and returns the current state and a method to send an event to the machine, to make it change (transition) to its new state.
Say we want to use a state machine to handle the connection to some remote service. Our user starts disconnected, and we want to let her connect to the service and disconnect from it. We can represent that machine like this:
The spec encodes all the information needed to run the machine:
- The list of states, corresponding to the keys of the
states
property. - The list of transitions, defined for each state using
EVENT_NAME: “nextState”
pairs. - The initial state, so we know what to do when the first event is received.
We can use that information to define our custom Hook:
As you can see, our custom Hook is just a special case of useReducer
: given the spec, we use the state transitions in the states
property to build a reducer function, and the initialState
as the reducer’s initial state. The return value of useReducer
meets our API requirements: we get the current state and a function to send (dispatch) events that will, in turn, update the machine’s state 😀.
Using useStateMachine
Now we can use our custom Hook to “drive” our application rendering and interactions:
For each possible state, a different component is rendered. Each component receives a set of callbacks to send events to the state machine and uses those callbacks to handle the user interaction.
Testing
useStateMachine
is just a glorified useReducer
, so it’s pretty easy to test. You can extract your state machine to your own custom Hook and test it using testing tools like react-hooks-testing-library. Or you can test the reducer function itself:
Maintenance
The great thing about using a state machine is that the spec encodes every possible state of your component, and also every valid transition from state to state. This makes adding new states and transitions really simple.
Let’s modify our example to take into account possible connection errors. Now our state machine looks like this:
Now, while we are in the connecting
state, we can receive a CONNECTION_ERROR
event and transition to the error
state. When in the error
state, we can try to CONNECT
again.
We update our components to handle the new state:
State machines are a very powerful tool for designing UI components. As we’ve seen in this story, even a naive implementation can be really helpful. You can start with something like this, and then, if you need to, go the full-blown-state-machine way and add transition guards, effects, nested machines, and all the other goodies that libraries like xstate offer.