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
メリット
この対応のメリットとしては
デメリット
この対応のデメリットとしては
- 管理する変数が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が残るというデメリットがありそうです。