Race Condition
A race condition occurs when two or more operations must execute in the correct order, but the program has not been properly synchronized to ensure this order.
Data fetching 系列文
1. Data fetching and performance
2. Race condition
3. Fetch: AbortController
js 是 single thread,所有 sync tasks 瀏覽器都會依序一個個執行
但遇到 async tasks 時就會放到 task queue,等到瀏覽器目前沒有其他工作時,再把就 queue 裡的任務拿出來執行。這也是前端必須要很熟悉的 Event loop。
Race condition reasons
race conditions 發生在多個 async tasks 同時或先後執行,無法保證哪個一會先完成,有可能是先執行的 task 先完成
也有可能先執行的 task 是後完成
In react
以 react 來說就像這個例子,當快速在兩個 butoon 上一直來回按,畫面上會覺得有 bug 產生。這是因為 react 每一個 function 都是獨立所以按四次就會 fetch 四次
const App = () => {
const [page, setPage] = useState('1');
return (
<>
<button onClick={() => setPage('1')}>Issue 1</button>
<button onClick={() => setPage('2')}>Issue 2</button>
<Page id={page} />
</>
);
};
const Page = ({ id }: { id: string }) => {
const [data, setData] = useState({})
// pass id to fetch relevant data
const url = `/some-url/${id}`;
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then((r) => {
setData(r);
});
}, [url]);
return (
<>
<h2>{data.title}</h2>
<p>{data.description}</p>
</>
)
}
Fixing race conditions
有很多種方法可以修 race condition 的問題
Force re-mounting
這個方法不算是解決 race condition,但可以解決跳來跳去問題。前面是直接 re-render update data (Component 還是同一個)。Force re-mounting 方法是 re-mount。
例如一開始 page 是 1 會 mount <Issue />
,當 page 變 2 時會 unmount <Issue />
,mount <About />
,也就解決了問題。
const App = () => {
const [page, setPage] = useState('1');
return (
<>
// skip button
{page === '1' && <Issue />}
{page === '2' && <About />}
</>
);
};
或是更簡單,加上 key
也可以,React 會認 key 去判斷是不是同一個 component,以這個狀況因為 key 不同,react 就會 unmount old key component,mount new key component。
const App = () => {
const [page, setPage] = useState('1');
return (
<>
// skip button
<Page id={page} key={page}/>
</>
);
};
不過 Force re-mounting 不算是最好的方法,因為 re-mounting 會導致效能變差等
Drop all previous results
add cleanup function
const Page = ({ id }: { id: string }) => {
const [data, setData] = useState({})
// pass id to fetch relevant data
const url = `/some-url/${id}`;
useEffect(() => {
let isActive = true;
fetch(url)
.then((r) => r.json())
.then((r) => {
if (isActive) { setData(r) }
})
return () => {
isActive = false;
};
}, [url]);
return (
<>
<h2>{data.title}</h2>
<p>{data.description}</p>
</>
)
}
加一個變數,並且在 re-render 前設為 false
(in cleanup function),例如若我按兩次 button
- 第一次 url change,useEffect
isActive = true
- 第二次 url change,執行第一次的 cleanup function, 把第一次
isActive= false
,執行第二次 useEffect - 第一次結果出來但因為 第一次
isActive= false
所以就不setDate
- 第二次結果出來,
setDate
若看不懂我在寫什麼,建議可以看之前文章有很詳細解釋 Deep dive React re-render
Cancel all previous requests
看之前要先知道什麼是 JS 原生 Class: AbortController ,我寫了一整篇文章介紹他可以當參考 ~
useEffect(() => {
// create controller here
const controller = new AbortController();
// pass controller as signal to fetch
fetch(url, { signal: controller.signal })
.then((r) => r.json())
.then((r) => {
setData(r);
});
.catch((error) => {
// error because of AbortController
if (error.name === 'AbortError') {
// do nothing
} else {
// do something, it's a real error!
}
});
return () => {
// abort the request here
controller.abort();
};
}, [url]);
這樣做的好處是,每次 re-render 都會直接中止之前的 request,不像 Drop all previous results 還是會等結果跑完只是沒 setState 而已。用 Cancel all previous requests 記得要處理錯誤,因為它中止會直接跳到 catch,並有特定為 AbortError
的 error.name。