React 在 component function 中提供了一個 useEffect
hook 來 隔絕和管理副作用 。React 在每次 render 之後執行 useEffect
。
副作用指的是當函式被呼叫時,除了回傳值以外,還會對外部環境產生影響的操作。常見的副作用包括:
若直接在 component function 中處理副作用,會造成以下問題:
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>;
}
useEffect
hook。useEffect
hook 中定義副作用函式,例如呼叫 API。// 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'))
初始渲染, 執行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。
宏任務執行:setTimeout
的 callback 會在 100 毫秒後執行,這時 callback 內的 console.log(state)
會依然使用最初 setTimeout
被註冊時的閉包環境中的 state 值,該值仍然是初次渲染時的 0,因此會印出 0。
0
0
1
0
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)
console.log(1)
,印出 1
。useEffect
hook 中的 console.log(2)
,印出 2
。Promise.resolve().then(() => console.log(3))
放到微任務隊列,等待同步程式執行完畢後執行。setTimeout(() => console.log(4), 0)
放到宏任務隊列,等待同步程式執行完畢後執行。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