All Articles

How to easily ignore useEffect HTTP calls with RxJS

Now that React Hooks have been officially released, even more patterns are emerging across the Internet.

useEffect

The useEffect hook’s among the most popular, as it can replace componentDidMount, componentDidUpdate, and componentWillUnmount.

Most of the initialization, updates, and cleanup logic a component may need can be put inside of useEffect.

An Ugly User Experience

On a recent project, I encountered a scenario where useEffect acted on HTTP requests I was no longer interested in.

Conceptually, the UI was like this:

  • On first load, fetch the list of fruits and render a <button> for each one.
  • Click a <button> to fetch that fruit’s details.

But watch what happens when I click multiple fruits in a row

Way after I stopped clicking, the fruit detail section kept changing!

The Code

Let’s see my custom hook that leverages useEffect.

Here’s the Codesandbox and GitHub links if you wish to follow along. The file is useFruitDetail.js.

import { useEffect, useState } from 'react';
import { getFruit } from './api';

export const useFruitDetail = (fruitName) => {
  const [fruitDetail, setFruitDetail] = useState(null);

  useEffect(() => {
    if (!fruitName) {
      return;
    }

    getFruit(fruitName).then(setFruitDetail);
  }, [fruitName]);

  return fruitDetail;
};

Whenever fruitName changes, we’ll request its details. And we have no way of cancelling a request! So quickly re-running this results in many state changes that we’re no longer interested in.

If you render this to the UI, you get a messy user experience where the detail section keeps flickering until the final request is resolved.

Enter RxJS

Ignoring old requests is trivial with RxJS.

It can do so much more than what I’ll demo here, so I highly recommend you dive into it!

This portion of our code, the effect code, needs to change.

() => {
  if (!fruitName) {
    return;
  }

  getFruit(fruitName).then(setFruitDetail);
};

Instead of a Promise, let’s convert getFruit into an Observable using the RxJS defer function. And instead of .then, we’ll call .subscribe.

import { defer } from 'rxjs';

// ...

() => {
  if (!fruitName) {
    return;
  }

  defer(() => getFruit(fruitName)).subscribe(setFruitDetail);
};

This doesn’t fix the issue yet. We still need to unsubscribe if fruitName changes.

According to React’s docs, we can return a function that’ll be executed at the end of our effect. This acts as the cleanup logic.

So something like this:

() => {
  if (!fruitName) {
    return;
  }

  const subscription = defer(() => getFruit(fruitName)).subscribe(
    setFruitDetail
  );

  return () => {
    subscription.unsubscribe();
  };
};

It Works!

This experience is much cleaner!

By clicking another fruit, useEffect sees fruitName change and runs the previous effect’s cleanup logic. As a result, we unsubscribe from the previous fetch call and focus on the current one.

Now our UI patiently waits until the user’s done clicking and the latest fruit’s details return.

Thanks for following this tutorial to the end!