DEV Community

Cover image for Custom React Hook to cancel network calls, and then synchronize API calls with component life cycle
Akashdeep Patra
Akashdeep Patra

Posted on • Updated on

Custom React Hook to cancel network calls, and then synchronize API calls with component life cycle

First let's talk about the problem we're trying to solve here

If you're working with React it's almost impossible that you never saw this error log in your browser console

Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount
method.
    in TextLayerInternal (created by Context.Consumer)
    in TextLayer (created by PageInternal) index.js:1446
d/console[e]
Enter fullscreen mode Exit fullscreen mode

not gonna lie this is probably one of the most painful things to get your head around after you have gained a good understanding of how component lifecycle works. This error basically means you are using an asynchronous block of code that has some state mutation inside it (By state mutation I mean setState ) thus resulting in a memory leak

Although in most cases it's harmless, there's still a possibility of un-optimized heap usage, chances of your code breaking, and all the other good stuff that goes around with it.

Now let's talk Solutions

well, there are a couple of ways we can tackle this problem, one of the most popular solutions is to use any logic that checks if the component is still mounted in the component tree and make any state change operation only then and you'd think that would just solve your problems right? right ??
well.... kinda, I mean let's take a step back and think about a very famous hook useIsMounted

now Think about a scene where you are making an API call on the mount of a component and using this hook you'll change the state only if it's still mounted

  const isMounted = useIsMounted();
  const [value, setValue] = useState();

  useEffect(() => {
    fetch('some resource url')
      .then((data) => {
        return data.json();
      })
      .then((data) => {
        if (isMounted()) {
          setValue(data);
        }
      });
  }, [input]);
Enter fullscreen mode Exit fullscreen mode

Looks like a perfectly okay piece of code that totally doesn't throw any errors right? well yeah I mean this works!!

But

  • Aren't you still making the fetch call?

  • Aren't you still fulfilling the promise? what you clearly don't need to do if the component is already unmounted right?

And depending on how API-driven your application is avoiding to fulfill all the network requests might benefit you in ways that you never considered

So how can we do that? well we can just cancel the ongoing request and as it turns out, modern browsers have had this feature for quite some time

The AbortController Interface allows you to, ya know just abort any web request.

As of now browser's fetch API and Axios officially supports AbortControllers

Now we can just be done with this here, but just to make it look a little bit cooler let's make a custom hook out of this and look at a live example

useAbortedEffect hook to cancel any network requests when the component unmounts

import { useEffect } from 'react';

const useAbortedEffect = (
  effect: (signal: AbortSignal) => Function | void,
  dependencies: Array<any>
) => {
  useEffect(() => {
    const abortController = new AbortController();
    const signal = abortController.signal;
    const cleanupEffect = effect(signal);

    return () => {
      if (cleanupEffect) {
        cleanupEffect();
      }
      abortController.abort();
    };
  }, [...dependencies]);
};

export default useAbortedEffect;

Enter fullscreen mode Exit fullscreen mode

Now let's break things down to understand what's going on. our custom effect takes a callback function that accepts an AbortSignal param, and a dependency array as an argument just like any other effect hook, inside our useEffect we instantiate an AbortController and pass the signal into our effect callback so that any network request we want to make should be able to get this signal. this would help us to control the execution cycle of all the APIs that will be declared in our effect callback. and in the unmount callback of our useEffect we just abort the controller and any network call that is going on in our effect will be canceled from the browser

Let's take an example to appreciate this hook

In this example, we'll be creating 3 nested routes using React router's Outlet API to make each page mount and re-mount consecutively so that we can monitor the network tab

import { Outlet, useNavigate } from 'react-router-dom';

const Home = () => {
  const navigate = useNavigate();
  return (
    <div>
      Home Page
      <div className="column">
        <button onClick={() => navigate('/first')}>First</button>
        <button onClick={() => navigate('/second')}>Second</button>
        <button onClick={() => navigate('/third')}>Third</button>
        <Outlet />
      </div>
    </div>
  );
};

export default Home;

Enter fullscreen mode Exit fullscreen mode

In each of our pages first, second & third we will use our custom hook to fire an API and pass the signal argument to the signal properties of fetch and Axios in order to control the request (remember this step is mandatory because any request that doesn't have this signal would not be canceled)

The First page component would look something like this

  //example with axios
  useAbortedEffect(
    (signal) => {
      axios
        .get('https://jsonplaceholder.typicode.com/posts', {
          signal
        })
        .then((data) => {
          console.log('First API call');
        })
        .catch((e: any) => {
          if (e.name === 'CanceledError') {
            console.log('First API aborted');
          }
        });
    },
    []
  );

return (
    <div>
      First Page
      <div
        style={{
          display: 'flex',
          gap: '10px',
          marginTop: '20px'
        }}>
        <button onClick={() => setCount(count + 1)}>Click </button>
        <span>Count : {count}</span>
      </div>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Now since I'm using a JSON placeholder as an endpoint suffice to say noticing any pending state of the network call would be tricky so let's simulate a slower network
In the dev-tool open up the network tab and select Slow 3G
from the networks dropdown (I'm using Chrome)

Chrome network tab slow3G

Now after starting the application start clicking on the First, Second & third link in the exact order and look at the network tab
Network tab screenshot

and since we had used console.log at each step in our custom effect let's look at the console too
Console screen shot

As you can see in after consecutively mounting and remounting the First and Second pages all the pending requests got canceled because of the Abort signal and we can see the exact console logs as well. This would work similarly to debouncing in javascript but instead of debouncing with timers during the event loop, we'll be debouncing network requests in the browser itself.

What you can achieve with this hook?

Well depending on how you have architected your application and how much API-driven it is , potentially you could

  • Avoid memory leaks in the components

  • Make Atomic API transactions with respect to your Component

  • Make less number of API calls altogether.

Github repo for the example

Do comment on the article so that I can make this better and improve any mistakes I have made, thanks in advance.

Feel free to follow me on other platforms as well

Top comments (2)

Collapse
 
samintegrateur profile image
Samuel Desbos

Thank you for this hook. I wonder if you have an idea about a trouble I'm struggling with : if I have a loading logic, with something like
finally {
setLoading(false)
}

Then with the "double render" in React 18, there is a first call which is canceled, so far so good, but this finally code happens after the second call has started, so the truthy loading of the second call is overriden and we don't see it. Of course this problem occurs only in dev mode, but it's annoying.

I can fix this with a "isMounted" logic but I thought we were done with this kind of things and it works only inside the useEffect (sometimes I need to make another call in my UI, so move it in a function called at will).

Collapse
 
mr_mornin_star profile image
Akashdeep Patra

Hi @samintegrateur I think with React 18 a lot of the older paradigms have changed , in fact React team has highly suggested not to use useEffect as a lifecycle mechanism,(check this link ) for maintaining api calls i'd highly suggest you use something like SWR or react query , you can also implement the cancellation with them but they do a ton of heavy lifting themselves in terms of these kind of tropes .