🥺

useEffectの中で呼ぶPromiseをキャンセルしたい

2 min read 2

問題

例えば、以下のようなコードがあるとする。
useState で状態を用意し、 useEffect の中でAPIにリクエストを飛ばす。そしてレスポンスを状態に反映させる。これはよく見るパターンである。

export const MyCount = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    fetch('/api/count')
      .then(response => repsonse.json())
      .then(repsonse => response.count)
      .then(setCount)
  }, [])
  
  return (
    <div>{count}</div>
  )
}

もしこのAPI通信に時間がかかり、 setCount が呼び出される前に <MyCount> コンポーネントを閉じてしまったらどうなるだろうか。そう、メモリリークが生じてしまう。
これはPromiseの実行をキャンセルできないことに起因する。したがって、キャンセルすればいいのである。

解決策

fetch の場合

fetch の場合は AbortController を使おう(頂いたコメント📝より)。

詳しくはこちら

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

useEffect(() => {
  const controller = new AbortController()
  const signal = controller.signal

  fetch('/api/count', { signal }).then(() => {})
  
  return () => controller.abort()
}, [])

それ以外の場合

Observableを使うことでこの問題を解決できる(コードサンプルはfetchのままでよろしくない)。RxJSが好きなのでRxJSを用いた例を紹介する。

export const MyCount = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const subscription = from(
      fetch('/api/count')
        .then(response => repsonse.json())
        .then(repsonse => response.count)
    ).subscribe(setCount)
    
    return () => subscription.unsubscribe()
  }, [])
  
  return (
    <div>{count}</div>
  )
}

このように Observableにすることでキャンセルできる。やったね!

とはいえ、この方法が必ずしも必須になるかといえばそうとは言えない。非同期通信は短時間で終わるし、非同期通信が終わるまでコンポーネントを取り除かない実装にすればいいだけなのである。

swichMap みたいに前の非同期処理をキャンセルして新しい非同期処理を開始したいときには必須のテクニックかな。

export const MyCount = () => {
  const [count, setCount] = useState(0)
  const [, reRender] = useState(false)
  
  useEffect(() => {
    const subscription = from(
      fetch('/api/count')
        .then(response => repsonse.json())
        .then(repsonse => response.count)
    ).subscribe(setCount)
    
    return () => subscription.unsubscribe()
  })
  
  return (
    <>
      <div>{count}</div>
      <button type="button" onClick={() => reRender(v => !v)}>
        re-render
      </button>
    </>
  )
}

Discussion

この例だと fetch そのものをキャンセルする方が良いですね
そうでなければリクエストは継続されるのでメモリリークは防げません

fetch のキャンセルについては下記ページが詳しいです

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

RxJS を利用する場合は fromFetch が便利で良いのかなと思います

https://rxjs.dev/api/fetch/fromFetch

Promise にはキャンセルに関する仕様がないので
どうしようもない場合も多いのですが
対応できる場合はきちんと対応して行きたいですね…

コメントありがとうございます!
AbortControllerもfromFetchも知らなかった……。

ログインするとコメントできます