Async Superpowers, Part 3

Floyd May
4 min readDec 29, 2021

--

This is the third in an four-part series that will help you easily write and test UIs that do asynchronous stuff.

Recap

In the previous two articles, we learned about how to model real-world computing problems with a state machine. We played with parsing strings, controlling a rollercoaster, and modeling a search UI. We also showed how we use a state and its associated data to prove (via automated tests) that the UI renders properly when it’s in a given state. We also proved that the UI dispatches actions in response to user input.

The state machine that drives our search UI.

Connecting It All Together

We have two remaining problems to solve. One, we need to “drive” our state machine so that actions cause the UI’s state to change; and two, we need to initiate those asynchronous communications at the right times and dispatch actions when they complete.

(Quick note: I’m using React for my code samples here, but don’t let that deter you if you’re using some other tech! This technique is applicable in any programming language and/or UI framework.)

For the first problem, we’ll use React’s useReducer hook. This is, unsurprisingly, built exactly for usages like ours:

const ThingSearchUI = () => {
const [state, dispatch] = useReducer(reducer, { tag: "Loading" });
return <ThingSearch state={state} dispatch={dispatch} />;
};

Let’s take a closer look. The useReducer function accepts a reducer function (the reducer we built in the previous article), and an initial state. In our case, the initial state for our state machine is Loading. The return values are the current state and a dispatch function. Notice how we’re passing this state and dispatch directly to ThingSearch, which we also learned how to test in the previous article. Now, our UI renders itself properly in each state and actions generated from user input are now “connected” to our reducer function, causing the state machine to transition.

If you’re using some other framework, you could replicate useReducer using something like this:

// assuming `this.state` and `this.dispatch` are available
// for use in your child components (or similar)
class ThingSearch extends Component {
onInit() {
this.state = { tag: "Loading" };
}
dispatch(action: Action) {
this.state = reducer(this.state, action);
}
}

Next, we need to initiate those asynchronous communications at the right times. For the three “asynchronous result” transitions in our state machine, they begin at the Loading, Reloading, and Loading More states. So, when our machine arrives at each state, we should initiate those asynchronous calls (like, say, using the fetch API to get some data over HTTP). So, we could write a function that, given a state and a dispatch function, makes the correct fetch call and dispatches the correct action:

async function reactToState(state, dispatch) {
if(state.tag === "Loading" || state.tag === "Reloading") {
const response = fetch({
method: "POST",
path: "/api/thingsearch",
content: { state.searchText, /* ... */ }
});
const data = await response.json();
dispatch({
tag: "Data Loaded",
things: data.things,
/* ... */
});
}
/* and so on */
}

Then, we can fashion a React component that uses this function when the state changes:

const ThingSearchReactor = ({ state, dispatch }) => {
useEffect(() => {
reactToState(state, dispatch);
}, [state]);
return <React.Fragment />
}

I call this kind of component that only listens for state changes and initiates asynchronous operations an “async reactor”. Tests for this component would likely check for initiating the right HTTP communications given some state, and dispatching the correct action when the HTTP call completes. Then, finally, we can compose the entire thing together like this:

const ThingSearchUI = () => {
const [state, dispatch] = useReducer(reducer, { tag: "Loading" });
return (<>
<ThingSearch state={state} dispatch={dispatch} />
<ThingSearchReactor state={state} dispatch={dispatch} />
</>);
};

Zooming Out

At this point, let’s take a step back and revisit what problem we were solving and see how all of these disjointed code snippets fit together. We have a “Thing Search” UI that has a search box, some dropdowns, and a list of “things”:

This UI has a number of states where we’re waiting for asynchronous operations to complete. Our solution for this has three major components:

  • The state machine
    This is implemented as a reducer function that accepts a current State and an Action and returns a new State. We modeled every behavior of our UI, including user interactions and asynchronous results, into this state machine.
  • The display component
    This is the portion that displays UI based on a given State and dispatches Actions when the user interacts with the UI.
  • The async reactor
    This component listens for state changes and initiates the asynchronous communications for our various “loading” states.

Each of these components can be thoroughly and easily unit tested in isolation, and composing them together is quite simple. A larger-scale “integration” test or two¹ could ensure that we’ve composed these pieces correctly.

A working example app based on create-react-app is posted here (thanks GitHub!). In the final article in this series, we’ll discuss some design guidance for modeling your own UIs using state machines and explore some common pitfalls. Stick around!

Footnotes

  1. No, we’re not going to have an argument about what is and isn’t an integration test

--

--