React useEffect での競合状態とメモリ リークの回避


「マウントされていないコンポーネントでReact状態の更新を実行できません」という警告に対処する方法を学びましょう

API リクエストからデータを取得する実装を見て、このコンポーネントで競合状態が発生する可能性があるかどうかを確認しましょう.

import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition() {
  const [todo, setTodo] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
      const newData = await response.json();
      setTodo(newData);
    };
    fetchData();
  }, []);
  if (data) {
    return <div>{data.title}</div>;
  } else {
    return null;
  }
}

useEffect React hook への依存関係として空の配列を指定しました.そのため、フェッチ リクエストが 1 回だけ発生するようにしました.ただし、このコンポーネントは依然として競合状態やメモリ リークが発生しやすい傾向にあります.どのように?

API サーバーの応答に時間がかかり、応答を受信する前にコンポーネントがアンマウントされた場合、メモリ リークが発生します.コンポーネントはアンマウントされましたが、要求に対する応答は完了時に引き続き受信されます.その後、応答が解析され、setTodo が呼び出されます. React は次の警告をスローします.

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 a useEffect cleanup function.



そして、メッセージは非常に簡単です.

同じ問題のもう 1 つの潜在的なシナリオは、todo リスト ID が prop として渡されたことです.

import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition( {id} ) {
  const [todo, setTodo] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
      const newData = await response.json();
      setTodo(newData);
    };
    fetchData();
  }, [id]);
  if (data) {
    return <div>{data.title}</div>;
  } else {
    return null;
  }
}

リクエストが終了する前にフックが別の ID を受け取り、最初のリクエストの前に 2 番目のリクエストが終了した場合、コンポーネントには最初のリクエストのデータが表示されます.

競合状態の問題に対する潜在的な解決策



これを修正するには、いくつかの方法があります.どちらのアプローチも、useEffect が提供するクリーンアップ機能を利用しています.
  • boolean フラグを使用して、コンポーネントがマウントされていることを確認できます.このようにして、フラグが true の場合にのみ状態を更新します.また、コンポーネント内で複数のリクエストを行っている場合は、常に最後のリクエストのデータを表示します.
  • コンポーネントがアンマウントされるたびに、AbortController を使用して以前のリクエストをキャンセルできます.ただし、AbortController は IE ではサポートされていません.したがって、このアプローチを使用する場合は、それについて考える必要があります.

  • ブール値フラグを使用した useEffect のクリーンアップ



    useEffect(() => {
      let isComponentMounted = true;
        const fetchData = async () => {
          const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
          const newData = await response.json();
          if(isComponentMounted) {
            setTodo(newData);
          }
        };
        fetchData();
        return () => {
          isComponentMounted = false;
        }
      }, []);
    

    この修正は、useEffect のクリーンアップ機能が機能する方法に依存しています.コンポーネントが複数回レンダリングされる場合、前の効果は次の効果を実行する前にクリーンアップされます.

    これが機能する方法により、ID が変更されるため、複数のリクエストの他の例でも正しく機能します.バックグラウンドで処理中の複数のリクエストが存在するという意味で、まだ競合状態があります.ただし、最後のリクエストの結果のみが UI に表示されます.

    AbortController を使用した useEffect のクリーンアップ



    前のアプローチは機能しますが、競合状態を処理する最良の方法ではありません.リクエストはバックグラウンドで進行中です.バックグラウンドで古いリクエストがあると、ユーザーの帯域幅が不必要に消費されます.また、ブラウザは同時リクエストの最大数 (最大 6 ~ 8) も制限しています.

    how to cancel an HTTP fetch request に関する以前の記事から、DOM 標準に追加された AbortController API について知っています.それを利用して、リクエスト自体を完全に中止できます.

    useEffect(() => {
      let abortController = new AbortController();
        const fetchData = async () => {
          try {
            const response = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
                signal: abortController.signal,
              });
          const newData = await response.json();
            setTodo(newData);
          }
          catch(error) {
             if (error.name === 'AbortError') {
              // Handling error thrown by aborting request
            }
          }
        };
        fetchData();
        return () => {
          abortController.abort();
        }
      }, []);
    

    リクエストを中止するとエラーがスローされるため、明示的に処理する必要があります.

    そして、このソリューションは前のソリューションと同じように機能します.再レンダリングの場合、次のエフェクトを実行する前にクリーンアップ関数が実行されます.違いは、AbortController を使用しているため、ブラウザもリクエストをキャンセルすることです.

    これらは、React の useEffect フックを使用して API リクエストを行う際に競合状態を回避できる 2 つの方法です.リクエストのキャンセルを機能として許可するサードパーティ ライブラリを使用する場合は、Axios を使用するか、他の多くの機能を提供するクエリを反応させることができます.

    ご不明な点がございましたら、下にコメントをお寄せください.

    2021 年 2 月 8 日に https://www.wisdomgeek.com で最初に公開されました.