問題
在編寫 React 程式碼時,如果你希望多次更新狀態,可能會嘗試使用 handleClickWrong
中的方式。然而,你會發現實際效果並不如預期:count
只增加了 1,而不是 3。
const root = document.getElementById('root');
const App = () => {
return (
<div>
<h1>Hello World</h1>
<Counter />
</div>
);
};
const Counter = () => {
const [count, setCount] = React.useState(0);
const handleClickWrong = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div>
<h1>{count}</h1>
<button onClick={handleClickWrong}>Increment</button>
</div>
);
};
const rootElement = ReactDOM.createRoot(root);
rootElement.render(<App />);
為什麼會出現這種情況呢?要弄清楚這個問題,我們需要了解 React 如何處理狀態更新的細節。
React 內部狀態更新機制
React 使用了一個內部機制來處理狀態更新。我們可以透過以下原始碼片段瞭解這背後的實現:
// React 原始碼中的基本狀態處理器
function basicStateReducer(state, action) {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
在我們使用 handleClickWrong
的情況下,React 會按照以下方式處理狀態更新:
// 假設初始狀態為 0,我們傳遞的值是 1
newState = basicStateReducer(0, 1);
由於 basicStateReducer
接收到的 action
不是函式,它會直接返回這個值作為最新狀態。因此,newState
會被更新為 1。
回到 handleClickWrong
的例子:
const handleClickWrong = () => {
setCount(count + 1); // = basicStateReducer(0, 1) => 1
setCount(count + 1); // = basicStateReducer(0, 1) => 1
setCount(count + 1); // = basicStateReducer(0, 1) => 1
};
由於 count
在當前 Counter
函式的作用域內,三次 setCount
呼叫的值都是 count + 1
。因此,即使呼叫了多次 setCount
,由於 React 只處理了最新的 count
值,結果每次都將狀態更新為 1。最終渲染時,count
依然只有 1。
解決方案
瞭解了原因後,我們可以改用函式形式的更新來解決這個問題。注意到 basicStateReducer
會檢查傳入的 action
,如果是函式,React 會用當前內部儲存的狀態作為引數傳給這個函式。這樣,我們可以基於最新的狀態進行更新。
const handleClick = () => {
setCount((prevCount) => prevCount + 1); // = basicStateReducer(0, (prevCount) => prevCount + 1) => 0 + 1 => 1
setCount((prevCount) => prevCount + 1); // = basicStateReducer(1, (prevCount) => prevCount + 1) => 1 + 1 => 2
setCount((prevCount) => prevCount + 1); // = basicStateReducer(2, (prevCount) => prevCount + 1) => 2 + 1 => 3
};
透過使用函式形式的更新,我們可以確保每次狀態更新都基於前一個狀態,從而實現預期的結果。