useEffectでレースコンディションを防ぐ

レースコンディションが発生しうるコード

 import { useEffect, useState } from "react";
 
 function App() {
   const [id, setId] = useState(0);
   const [name, setName] = useState("");
 
   useEffect(() => {
     void (async () => {
       const response = await fetch(`https://swapi.dev/api/people/${id}/`);
       const data = await response.json();
       setName(data.name);
     })();
   }, [id]);
 
   return (
     <div className="App">
       <button
         onClick={() => {
           const id = Math.round(Math.random() * 30);
           setId(id);
         }}
       >
         Fetch data
       </button>
       <p>ID: {id}</p>
       <p>Name: {name}</p>
     </div>
   );
 }
 
 export default App;
  • ボタンを押すと、idがランダムで決まり、そのidのデータをバックエンドAPIから取得して、nameとして表示するアプリ
  • APIからの取得はuseEffect内で非同期で行っている
  • この時、ボタンを連打すると、その都度idが変わりuseEffectが何度も呼ばれる
  • APIは非同期なので、前のAPIの取得(fetch())に時間が掛かり、後のAPIの取得がすぐに終わると、後のAPIのレスポンスで作ったnameが前のAPIのレスポンスのnameで上書きされる

レースコンディションが発生しないコード

  useEffect(() => {
    let cancel = false;
    void (async () => {
      const response = await fetch(`https://swapi.dev/api/people/${id}/`);
      const data = await response.json();
      if (!cancel) {
        setName(data.name);
      }
      return () => {
        cancel = true;
      }
    })();
  }, [id]);
  • useEffectでクリーンアップ関数を指定すると、そのコンポーネントがmountされた時、useEffectが再度実行された時にクリーンアップ関数が実行される
  • これを利用してレースコンディションを防ぐ
  • 上のように、useEffect内でcancelというフラグを用意し、そのフラグを調べてからsetName()するようにする
  • これによって、ボタンを連打した場合、後のAPIが呼ばれる時に前のAPIのcancelをtrueにするので、前のAPIのfetch()がようやく終わってsetName()しようとした時にはif (!cancel)が通らなくなってる

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS

Last-modified: 2022-02-07 (月) 16:15:54