跳至主要内容

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 處理副作用的步驟

import React, { useEffect } from "react";
import axios from "axios";
function MyComponent() {
const [id, setId] = useState(1);

useEffect(() => {
// 定義副作用,例如呼叫 API
console.log("Component rendered");
axios.get(`https://api.example.com/data${id}`).then((response) => {
console.log(response.data);
});
// 可選的 cleanup 函式,會在下一次 effect 執行前或 component 卸載時執行
return () => {
console.log("Cleanup before the next effect or on unmount");
};
}, [id]); // 依賴陣列,當陣列中的值有變動時才會執行副作用

return <div>My Component</div>;
}
  1. 在 component function 中使用 useEffect hook。
  2. useEffect hook 中定義副作用函式,例如呼叫 API。
  3. 如果有需要清除副作用,可以在副作用函式中回傳一個 cleanup 函式。
  4. 在 dependencies array 中傳入依賴陣列,以跳過執行不需要的 render 副作用。

實例練習 1

12. useEffect

// This is a React Quiz from BFE.dev

import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'

function App() {
const [state, setState] = useState(0)
console.log(state)

useEffect(() => {
setState(state => state + 1)
}, [])

useEffect(() => {
console.log(state)
setTimeout(() => {
console.log(state)
}, 100)
}, [])

return null
}

ReactDOM.render(<App/>, document.getElementById('root'))

解題 1

  1. 初始渲染, 執行console.log(state),這時的 state 是 0,所以會印出 0。

  2. 第一個 useEffect hook,setState(state => state + 1) 會等到所有的 side effect 都執行完後才會執行。

  3. 第二個 useEffect hook 中的 console.log(state) 會印出 0,這時的 state 是 0,所以會印出 0。

  4. 當遇到第二個 useEffect hook 中的 setTimeout 會在 100ms 後執行,所以根據 event loop 的機制會被放到宏任務 queue 中,等到 call stack 空了之後才會執行。

  5. setState 觸發 re-render:當 setState(state => state + 1) 被執行後,React 會進行一次重新渲染,state 的值會從 0 更新為 1,觸發 component re-render。

  6. 重新渲染後:重新渲染的 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

24. useEffect() timing

import * as React from "react";
import { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { screen, fireEvent } from "@testing-library/dom";

function App() {
const [state, setState] = useState(0)
console.log(1)

useEffect(() => {
console.log(2)
}, [state])

Promise.resolve().then(() => console.log(3))

setTimeout(() => console.log(4), 0)

const onClick = () => {
console.log(5)
setState(num => num + 1)
console.log(6)
}
return <div>
<button onClick={onClick}>click me</button>
</div>
}

const root = createRoot(document.getElementById('root'));
root.render(<App/>)

setTimeout(() => fireEvent.click(screen.getByText('click me')), 100)

解題 2

  1. 初始渲染,執行 console.log(1),印出 1
  2. 執行 useEffect hook 中的 console.log(2),印出 2
  3. Promise.resolve().then(() => console.log(3)) 放到微任務隊列,等待同步程式執行完畢後執行。
  4. setTimeout(() => console.log(4), 0) 放到宏任務隊列,等待同步程式執行完畢後執行。
  5. 事件處理 (onClick):
  • React 在 100 毫秒後模擬點擊按鈕。
  • onClick callback 放到事件隊列,放到宏任務隊列,等待同步程式執行完畢後執行。
  1. 從微任務隊列中取出 Promise.resolve().then(() => console.log(3)),執行 console.log(3),印出 3
  2. 從宏任務隊列中取出 setTimeout(() => console.log(4), 0),執行 console.log(4),印出 4
  3. 從宏任務隊列中取出 onClick callback,執行 console.log(5),印出 5setState(num => num + 1) 會等到所有的 side effect 都執行完後才會執行。
  4. console.log(6),印出 6
  5. 執行 setState(num => num + 1),觸發 re-render,重新執行 component function,state 從 0 變成 1。
  6. 重新渲染後,執行 console.log(1),印出 1
  7. 執行 useEffect hook 中的 console.log(2),印出 2
  8. Promise.resolve().then(() => console.log(3)) 放到微任務隊列,等待同步程式執行完畢後執行。
  9. setTimeout(() => console.log(4), 0) 放到宏任務隊列,等待同步程式執行完畢後執行。
  10. 從微任務隊列中取出 Promise.resolve().then(() => console.log(3)),執行 console.log(3),印出 3
  11. 從宏任務隊列中取出 setTimeout(() => console.log(4), 0),執行 console.log(4),印出 4
1
2
3
4
5
6
1
2
3
4