https://ja.wikipedia.org/wiki/%E7%AB%B6%E5%90%88%E7%8A%B6%E6%85%8B
簡単に言えば、2つ以上のスレッドが1つの共有リソースにアクセスして起こる状態(またはそれによって発生するバグ)と考えてください。
レースコンディションの良い例:最近、いくつかのクエリを高速で押しました。 その結果、検索語と画面の結果が一致しないバグが表示されます。
ここで2つ以上のスレッドとは何ですか、1つの共有リソースは何でしたか?
複数のスレッドは検索要求です:
検索を実行すると fetch('/search?q=コーヒー') のようなコードが実行され、もしユーザが 2 つの検索を実行すると 2 つの fetch が実行されます。
1つの共有リソースは検索結果View(UI)です:
fetchの後に実行されるコードは、検索結果を持ってUIを更新するコードが実行されることは明らかです。 すべての fetch は 1 つの検索結果リスト (UI) に書き込み操作を試みるので 共有リソース になります。
YogiyoアプリはiOSネイティブ言語で書かれている可能性が高いですが、これらのRace ConditionのバグはWebで書かれたアプリでも十分に発生する可能性があります。
あなたのコードはシングルスレッドで実行されますが、ブラウザはマルチスレッドで動作するためです。
以下のコードは、Race Conditionのバグを生成する簡単な例です。
const SearchResult = () => {
const [result, setResult] = useState();
const onClickMusic = async (name: string) => {
setResult(
await fetch(`/music/${name}`),
);
};
return(
<div>
<div>
<button onClick={() => onClickMusic('music1')}>
music 1
</button>
<button onClick={() => onClickMusic('music2')}>
music 2
</button>
<button onClick={() => onClickMusic('music3')}>
music 3
</button>
</div>
<div>
選択した曲の情報は{result}です。
</div>
</div>
);
};
特別なコードではありません。 一般的に多く書かれたコードであり、このコードは上記のヨギヨバグをそのまま持っています。
どのような場合にバグが発生しますか?
music1をクリック -> fetch(music1) -> music2をクリック -> fetch(music2) -> setResult(music2) -> setResult(music1)
このような場合を考えてください。
__ユーザーが最後に押すと「music2」が表示されますが、画面には「music1」に関する情報が表示されます。
これらの問題の原因は、ネットワーク要求がどれくらいかかるかを知らず、遅く出発した要求が最初に到着する可能性があります。
この記事の最初の映像だけを見ても分かるように、これはまれに発生する現象ではなく、心を食べればとても簡単に再現できるという点にあります。
そうではありません。
ネットワークが完了した後に実行されるコードがより複雑にチェーンされていて(複数の連鎖useEffectなど)、より多くの値を変更した場合、今は単純なUIのバグではなく本当のバグを作成できます。 難しくなる可能性があります。
Reactを使用している場合は、SuspenseとRender-as-you-fetchパターンに置き換えます。
私の他の記事から 言及したように、Suspenseは単にローディングSpinnerを回すためのパターンではありません。
詳細については、React 公式ドキュメントでの Suspense との競合状態 をご覧ください。
最も簡単な方法です。
isLoadingという名前のステートをもう1つ作成し、すでに進行中のリクエストがある場合は、次のリクエストを無視します。
const Component=()=>{
const [isLoading、setIsLoading] = useState(false);
const [result, setResult] = useState();
const onClickMusic = (name: string) => {
if (isLoading) return;
try{
setIsLoading(true);
setResult(
await fetch(`/music/${name}`),
);
} finally {
setIsLoading(false);
}
};
/* .... */
};
この方法にはマイナーな欠点があります。
要求が終了するまで、コンポーネントはブロック状態になります。
ネットワーク要求は状態が悪い場合は10秒以上かかることもあり、インターネットがまったくならない場合は30~60秒が過ぎた後、結局何もしないまま__失敗状態に戻る場合もあります。
ネットワークの面白いことは、ロードに時間がかかりすぎてユーザーが別のアクションを取ると、そのアクションに対する要求は非常に迅速に戻る可能性があることです。
(ページが長時間表示されず、F5を押してすぐに表示される場合)
isLoadingメソッドは、これらの幸運な場合の可能性を根本的にブロックします。
(マイナーな欠点だと言っただけに必ず解決しなければならない問題ではないかもしれません。)
より良いアプローチは、最後の要求のみを有効な要求として認識することです。 これはUXでより良い経験を提供します。
const Component=()=>{
const fetchCounter = useRef(0);
const [result, setResult] = useState();
const onClickMusic = (name: string) => {
const prevFetchCounter = ++fetchCounter.current;
const newResult = await fetch(`/music/${name}`);
// リクエスト間に異なるリクエストがあったかどうかを調べます。
if(prevFetchCounter === fetchCounter.current) {
setResult(newResult);
}
};
/* .... */
};
fetchCounterという変数を作成し、要求の開始前にローカル変数にコピーします。fetchであり、通常は時間がかかります(0.1秒も時間がかかる作業です))。fetchの途中で新しいfetchが実行されたことを意味します。 私が持っている newResultは今古い値なので、上書きせずに終了します。数行以内の短いコードですが、簡単にすることができます。
このコードが不慣れな場合は、理解するまで読んでみてください。
すでに実行されているfetchはキャンセルできます。 前のリクエストをキャンセルするコードを追加すると、完璧なコードになります。
もしこのトピックについて興味があるなら、ここでもっと読むことができる記事があります:
레이스컨디션