きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

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 によるとモダンブラウザなら普通に使えるようです。

使い方は AbortControllernew してメンバの signalfetch に仕込むと 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...
  )
}

もう少し簡潔に書けると思ったのですが、とりあえずこんな感じです。 controllerfetch が終了したかどうかも取得できれば 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 年現在では普通に使えているようで何よりです。