🥺
useEffectの中で呼ぶPromiseをキャンセルしたい
問題
例えば、以下のようなコードがあるとする。
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
を使おう(頂いたコメント📝より)。
詳しくはこちら
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 のキャンセルについては下記ページが詳しいです
RxJS を利用する場合は fromFetch が便利で良いのかなと思います
Promise にはキャンセルに関する仕様がないので
どうしようもない場合も多いのですが
対応できる場合はきちんと対応して行きたいですね…
コメントありがとうございます!
AbortControllerもfromFetchも知らなかった……。