Tech Blog

Facebook Icon Twitter Icon Linkedin Icon

AnyMind Group

Facebook Icon Twitter Icon Linkedin Icon

Robust components with state machines

Hello, my name is Pablo and I work as a backend developer at AnyFactory. For this article though I’ll wear my frontend development hat to present a simple way of dealing with moderately complex client/server interactions on web applications using React and a robust architecture involving state machine modeling via reducers. Hope you enjoy it!

■ What are state machines?

State machines (known as well as finite-state machines or FSM) are computational constructs that can be used to model linear processes represented by a set of states.

One of the most known usages of state machines are public transportation turnstiles. They have a finite number of states: locked and unlocked. Here is a simple graphic that shows us these states, with their possible inputs and transitions:

The initial state of the turnstile is locked. No matter how many times we may push it, it stays in that locked state. However, if we pass a coin to it, then it transitions to the unlocked state. Another coin at this point would do nothing; it would still be in the unlocked state. A push from the other side would work, and we’d be able to pass. This action also transitions the machine to the initial locked state.

■ State machines implemented with reducers

We can replicate state machines using React and reducers, which were once part of the Redux library but are now available as React hooks. Implementing then state machines with reducers is simple, we can do that following this pattern (in pseudocode):

initial_state = <state>
reducer <- state, action
  select state
    <state>: select action
	  <action>: -> next_state
  -> state

When the reducer is called with the current state and a given action, it selects a block of code defined for the current state, and then does the same for a block of code defined for the action in the context of the selected state. If the action exists it returns a new state according to what was defined (a valid state transition) and if the action does not exist for the selected state it returns the current state again. For our turnstile example the reducer will be defined this way (again in pseudocode):

initial_state = locked
reducer <- state, action
  select state
    locked: select action
      coin: -> unlocked
	unlocked: select action
	  push: -> locked
  -> state

■ Practical example

Now we’re clear about the structure of the reducer, we’re going to design a simple client/server state machine based on the following state diagram:

Let’s start importing React and some hooks we’re gonna need later:

import React, { useReducer, useCallback } from "react";

Now we’re going to implement our state and actions:

const State = {
  INITIAL: "INITIAL",
  LOADING: "LOADING",
  FAILURE: "FAILURE",
  SUCCESS: "SUCCESS"
};

const Action = {
  REQUEST: "REQUEST",
  SUCCEED: "SUCCEED",
  FAIL: "FAIL"
};

That’s four states counting the initial state, and three actions to transition from one state to another, the reducer then is implemented like this:

function reducer(state, action) {
  switch (state) {
    case State.INITIAL:
    case State.SUCCESS:
    case State.FAILURE:
      switch (action.type) {
        case Action.REQUEST:
          return State.LOADING;
        default:
          return state;
      }
    case State.LOADING:
      switch (action.type) {
        case Action.SUCCEED:
          return State.SUCCESS;
        case Action.FAIL:
          return State.FAILURE;
        default:
          return state;
      }
    default:
      throw new Error("Invalid state");
  }
}

Then we’re gonna add some view components, first we write the view for the INITIAL state:

function InitialView({ onClick }) {
  return (
    <React.Fragment>
      <p>Welcome!</p>
      <button onClick={onClick}>Send first request!</button>
    </React.Fragment>
  );
}

Nothing special, just a welcome message and a button to send the first request. After sending a request we’re gonna need a view for the LOADING state, which is just a message informing the user that the request is in flight:

function LoadingView() {
  return <p>Sending request...</p>;
}

Then we’re gonna need two final views, one for SUCCESS and another for FAILURE states, both allowing us to send another request:

function SuccessView({ onClick }) {
  return (
    <React.Fragment>
      <p>The request succeeded :)</p>
      <button onClick={onClick}>Send another!</button>
    </React.Fragment>
  );
}

function FailureView({ onClick }) {
  return (
    <React.Fragment>
      <p>The request failed :(</p>
      <button onClick={onClick}>Try again!</button>
    </React.Fragment>
  );
}

We’re gonna implement now a controller component that will load the reducer, dispatch our actions and decide which view is rendered depending on the current state. We have to dispatch the REQUEST action immediately when the views call the event handler defined as onClick, and to simulate a delayed server response we will be dispatching SUCCEED or FAIL actions, by randomly picking one after a timeout of one second:

function RequestController() {
  const [state, dispatch] = useReducer(reducer, State.INITIAL);

  const dispatchRequest = useCallback(() => {
    dispatch({ type: Action.REQUEST });

    // Simulates a delayed server response
    setTimeout(() => {
      dispatch({ type: Math.random() < 0.5 ? Action.SUCCEED : Action.FAIL });
    }, 1000);
  }, [dispatch]);

  switch (state) {
    case State.INITIAL:
      return <InitialView onClick={dispatchRequest} />;
    case State.LOADING:
      return <LoadingView />;
    case State.FAILURE:
      return <FailureView onClick={dispatchRequest} />;
    case State.SUCCESS:
      return <SuccessView onClick={dispatchRequest} />;
    default:
      throw new Error("Invalid view");
  }
}

Then with a markup like this:

<div id="app"></div>

We can finish our App component that will render our RequestController and anything else we want to display:

function App() {
  return (
    <div>
      <RequestController />
    </div>
  );
}

You can find the complete code running on this code sandbox:

The example application is now finished and we can be reasonably sure that it will behave exactly in the way that we designed it, each view will be presented when is required and the component will never be in an undefined or mixed state.

■ Conclusion

State machines are a simple way of defining valid state transitions that can make your application very robust. Nevertheless, we could go further and add static type checking via Typescript to minimize as well errors that can arise from the implementation of the state machine itself. I’ll leave that as an exercise for the reader and will talk more about static type checking on the next article.

See you then!

Latest News