Takuya Kikuchi
TK's Blog ✍️

TK's Blog ✍️

Problems with data fetching Effect, and Cleanup

Takuya Kikuchi's photo
Takuya Kikuchi
·Jul 9, 2022·

3 min read

Play this article

Click here to read the article in Japanese:zenn.dev/takuyakikuchi/articles/a96b8d97a04..

I was reading the Official React Docs You Might Not Need an Effect, which presents examples where useEffect() is not required.
I wrote this article because I needed to wrap my head around the "fetching data" part where I learned a lot.

Problematic code

(The example code used in this article is taken directly from You Might Not Need an Effect)

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}
`

This example may cause a problem called a "race condition".
Race condition - Wikipedia

To take the example from the article, consider typing "hello" quickly.
The query changes from "h" to "he", "hel", "hell", and "hello", and this change in input initiates separate data fetches.
Since "hello" is typed last, we would expect the "hello" result to be the last one returned, but the problem is that this may not be the case.
It is possible that the "hell" response comes after the "hello" response, and if that is the case, the "hell" result will be displayed as the setResults() is executed last.

The visualization looks like this.
The order of the results is switched during data fetch, and the "hell" results will be the final results.

スクリーンショット 2022-07-09 21.31.48.png

Solution using cleanup code

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1); 
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    // ====== 💫 here's the point =====
    return () => {
      ignore = true;
    }
    // ============================
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

Now, if we look at the code for the solution, we see that cleanup has been added.
The cleanup uses a variable called ignore to control the execution of setResults().
Here, I needed to wrap my head around it.

First, let's see when the useEffect() cleanup is executed.
In the React official doc, Using the Effect Hook – React

  1. React performs cleanup when a component is unmounted.
  2. React also cleans up side-effects from the previous render before executing the next side-effect.

The timing of 2 is important in this case.
The useEffect() is executed in the order of "he", "hel", "hell", and "hello", and the previous useEffect() is cleaned up at the timing before the next useEffect() is executed.
In this example, ignore is set to true in the cleanup, so setResults() will not be executed for the useEffect() that is executed before the cleanup has completed data fetch.
"hello", which is the last useEffect() to be executed, has no next useEffect(), so cleanup is not executed, resulting in setResults() being the last to be executed.

This is what I think the visualization would look like.

スクリーンショット 2022-07-09 21.32.15.png

This is the Effect of fetching data using cleanup.

Finally

In this article, we learned about cleanup in useEffect() and why it is important to implement cleanup in useEffect() data fetch.

It is considered good practice to extract the side effects of data fetch to a custom hook. The original article, which introduces that and many other situations where useEffect() should not be used, is very interesting and I encourage you to read it. You Might Not Need an Effect

 
Share this