iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Modern Web

30天React練功坊-攻克常見實務/面試問題系列 第 11

30天React練功坊-攻克常見實務/面試問題 Day11: Race condition with useEffect

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

我們昨天看了一個看似useEffect在搞事的問題,雖然說不能與它完全無關,但實際上他確實挺無辜的,真要怪的話我們就怪react為什麼要這樣設計吧! 今天我們另外再來看個也是會在useEffect時碰上的問題,這類的情境往往發生在data fetch的時候,馬上來看一下今天的題目吧!

本日題目

首先一樣請你看一下今天的codesandbox,要稍微耐著點性子,這次題目較長一些。

day11-demo-image

今天的例子就稍微比較複雜一些,首先我們有個下拉的選單讓你去選取你想fetch哪個userId的資料,當你下好離手後便會開始fetch需要的資料,資料請求完後會得到一個userData,病在下方顯示這次請求到userData中的name值,若你今天最終顯示的名字與發出請求時的userId有對應到,那麼便會顯示綠色字體,反之則顯示紅色。

請求的過程我塞了一些邏輯,這個部分你可以暫時忽略,都是為了觸發我接著要描述的情況。
乍看之下這份程式碼並沒有什麼大問題,每次點選後並等待一段時間後,最終顯示的名字與userId都是有匹配上的。

但若是你今天按照以下的操作,如以下的gif所示

  1. 重新載入頁面,你應該會看見他正在loading user1的資料
  2. 快速點選user 2
  3. 快速點選user 3

day11-demo-gif

你會發現有趣的事情,最終的畫面是停在這

day11-demo-image-2

我明明是選user3,但最後出來的名字卻是屬於user1的,請觀察以下的程式碼並試著修復此問題。

function sleep(delay) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(), delay);
  });
}

const expectedUserMapping = {
  "1": "Leanne Graham",
  "2": "Ervin Howell",
  "3": "Clementine Bauch"
};

export default function UserProfile() {
  const [userId, setUserId] = useState(1);
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${userId}`
      );
      const result = await response.json();
      // plz forgive me for writing this, it's just for demo
      await sleep(userId === 1 ? 5000 : userId === 2 ? 3000 : 1000);
      setUserData(result);
    };
    fetchData();
  }, [userId]);

  return (
    <div>
      <h1>Race condition with useEffect</h1>
      <select onChange={(e) => setUserId(e.target.value)} value={userId}>
        <option value={1}>User 1</option>
        <option value={2}>User 2</option>
        <option value={3}>User 3</option>
      </select>

      {userData ? (
        <h2
          style={{
            color:
              userData.name === expectedUserMapping[userId] ? "green" : "red"
          }}
        >{`User Name: ${userData.name}`}</h2>
      ) : (
        "Loading..."
      )}
    </div>
  );
}

解答與基本解釋

這也是個相當值得玩味的問題,實際上這跟react並沒有太直接相關,單純是js promise常出現的race condition,只是在不使用三方套件的情況下大家多半會把fetch請求放在useEffect中,因此這個問題也是經常在各大影片或是教學中會出現的範例之一。

今天題目你已經知道是race condition了,那麼你只要針對這點下手即可,我們希望當我們切換userId時發出新請求,同時在重新渲染前阻止前一個發送的請求去更新我們的userData狀態,這類的情況就是我們之前提過cleanup function該出手的時機了,主流上有三種解決的方式,我這邊先列出其中兩種。

  1. 主動建立變數控制

這個方法相對直觀一些,我們建立一個flag來決定是否發出的請求是否要更動最終的狀態,若在請求完成前我們就重新渲染便會利用cleanup function將isCancelled設為true,這麼一來在之前的請求完成後就不會更新state,僅有最後一個發出的請求會去更新state。

useEffect(() => {
  let isCancelled = false;

  const fetchData = async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    const result = await response.json();
    await sleep(userId === 1 ? 7000 : userId === 2 ? 3000 : 1000);

    if (!isCancelled) {
      setUserData(result);
    }
  };

  fetchData();

  return () => {
    isCancelled = true;
  };
}, [userId]);
  1. AbortController

這個方法理論上是更為理想的,原因在於我們並不僅是阻止update state的行為,而是連不必要的請求也一併取消了,AbortController可以建立一個實體,並利用signal去追蹤目前fetch或是dom操作這類的非同步行為,給予你在必要的時候終止操作的選項,下方便是一個基本的範例,不過由於範例中用的api請求極為快速,為了讓他有機會去取消request,你必須在瀏覽器動點手腳,我建議將瀏覽器請求限制為slow 3G來看最終的效果

day11-demo-image-3

useEffect(() => {
  const abortController = new AbortController();

  const fetchData = async () => {
    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, { signal: abortController.signal });
      const result = await response.json();
      await sleep(userId === 1 ? 7000 : userId === 2 ? 3000 : 1000);
      setUserData(result);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Fetch error:', error);
      }
    }
  };

  fetchData();

  return () => {
    abortController.abort();
  };
}, [userId]);
  1. 不要在useEffect中fetch資料

這可不是幹話,有不少團隊在實務上是將請求利用第三方套件完全抽離的,像是swr或是react-query都是常見的選擇,讓套件替你去處理這類的race condition以及一些cache的問題。

總結

我們今天看了一個相對進階一些的範例,需要用到你之前幾天學到的知識才有可能正確解出,至於AbortController或是第三方套件的使用這就交給你自己去研究了,當然你還存在著其他的做法,比方說利用一些行為去限制請求的發生,確保這類的情況不會出現!至於哪一種最好呢...就交給你自己判斷囉,給你一些時間消化,我們明天見吧!

本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!


上一篇
30天React練功坊-攻克常見實務/面試問題 Day10: useEffect got called twice with empty dependency array
下一篇
30天React練功坊-攻克常見實務/面試問題 Day12: Sometimes useState just not good enough
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言