ItIron2023
react
我們昨天看了一個看似useEffect在搞事的問題,雖然說不能與它完全無關,但實際上他確實挺無辜的,真要怪的話我們就怪react為什麼要這樣設計吧! 今天我們另外再來看個也是會在useEffect時碰上的問題,這類的情境往往發生在data fetch的時候,馬上來看一下今天的題目吧!
首先一樣請你看一下今天的codesandbox,要稍微耐著點性子,這次題目較長一些。
今天的例子就稍微比較複雜一些,首先我們有個下拉的選單讓你去選取你想fetch哪個userId的資料,當你下好離手後便會開始fetch需要的資料,資料請求完後會得到一個userData,病在下方顯示這次請求到userData中的name值,若你今天最終顯示的名字與發出請求時的userId有對應到,那麼便會顯示綠色字體,反之則顯示紅色。
請求的過程我塞了一些邏輯,這個部分你可以暫時忽略,都是為了觸發我接著要描述的情況。
乍看之下這份程式碼並沒有什麼大問題,每次點選後並等待一段時間後,最終顯示的名字與userId都是有匹配上的。
但若是你今天按照以下的操作,如以下的gif所示
你會發現有趣的事情,最終的畫面是停在這
我明明是選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該出手的時機了,主流上有三種解決的方式,我這邊先列出其中兩種。
這個方法相對直觀一些,我們建立一個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]);
這個方法理論上是更為理想的,原因在於我們並不僅是阻止update state的行為,而是連不必要的請求也一併取消了,AbortController可以建立一個實體,並利用signal去追蹤目前fetch或是dom操作這類的非同步行為,給予你在必要的時候終止操作的選項,下方便是一個基本的範例,不過由於範例中用的api請求極為快速,為了讓他有機會去取消request,你必須在瀏覽器動點手腳,我建議將瀏覽器請求限制為slow 3G來看最終的效果。
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]);
這可不是幹話,有不少團隊在實務上是將請求利用第三方套件完全抽離的,像是swr或是react-query都是常見的選擇,讓套件替你去處理這類的race condition以及一些cache的問題。
我們今天看了一個相對進階一些的範例,需要用到你之前幾天學到的知識才有可能正確解出,至於AbortController或是第三方套件的使用這就交給你自己去研究了,當然你還存在著其他的做法,比方說利用一些行為去限制請求的發生,確保這類的情況不會出現!至於哪一種最好呢...就交給你自己判斷囉,給你一些時間消化,我們明天見吧!
本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!