React 中的副作用處理:effect 初探
React 在 component function 中提供了一個 useEffect
hook 來 隔絕和管理副作用 。React 在每次 render 之後執行 useEffect
。
副作用 side effect
副作用指的是當函式被呼叫時,除了回傳值以外,還會對外部環境產生影響的操作。常見的副作用包括:
- 呼叫 API
- 操作 DOM(例如手動新增事件監聽器)
- 設定計時器(例如 setTimeout、setInterval)
- 訂閱事件(例如 WebSocket 或其他事件系統)
- 修改全域變數
使用 useEffect hook 處理副作用的原因
若直接在 component function 中處理副作用,會造成以下問題:
- 重複副作用執行:由於函式多次執行而產生疊加副作用會造成 react 無法預測行為,可能導致資料流或程式邏輯無法正常運作。
- 效能問題: 容易阻塞函式計算,導致產生 react element 的速度變慢,進而造成畫面更新的延遲。
- 無法清理副作用:直接操作 DOM 或設置計時器時,無法在 component 卸載時自動清理這些副作用,可能導致記憶體洩漏。
以 useEffect hook 處理副作用的步驟
1function MyComponent() {2 const [id, setId] = useState(1);34 useEffect(() => {5 // 定義副作用,例如呼叫 API6 console.log("Component rendered");7 axios.get(`https://api.example.com/data${id}`).then((response) => {8 console.log(response.data);9 });10 // 可選的 cleanup 函式,會在下一次 effect 執行前或 component 卸載時執行11 return () => {12 console.log("Cleanup before the next effect or on unmount");13 };14 }, [id]); // 依賴陣列,當陣列中的值有變動時才會執行副作用1516 return <div>My Component</div>;17}
- 在 component function 中使用
useEffect
hook。 - 在
useEffect
hook 中定義副作用函式,例如呼叫 API。 - 如果有需要清除副作用,可以在副作用函式中回傳一個 cleanup 函式。
- 在 dependencies array 中傳入依賴陣列,以跳過執行不需要的 render 副作用。
實例練習 1
1// This is a React Quiz from BFE.dev23function App() {4 const [state, setState] = useState(0)5 console.log(state)67 useEffect(() => {8 setState(state => state + 1)9 }, [])1011 useEffect(() => {12 console.log(state)13 setTimeout(() => {14 console.log(state)15 }, 100)16 }, [])1718 return null19}2021ReactDOM.render(<App/>, document.getElementById('root'))
解題 1
-
初始渲染, 執行
console.log(state)
,這時的 state 是 0,所以會印出 0。 -
第一個 useEffect hook,
setState(state => state + 1)
會等到所有的 side effect 都執行完後才會執行。 -
第二個 useEffect hook 中的
console.log(state)
會印出 0,這時的 state 是 0,所以會印出 0。 -
當遇到第二個 useEffect hook 中的
setTimeout
會在 100ms 後執行,所以根據 event loop 的機制會被放到宏任務 queue 中,等到 call stack 空了之後才會執行。 -
setState
觸發 re-render:當setState(state => state + 1)
被執行後,React 會進行一次重新渲染,state 的值會從 0 更新為 1,觸發 component re-render。 -
重新渲染後:重新渲染的
console.log(state)
:在重新渲染過程中,state 現在是 1,所以新的console.log(state)
會印出 1。7. 宏任務執行:setTimeout
的 callback 會在 100 毫秒後執行,這時 callback 內的console.log(state)
會依然使用最初setTimeout
被註冊時的閉包環境中的 state 值,該值仍然是初次渲染時的 0,因此會印出 0。
0
0
1
0
實例練習 2
1function App() {2 const [state, setState] = useState(0)3 console.log(1)45 useEffect(() => {6 console.log(2)7 }, [state])89 Promise.resolve().then(() => console.log(3))1011 setTimeout(() => console.log(4), 0)1213 const onClick = () => {14 console.log(5)15 setState(num => num + 1)16 console.log(6)17 }18 return <div>19 <button onClick={onClick}>click me</button>20 </div>21}2223const root = createRoot(document.getElementById('root'));24root.render(<App/>)2526setTimeout(() => fireEvent.click(screen.getByText('click me')), 100)
解題 2
- 初始渲染,執行
console.log(1)
,印出1
。 - 執行
useEffect
hook 中的console.log(2)
,印出2
。 - 將
Promise.resolve().then(() => console.log(3))
放到微任務隊列,等待同步程式執行完畢後執行。 - 將
setTimeout(() => console.log(4), 0)
放到宏任務隊列,等待同步程式執行完畢後執行。 - 事件處理 (onClick):
- React 在 100 毫秒後模擬點擊按鈕。
- 將
onClick
callback 放到事件隊列,放到宏任務隊列,等待同步程式執行完畢後執行。
- 從微任務隊列中取出
Promise.resolve().then(() => console.log(3))
,執行console.log(3)
,印出3
。 - 從宏任務隊列中取出
setTimeout(() => console.log(4), 0)
,執行console.log(4)
,印出4
。 - 從宏任務隊列中取出
onClick
callback,執行console.log(5)
,印出5
,setState(num => num + 1)
會等到所有的 side effect 都執行完後才會執行。 console.log(6)
,印出6
。- 執行
setState(num => num + 1)
,觸發 re-render,重新執行 component function,state 從 0 變成 1。 - 重新渲染後,執行
console.log(1)
,印出1
。 - 執行
useEffect
hook 中的console.log(2)
,印出2
。 - 將
Promise.resolve().then(() => console.log(3))
放到微任務隊列,等待同步程式執行完畢後執行。 - 將
setTimeout(() => console.log(4), 0)
放到宏任務隊列,等待同步程式執行完畢後執行。 - 從微任務隊列中取出
Promise.resolve().then(() => console.log(3))
,執行console.log(3)
,印出3
。 - 從宏任務隊列中取出
setTimeout(() => console.log(4), 0)
,執行console.log(4)
,印出4
。
1
2
3
4
5
6
1
2
3
4