Simple minds think alike

より多くの可能性を

【React hooks】"Warning: Can't perform a React state update on an unmounted component."の止め方

React hooksで実装したSPAアプリをChromeで動かした際に、以下のWarningが表示されていたので対処してみました。

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 %s.%s","a useEffect cleanup function","\n in UserPage 

例えば、以下のようなコードで useEffect の中でAPIにアクセスして、返ってきたら state にセットするようなアプリケーションでよく発生するWarningかと思います。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [user, setUser] = 
    useState({ 
      id: 1, 
      first_name: "安倍", 
      last_name: "晋三"
    });

  useEffect(() => {
    axios.get('https://reqres.in/api/users/2')
      .then((response) => {
        if (!!response.data) {
          setUser(response.data.data)
        }
      })
  }, []);

  return (
    <div className="App">
      <header className="App-body">
        ID:{user.id}、名前:{user.first_name} {user.last_name}
      </header>
    </div>
  );
}

export default App;

前提

  • React: 17.0.1
  • Axios: 0.21.1

Warningの発生原因

Warningを出しているReactのコードを確認したところ以下のコメントが書いてありました。

Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.: 1. Updating an ancestor that a component had registered itself with on mount. 2. Resetting state when a component is hidden after going offscreen.

例えば、APIからレスポンスが返ってくるまでに時間がかかっていて、レスポンスを受け取る前に別ページに遷移した等、コンポーネントがアンマウントされている場合に、stateを保存する(上記の例だとsetUser)タイミングで保存できずにWarningが表示されるようです。

Warningの表示を管理している変数のコメントを確認したところ、開発環境でしか表示されないWarningのようなので、本番環境におけるメモリリークを回避するためにも開発時に見かけたタイミングで対処しておきたいWarningです。

対応パターン①:アンマウント状態を変数で管理する

以下のようにアンマウント時にisMounted変数を更新するクリーンナップ関数を追加したうえで、既にアンマウントされている状態の場合に保存しないようにしたらWarningがなくなりました。

useEffect の戻り値に関数が指定されている場合、それはクリーンナップ関数になり、コンポーネントのアンマウント時に1度だけ実行されます。

useEffect(() => {
  let isMounted = true
  axios.get('https://reqres.in/api/users/2')
    .then((response) => {
      if (!!response.data) {
        if (isMounted) {
          setUser(response.data.data)
        }
      }
    })

  return () => { isMounted = false };
}, []);

クリーンナップ関数の詳細は、公式ドキュメントに記載されています。 reactjs.org

メリット

この対応のメリットとしては

  • 様々なケースで対処できる
    • ケース①:APIからレスポンスが遅い場合
    • ケース②:APIからレスポンスを取得した後の処理が長い場合

デメリット

この対応のデメリットとしては

  • 管理する変数が1つ増える

ため汎用的ではありますが、コードが煩雑になり、保守しづらくなることかと思います。

対応パターン②Ajaxをキャンセルする

APIからレスポンスが返ってくるのが遅いことが主な原因であれば、以下のように axios のキャンセルトークン等を利用してAPIリクエストをキャンセルすると良さそうです。

useEffect(() => {
  const source = axios.CancelToken.source()
  axios.get('https://reqres.in/api/users/2')
    .then((response) => {
      if (!!response.data) {
        setUser(response.data.data)
      }
    })
  return () => { 
    source.cancel("APIはキャンセルされました");
  };
}, []);

メリット・デメリット

この対応は、変数( source )が増えてはいますが、成功時の処理の中(thenの中)では使われないので、対応パターン①とは反対に保守がしやすいというメリットはあるものの、汎用的ではないためAjaxをキャンセルしてWarningがなくなるケース以外ではWarningが残るというデメリットがありそうです。

参考資料