There are plenty of ways to build reactive interfaces, but my favorite is to use finite state machines. I’ve written before on how JavaScript Promises are finite state machines, but I only briefly touched on how finite state machines communicate between each other. With communication comes the potential for race conditions and race conditions bring sadness.
There are different channels for this communication. In the browser we have: event listeners, the DOM, message channels, callbacks (including promises), methods, shared memory, etc.
What’s the difference between a rocket and a washing machine? For our purposes, the difference is how long they last. A rocket is created, performs it’s task and (hopefully) burns up on reentry. A washing machine is created, and lasts for a long time, performing its task many times over and over again.
To illustrate a race condition we’ll implement a search box using various rocket and washing machine styles. Each style is available from this search box where you can switch between them and inspect how they behave. The full code is on GitHub.
A Broken Search Box
Let’s look at some faulty code to implement a search box.
import { elements, clear_and_spin, hide_spinner } from './base.mjs';
const {
form, results
} = elements();form.addEventListener('submit', async (e) => {
e.preventDefault(); // Clear previous search results and show the spinner
clear_and_spin(); // Fetch search results
const args = new URLSearchParams(new FormData(e.target));
const response = await fetch('https://www.googleapis.com/customsearch/v1?' + args.toString());
const json = await response.json();
const { items } = json; // Display results and hide spinner
for (const { htmlTitle, link, snippet } of items ?? []) {
results.insertAdjacentHTML('beforeend', `<li>
<a href="${link}">${htmlTitle}</a>
<p>${snippet}</p>
</li>`);
}
hide_spinner();
});
An immediate red flag is the async event listener. While it doesn’t mean for certain that a race condition exists, it does indicate that you should slow down to verify that the code works. In this case it…