Reactコンポーネントのアンマウント後の更新を避けるために AbortController を使ってみる
これです。
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 a useEffect cleanup function. in UserList (at App.tsx:53)
この warning を消すために useEffect
のクリーンアップ処理をきちんと実装してみようと思います。
動作するコードは github に置いてあります。
AbortController
fetch
を中断するために AbortController というものが利用できるみたいです。Can I use によるとモダンブラウザなら普通に使えるようです。
使い方は AbortController
を new
してメンバの signal
を fetch
に仕込むと AbortController
インスタンス経由でその fetch
を中断できる、というものです。
const controller = new AbortController() const { signal } = controller fetch('http://localhost:3080/', { signal })
useEffect 内で利用してみる
以上のサイトを参考に fetch
が完了する前にアンマウントされた場合にクリーンアップ処理で fetch
を中断するようなコードを書きました。useEffect
で返される関数がアンマウント時(もしくは同じ useEffect
が再実行された時)に呼ばれクリーンアップ処理として作用します。
interface Data { // data structure... } const UserList: React.FC = () => { const [data, setData] = useState<Data>() const [error, setError] = useState<Error>() useEffect(() => { const controller = new AbortController() const { signal } = controller let finished = false fetch('http://localhost:3080/', { signal }) .then(res => res.json()) .then(setData) .catch(err => { // `abort` した後に `setError` してしまうと結局冒頭のエラーが出るので弾く if (err.name !== 'AbortError') setError(err) }) .finally(() => { finished = true }) return () => { if (!finished) { controller.abort() console.log('UserList: Fetch aborted') } } }, [setData, setError]) if (error) return <div>{error.toString()}</div> if (!data) return <div>Loading...</div> return ( // Render data... ) }
もう少し簡潔に書けると思ったのですが、とりあえずこんな感じです。
controller
で fetch
が終了したかどうかも取得できれば finished
なんて変数を利用しなくてもいいんですけどね。
型定義を見る限り aborted
しか取れなさそうです。
// lib.dom.d.ts interface AbortController { readonly signal: AbortSignal; abort(): void; } interface AbortSignal extends EventTarget { readonly aborted: boolean; onabort: ((this: AbortSignal, ev: Event) => any) | null; addEventListener<K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; removeEventListener<K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; }
https://blog.jxck.io/entries/2017-07-19/aborting-fetch.html の記事を読みながら使えるようになるまでに色々あったんだなと思いつつ、2019 年現在では普通に使えているようで何よりです。