In the React component, we execute the method in useEffect() and return a function to clean up its side effects. Here is a scenario in our business where custom Hooks are used to call the interface every 2s to update data.

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() = > {
    const id = setInterval(async() = > {const data = await fetchData();
      setList(list= > list.concat(data));
    }, 2000);
    return () = > clearInterval(id);
  }, [fetchData]);

  return list;
}
Copy the code

🐚 problem

The problem with this method is that it does not take into account the execution time of the fetchData() method, which can result in a pile of polling tasks if it takes longer than 2s. In addition, it is necessary to make this timing dynamic in the future so that the server can deliver the interval to reduce the pressure on the server.

So here we can consider using setTimeout instead of setInterval. Since the delay is set after the last request is completed each time, it ensures that they don’t pile up. Here is the modified code.

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() = > {
    let id;
    async function getList() {
      const data = await fetchData();
      setList(list= > list.concat(data));
      id = setTimeout(getList, 2000);
    }
    getList();
    return () = > clearTimeout(id);
  }, [fetchData]);

  return list;
}
Copy the code

However, changing to setTimeout introduces new problems. The next setTimeout execution will wait for fetchData() to complete. If we unload the component before fetchData() has finished, clearTimeout() will meaninglessly clear the current callback, and the new delayed callback created by calling getList() after fetchData() will continue to execute.

Online example: CodeSandbox

You can see that the number of interface requests continues to increase after the button is clicked to hide the component. So how do you solve this problem? Several solutions are provided below.

🌟 How to solve the problem

🐋 Promise Effect

This problem is caused by the inability to cancel the subsequent setTimeout() that has not been defined during the Promise execution. So the initial thought was that instead of logging timeoutID directly, we should log the entire logical Promise object up. When the Promise completes, we clear the timeout to make sure we clear the task exactly each time.

Online example: CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() = > {
    let getListPromise;
    async function getList() {
      const data = await fetchData();
      setList((list) = > list.concat(data));
      return setTimeout(() = > {
        getListPromise = getList();
      }, 2000);
    }

    getListPromise = getList();
    return () = > {
      getListPromise.then((id) = > clearTimeout(id));
    };
  }, [fetchData]);
  return list;
}
Copy the code

🐳 AbortController

The above solution solves the problem better, but the Promise task is still executed when the component is uninstalled, resulting in a waste of resources. Let’s think about it a little differently. Promise asynchronous requests should also be a side effect of a component that needs to be “cleaned up.” Once the Promise task is cleared, the subsequent process will not execute, and this will not be a problem.

The clear Promise can currently be implemented with AbortController, and we end up letting the code go into the Reject logic by executing controller.abort() on the unload callback, preventing subsequent code execution.

Online example: CodeSandbox

import { useState, useEffect } from 'react';

function fetchDataWithAbort({ fetchData, signal }) {
  if (signal.aborted) {
    return Promise.reject("aborted");
  }
  return new Promise((resolve, reject) = > {
    fetchData().then(resolve, reject);
    signal.addEventListener("aborted".() = > {
      reject("aborted");
    });
  });
}
function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() = > {
    let id;
    const controller = new AbortController();
    async function getList() {
      try {
        const data = await fetchDataWithAbort({ fetchData, signal: controller.signal });
        setList(list= > list.concat(data));
        id = setTimeout(getList, 2000);
      } catch(e) {
        console.error(e);
      }
    }
    getList();
    return () = > {
      clearTimeout(id);
      controller.abort();
    };
  }, [fetchData]);

  return list;
}
Copy the code

🐬 status flag

In the above scenario, we essentially let the asynchronous request throw an error, interrupting the execution of subsequent code. Is it ok if I set a tag variable and the tag is in a non-unload state to execute subsequent logic? So the scheme came into being.

Defines an unmounted variable that is marked true if unloaded in the callback. If unmounted === true, do not follow the logic to achieve the same effect.

Online example: CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() = > {
    let id;
    let unmounted;
    async function getList() {
      const data = await fetchData();
      if(unmounted) {
        return;
      }

      setList(list= > list.concat(data));
      id = setTimeout(getList, 2000);
    }
    getList();
    return () = > {
      unmounted = true;
      clearTimeout(id);
    }
  }, [fetchData]);

  return list;
}
Copy the code

🎃 afterword.

The essence of the problem is how to clean up the side effects of component uninstallation during a long asynchronous task.

In fact, this is not limited to the Case of this article, we often write in useEffect request interface, return after updating the State logic will also have similar problems.

Just because setState has no effect on an unloaded component, there is no perception at the user level. React will also help us identify the scenario and warn if the setState operation is performed after uninstalling the component.

In addition, asynchronous requests are usually faster, so no one will notice this problem.

So what other solutions do you have to solve this problem? Welcome to leave a comment

How To Call Web APIs with the useEffect Hook in React