クリーンアップと競合

送った順に返るとは限らない

カテゴリを続けて2回選んだとします。1回目のリクエストが混んでいて 1.2 秒かかり、2回目がすぐ返ってきたら、どうなるでしょうか。

  1. くつを選ぶ。リクエスト A が飛ぶ
  2. すぐ食器に変える。リクエスト B が飛ぶ
  3. B が先に返る。画面は食器になる
  4. あとから A が返る。画面がくつに戻る

選んだのは食器なのに、画面はくつ。これが競合 (レースコンディション) です。通信は送った順に返る保証がありません。実務でいちばん再現しにくく、いちばん報告されるバグの一つです。

クリーンアップ関数

useEffect に渡した関数は、後片付けの関数を返せます。

useEffect(() => { let ignore = false; async function load() { const res = await fetch("/api/items?category=" + category); const data: Item[] = await res.json(); if (ignore) return; setItems(data); } load(); return () => { ignore = true; }; }, [category]);

返した関数は、次に effect が動く直前と、部品が画面から消えるときに呼ばれます。つまりカテゴリを変えた瞬間、古いほうの ignore が true になります。古い応答が遅れて返ってきても if (ignore) return; で捨てられ、画面は書き換わりません。

ignore が effect ごとに別物である点が肝心です。effect が動くたびに新しい ignore が作られるので、古い回の変数を書き換えても新しい回には影響しません。

起きたことを数えて確かめる

今回の画面には「受信 n 回」という表示を先に入れてあります。これは if (ignore) return; より前で数えていて、捨てた応答も数に入ります

直したあとに続けて選び直すと、受信は 3 回まで進むのに、並んでいるのは最後に選んだカテゴリのまま。古い応答が確かに届いていて、確かに捨てられていることが数で見えます。直す前は、受信が 3 回になった時点で画面が古いほうに戻ります。

演習

古い応答を無視して、最後に選んだカテゴリが残るようにします。

ヒント

App.tsx
mock-api.json
App.tsx
mock-api.json
プレビュー

できているか

  • 最初は5件出ている
  • くつ・食器と続けて選ぶと、遅れて届いたくつの結果に戻らない