🏇

useEffect()を使ったデータ取得で起こる問題とクリーンアップ

2022/07/09に公開

You Might Not Need an EffectというuseEffect()が必要でない事例を紹介しているReactのオフィシャルDocsを読んでいて、"Fetching data"のところを理解するのに頭を整理する必要があり学びもあったから、記事にしてみました。

問題のあるコード

(この記事で使っている例のコードは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);
  }
  // ...
}

Click 👉 to read the article in English.Problems with data fetching Effect, and Cleanup

この例では、”競合状態”という問題が起こる可能性があります。
競合状態 - Wikipedia

記事の例をそのままとりますが、"hello "と速くタイプした時を考えてください。
クエリは"h"から"he"、"hel"、"hell"、"hello"へと変化し、このインプットの変化により、別々のデータ取得を開始します。
"hello"が最後にタイプされているので、データ取得の結果も"hello"のものが最後に返ってくることを期待しますが、そうはならないかも知れないというのが問題点です。
"hell" のレスポンスが "hello" のより後になる可能性もあり、そうなるとsetResults()が最後に実行された"hell"も結果が表示されてしまうことになります。

ビジュアライズしてみるとこんな感じ。
データ取得の時に順位の入れ替えがおきて、最後にデータ取得の結果が返ってきた"hell"の結果が最終的なresultsになってます。
race-condition

クリーンアップコードを利用した解決方法

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);
      }
    });
    // ====== 💫 ここがポイント =====
    return () => {
      ignore = true;
    };
    // ============================
  }, [query, page]);

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

では、解決策のコードを見てみると、クリーンアップが追加されています。
クリーンアップでは、ignoreという変数を用いて、setResults()の実行を制御しています。
ここで、私は頭の整理が必要でした。

まずは、useEffect()のクリーンアップが実行されるタイミングを確認してみます。
Reactの公式Docsでは、

  1. React はコンポーネントがアンマウントされるときにクリーンアップを実行する。
  2. ひとつ前のレンダーによる副作用を、次回の副作用を実行する前にもクリーンアップする。

とあります。
この、2のタイミングが今回の大事なポイントです。
"he"、"hel"、"hell"、"hello"の順でuseEffect()が実行されていき、次のuseEffect()が実行される前のタイミングで、ひとつ前のuseEffect()がクリーンアップされます。
今回の例では、クリーンアップでignoretrueにしているので、クリーンアップがデータ取得の完了前に実行されたuseEffect()ではsetResults()は実行されません。
一番最後にuseEffect()が実行されている"hello"は、次のuseEffect()がない為、クリーンアップが実行されず、結果setResults()が最後に実行されることとなります。

これをビジュアライズしてみるとこんな感じだと思います。
cleanup

これがクリーンアップを用いたデータ取得の副作用となります。

さいごに

今回は、useEffect()のクリーンアップについて学べ、なぜデータ取得のuseEffect()では、クリーンアップを実施することが大事であるかわかりました。

データ取得の副作用は、custom hookに抜き出すのが良いプラクティスとされています。そのことや、他にuseEffect()を使うべきでない状況が多く紹介されている元の記事はとても面白い内容なので、ぜひご一読してみてください。
You Might Not Need an Effect

This article is also available in English: https://dev.to/takuyakikuchi/problems-with-data-fetching-effect-and-cleanup-1397

GitHubで編集を提案

Discussion