HOWTO · React
使用 React Hooks 強制重新渲染
React Hooks 的引入改變了我們編寫和構建 React 元件的方式。在本文中,我們將探討如何使用 React Hooks 強制重新渲染。
本頁內容
React 函式元件沒有 forceUpdate() 方法,但這個問題背後的需求是真實的:有東西改變了,畫面就應該呈現出來。Hooks 的答案是,你永遠不會直接推動一次渲染——你更新狀態,由 React 安排渲染。實務上,當顯示的資料改變時使用 useState;當你確實需要一個背後沒有真實資料的明確觸發器時,則使用 useReducer 計數器。
本文說明什麼會導致渲染、展示可運作的 useState 與 useReducer 模式,並釐清為什麼 useEffect 和僅限類別元件的 forceUpdate() 都不是函式元件的答案。
什麼會導致 React 元件渲染
自 React 16.8 起,Hooks 讓函式元件能持有狀態並回應更新。當下列任一輸入改變時,函式元件就會重新渲染:
useState的 setter 或useReducer的 dispatch 提供了新的狀態值——React 會以Object.is將它與先前的狀態比較,不同時便重新渲染。- 父元件重新渲染,元件函式便以新的(或相同的)props 再次執行。
- 元件所讀取的 context 值改變了。
對於「強制重新渲染」這個問題,有兩個細節很關鍵。第一,渲染不等於更動 DOM——React 先渲染,再只把差異提交到頁面上。第二,由於 React 在新狀態相同時可以放棄這次渲染,把目前的值再傳回 setter 並不是取得另一次渲染的可靠方法;觸發器必須產生新的值。
渲染的資料改變時使用 state
當畫面上的資訊改變時,就在狀態中表達它。useState 是最直接的工具:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function RerenderExample() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Current count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Refresh count
</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root'))
.render(<RerenderExample />);
</script>
</body>
</html>
本文中的每個 React 範例都是一個完整的瀏覽器頁面:將它儲存為 .html 檔案並開啟,CDN 腳本就會載入 React 18 與 Babel,讓 text/babel 內的 JSX 依原樣執行。這些頁面是以靜態瀏覽器原始碼呈現的——本文的執行環境並未執行它們,因此不宣稱有任何主控台輸出。首次渲染時,段落會顯示 Current count: 0(此標籤在範例中維持英文);每次點擊都會安排一次計數加一的渲染。updater 寫法 c => c + 1 建立在最新狀態之上,而非事件處理常式建立時所捕捉的值,這能避免快速連點時遺失累加。
這涵蓋了幾乎所有真實的「強制重新渲染」情境:UI 之所以陳舊,只是因為狀態尚未更新。
需要明確的渲染觸發器時使用 reducer
有時顯示的資料並未改變,但你仍想讓 React 再執行一次元件——這是 Hooks 中最接近強制更新的做法,也是 React Hooks FAQ 推廣開來的模式。useReducer 提供了這個能力:每次 dispatch 都回傳一個新的狀態值,因此變更檢查總是通過,渲染隨之發生。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'reset':
return initialState;
default:
return state;
}
}
function RerenderWithReducer() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>
Rerender
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root'))
.render(<RerenderWithReducer />);
</script>
</body>
</html>
每次 dispatch increment 都會回傳一個新的 { count: ... } 物件,正是這個新值讓 React 再次渲染。在 default 分支回傳目前的 state,可在 dispatch 未知的 action 時讓 UI 維持不變,而不是拋出錯誤。
reducer 是純 JavaScript,因此它的狀態轉移可以直接檢驗。執行這個範例:
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
}
let state = { count: 0 };
state = reducer(state, { type: 'increment' });
state = reducer(state, { type: 'increment' });
console.log(`Count: ${state.count}`);
Count: 2
當更新是多步驟的、依賴先前的狀態,或你需要的正是明確的計數器觸發器時,就選用 useReducer。對於單一改變的值,useState 仍然更簡單。
為什麼不該用 effect 來強制渲染
useEffect 讓元件與 React 之外的事物保持同步——擷取的資源、訂閱、計時器,或手動管理的 DOM——而且它在渲染提交之後才執行。它不是渲染觸發器。一個唯一工作只是呼叫 state setter 以換取再一次渲染的 effect,例如 useEffect(() => { setTick(t => t + 1); }),等於多付一次渲染與提交週期卻沒有產生任何新東西;如果它的依賴持續改變,還會無限循環。
這個模式的正當版本,是因為外部資料確實送達才設定狀態。這裡以 subscribeProfile 代表對外部系統的擷取或訂閱,讓頁面保持自足:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function subscribeProfile(userId, onData) {
const timer = setTimeout(
() => onData({ name: 'Ada Lovelace' }), 500);
return () => clearTimeout(timer);
}
function ProfileCard({ userId }) {
const [profile, setProfile] = React.useState(null);
React.useEffect(() => {
return subscribeProfile(userId, setProfile);
}, [userId]);
return <p>{profile ? profile.name : 'Loading…'}</p>;
}
ReactDOM.createRoot(document.getElementById('root'))
.render(<ProfileCard userId={1} />);
</script>
</body>
</html>
這裡渲染之所以發生,是因為 setProfile 送達了新資料,而不是因為 effect 要求渲染。回傳取消訂閱的函式,可防止遲來的送達在元件卸載或 effect 重新執行之後仍寫入狀態——這與在 fetch 周圍設置 cancelled 旗標的作用相同。當某個值可以從現有的 props 或狀態計算出來時,就在渲染主體中計算,並完全略過 effect——在渲染期間推導永遠比渲染兩次便宜。
forceUpdate() 何時適用於類別元件
forceUpdate() 只存在於類別元件上。呼叫 this.forceUpdate() 會重新執行 render(),同時略過 shouldComponentUpdate:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
class Refreshable extends React.Component {
render() {
return <p>Rendered at {Date.now()}</p>;
}
}
const refreshableRef = React.createRef();
function App() {
return (
<div>
<Refreshable ref={refreshableRef} />
<button onClick={
() => refreshableRef.current.forceUpdate()
}>
Force update
</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root'))
.render(<App />);
</script>
</body>
</html>
這裡按鈕透過 ref 在類別實例上呼叫 forceUpdate(),因此每次點擊都會重新執行 render(),並在下一次繪製時顯示新的時間戳。這個 API 刻意被定位為最後手段:它繞過 React 正常的資料流,因此 React 文件把它保留給不尋常的情況,例如輸出依賴存在於 React 之外的可變資料。當變更可以表達為狀態時,setState 是類別元件中更好的工具——而函式元件根本沒有 forceUpdate(),這就是為什麼上面的 useReducer 計數器是把舊的強制更新程式碼移植到 Hooks 時的慣用做法。
選擇正確的更新模式
| 情境 | 選用 |
|---|---|
| 顯示的資料改變了 | useState——設定新值 |
| 多步驟轉移,或真正需要明確觸發器 | useReducer——每次 dispatch 都回傳新狀態 |
| 與外部系統同步 | useEffect——渲染跟隨它設定的狀態 |
| 新值等於目前狀態 | 沒有可靠做法——React 可能放棄渲染 |
| 類別元件 | setState;forceUpdate() 僅用於外部可變資料 |
簡言之:在函式元件中,你不是推動渲染,而是更新資料,讓 React 從變更中拉出渲染。
常見問題
-
我可以在一個元件中使用多個 Hooks 嗎?
可以。在頂層無條件地呼叫 Hooks,並在每次渲染中保持相同順序,React 就能正確對應各 Hooks 的狀態槽。 -
什麼時候
useReducer比useState更適合?
當下一個狀態以多種方式依賴前一個狀態、當數個相關的值需要一起改變,或當你想要明確的 dispatch 觸發器時,優先選用useReducer。對於各自獨立的簡單值,useState就足夠了。 -
useEffect到底是用來做什麼的?
與外部系統同步——擷取資料、訂閱、計時器、手動 DOM 操作——以及事後的清理。它不應該被用來要求額外的渲染。 -
我可以在不改變狀態的情況下強制重新渲染嗎?
沒有受支援的 Hooks API 可以做到這點,而且把狀態設定為目前的值會讓 React 略過渲染。useReducer計數器之所以有效,正是因為每次 dispatch 都交給 React 一個新值。 -
Hooks 有
forceUpdate()嗎?
沒有——它是類別元件的方法。useReducer計數器模式是公認的函式元件替代方案,適用於真正需要它的少數情況。