クリーンアップと競合
送った順に返るとは限らない
カテゴリを続けて2回選んだとします。1回目のリクエストが混んでいて 1.2 秒かかり、2回目がすぐ返ってきたら、どうなるでしょうか。
- くつを選ぶ。リクエスト A が飛ぶ
- すぐ食器に変える。リクエスト B が飛ぶ
- B が先に返る。画面は食器になる
- あとから 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 回になった時点で画面が古いほうに戻ります。
演習
古い応答を無視して、最後に選んだカテゴリが残るようにします。