Łukasz Makuch

Łukasz Makuch

React hooks... Oops! Part 4 - API responses get out of sync with the state

A man in front of a monitor

In the previous post from the React hooks... Oops! series we made it clear that placing values in the dependency list of useEffect doesn't turn them into observables.

Today we'll examine why the state may get out of sync.

React made working with the DOM API a breeze! Thanks to its simple, declarative nature, it is no longer a challenge to correctly synchronize the DOM with the state of the application. It simply works, and that's awesome! 🎉

But the DOM API is not the only API there is. In fact, many React apps end up fetching data from some other APIs, like a REST API. React itself doesn't provide us with any tools designed specifically for data fetching. But hey! It's nothing new! So it shouldn't be a problem, right? 🤔 Sadly, if we're not careful enough, API responses may get out of sync with the state of the app! 😨

Let's maybe have a look at a concrete example. This is the app we're building.

A simple react app fetching data from the backend

There are tabs representing different characters. Clicking a tab fetches the surname of the selected character from a backend API.

This is the code of the card component:

function CharacterCard({ characterId }) {
  const [character, setCharacter] = React.useState(null);
  React.useEffect(
    function () {
      fetchCharacter(characterId).then(setCharacter)
    },
    [characterId]
  );
  return character && (
    <article>
      <h1>
        It's {character.firstname} {character.lastname}
      </h1>
    </article>
  );
}

You might have already seen plenty of examples like that. useEffect is responsible for actually calling the fetchCharacter function and then useState holds the received data.

The good news is that it works most of the times. The bad news is that it works only most of the times.

This is what happens when the network connectivity is unstable and the user clicks quickly.

React state getting out of sync

The selected tab is the one for Murl, but the user sees Milan's data. It's buggy! 🐛 Why?

useEffect's dependency array makes it possible to synchronize triggering the effect with some state. In our example, every time the characterId changes (and the component re-renders), it calls fetchCharacter. What happens next happens outside of React. The results of async functions triggered inside useEffect are not synchronized with the effect's dependency array. For us it means that React will call fetchCharacter and then receive the data through setCharacter, but when and in what order it happens depends on the network.

The app is buggy because even though the user first clicked the tab for Milan and only then the one for Murl, the HTTP response for Milan came after the one for Murl. The effects are triggered in the right order, but the results come in an unpredictable order. And because we simply update the state of the card when we get a response, making the wrong assumption that it'll be in sync with the state of the tab, there's a bug in the app.

How to fix it?

One way would be to use the useRef hook and place the current characterId in that ref, so that in the .then callback of the Promise returned by the fetchCharacter function we can check if we're still interested in the response for that particular character. So many moving pieces! 🤯

Lucky us, there's a better way. It's true making API requests is nothing new and that it shouldn't be hard. It's just that the built-in hooks are low-level building blocks, designed to build on top of them, and not something optimized for specific use cases. That's why if we want to query the backend, we're better off using hooks designed specifically for that.

React query is a library that makes this and many other problems go away.

The same component refactored to use a high-level hook designed specifically for working with APIs looks like this:

function CharacterCard({ characterId }) {
  const character = useQuery(["character", characterId], fetchCharacter).data;
  return (
    <article>
      <h1>
        It's {character.firstname} {character.lastname}
      </h1>
    </article>
  );
}

One hook. One line. Zero bugs.

React query enables fetching data without bugs.gif

Judging from my professional experience, react query dramatically decreases the amount of code you have to write, makes apps less buggy and even faster!

A friend of mine once put it that way:

Where has this been my whole life?

I hope you'll give react query a go!

Stay tuned for the next post!