React
前端筆記
Udemy 課程筆記
(更新完 state 後才會觸發 re-render 使 UI 依照 state 更新,且每一次的 render 都有它自己版本的 state 值,同一次 render 中的 state 值是固定且永遠不變的。)
在 React 中要更新 UI 的就必須透過呼叫 setStateFunc
,讓 React 更新 state 後 re-render component,但如果開發者在同一個 handler(時機點)叫用多次 setStateFunc
,React 就會因為叫用幾次 setStateFunc
而重新 re-render component 幾次嗎?
import { useState } from 'react';
export default function Counter() {
console.log('re-render counter');
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(1);
// state 會立即更新並 re-render 這個 component 嗎?
// 這樣子看起來就會 re-render 至少 3 次
setNumber(2);
setNumber(3);
}}
>
+3
</button>
</>
);
}
結果是不會的,點擊按鈕後 <Counter />
只會被 re-render 一次而已,也就是說在同一個 handler 內 React 並不會馬上更新 state。
React waits until all code in the event handlers has run before processing your state updates.
React 會等到 handler 內的程式碼執行完畢後才會更新 state。
import { useState } from 'react';
export default function Counter() {
console.log('re-render counter');
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(1);
// 並不會馬上更新 state
setNumber(2);
// 並不會馬上更新 state
setNumber(3);
// 並不會馬上更新 state
}}
>
+3
</button>
</>
);
}
React 會保證執行 setStateFunc
的順序(依照開發者呼叫的順序),將 setStateFunc
存放在 queue(就跟 event queue 的概念差不多,但是這個 queue 中是存放 setStateFunc
),然後合併 queue 內的 setStateFunc
,並執行一次 re-render,減少不必要的 re-render,因為 re-render = 函式叫用 = 函式內的計算(其 children component 也要全部 re-render)。
React does not batch across multiple intentional events like clicks—each click is handled separately. Rest assured that React only does batching when it’s generally safe to do. This ensures that, for example, if the first button click disables a form, the second click would not submit it again.
但 React 不會橫跨 handler,將其 handler 的setStateFunc
合併,比方來說第一次點擊按鈕後 form 被 disabled,不可能第二次點擊就發送 form(因為已經被 disabled了)
所以套上 queue 的概念,上方程式碼其實應該是這樣子執行的:
import { useState } from 'react';
export default function Counter() {
console.log('re-render counter');
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(1);
// 並不會馬上更新 state,因為這個 handler 內還有其他程式碼需要執行,所以這個 setStateFunc 就會被推入 queue。
setNumber(2);
// 並不會馬上更新 state,因為這個 handler 內還有其他程式碼需要執行,所以這個 setStateFunc 就會被推入 queue。
setNumber(3);
// 將這個 setStateFunc 推入 queue 中,因為這個 handler 內沒有其他程式碼需要執行了,React 準備著手 state 更新
// STEP1: 依序執行 queue 內的 setStateFucn
// - setNumber(1), setNumber(2), setNumber(3)
// STEP2: state 更新完畢
// STEP3: componenet re-render,達到 UI 更新的目標
// - number 為 3
}}
>
+3
</button>
</>
);
}
像這樣子等待 handler 內無其他程式碼後且為了效能減少不必要的 re-render,React 官方稱做「batch update」。
import { useState } from 'react';
export default function Counter() {
console.log('re-render counter');
const [number, setNumber] = useState(0);
const [username, setUsername] = useState('');
const clickHandler = () => {
setNumber(1);
// 因為 handler 內還有其他程式碼需要執行,因此會先把 setStateFunc 推入 queue
setNumber(2);
// 因為 handler 內還有其他程式碼需要執行,因此會先把 setStateFunc 推入 queue
setNumber(3);
// 因為 handler 內還有其他程式碼需要執行,因此會先把 setStateFunc 推入 queue
setUsername('Lun');
// handler 內已無其他需要執行的程式碼,因此把該 setStateFunc 推入 queue 後就開始依照 queue 著手更新 state
// 所以最後 number 會被更新為 3,username 為 'Lun',且多虧 React queue,畫面只會 re-render 一次
};
return (
<>
<h1>{number}</h1>
<h1>{username}</h1>
<button onClick={clickHandler}>+3</button>
</>
);
}
effect function 在元件第一次初始載入後就一定會叫用一次,之後就看 dependency array 的條件叫用。
每一次 render 都會有自己的 states, functions 及 effect functions。
如果在不同 effect function 中叫用 setStateFunc,在同一回叫用時也會 batch update:
export default function App() {
const [stateOne, setStateOne] = useState();
const [stateTwo, setStateTwo] = useState();
const renderCounter = useRef(0);
renderCounter.current++;
useEffect(() => {
console.log("effect two");
setStateTwo(2);
}, []);
useEffect(() => {
console.log("effect one");
setStateOne(1);
}, []);
return (
<div className="App">
<p>stateOne: {stateOne}</p>
<p>stateTwo: {stateTwo}</p>
<p>renderCounter: {renderCounter.current}</p>
</div>
);
}
(會發現元件載入兩次)
這裡可以看到另一個範例-同一回 effect 也是會 batch update
元件第一次載入:
export default function App() {
const stateOne = undefined
const stateTwo = undefined
const renderCounter = 1 // 因為有 renderCounter.current ++
// 第一次載入完會叫用 effect functions
useEffect(() => {
console.log("effect two");
setStateTwo(2);
}, []);
useEffect(() => {
console.log("effect one");
setStateOne(1);
}, []);
/* 可以想像成這樣:
* function outerUseEffect () => {
* useEffect()
* useEffect()
* }
*
* outerUseEffect()
* */
}
元件第一次載入後預設叫用 effect functions:
export default function App() {
// effect functions 更新 state
const stateOne = 1
const stateTwo = 2
// 這邊只 + 1 而不是 + 2
// 因為兩個 effect functions 叫用的時機點是元件第一次載入後叫用,所以 React 為了節省效能,而自動排程 batch update
const renderCounter = 2
useEffect(() => {
console.log("effect two");
setStateTwo(2);
}, []);
useEffect(() => {
console.log("effect one");
setStateOne(1);
}, []);
}
所以可以發現 effect function 在叫用 setStateFunc 時也會維持同一次 render 是自己的 scope 及 batch update state 的規則。
React 18+ 後才有不管怎麼樣都會 batch update 的功能(即便非同步執行):
// 在 18+ 非同步還是會執行 batch update
const handleAsyncIncrese = () => {
Promise.resolve().then(() => {
setStateOne((prev) => prev + 1);
setStateOne((prev) => prev + 1);
setStateOne((prev) => prev + 1);
});
};
但是在 18+ 以前並不支援,所以在非同步內使用 setStateFunction 會立即觸發 React 比對當前歷史 state(如果有不同就會 render,而不會先堆疊上一次 render 時非同步程式碼內的 setStateFunction):
// 所以這端 Promise 內更新 state,導致元件渲染三次
// ...
console.log('[outer]')
const handleAsyncIncrease = () => {
Promise.resolve().then(() => {
setStateOne((prev) => prev + 1); // 馬上比較上一次 render 的 state,發現有不同就 render
console.log('[end first]')
setStateOne((prev) => prev + 1); // 馬上比較上一次 render 的 state,發現有不同就 render
setStateOne((prev) => prev + 1); // 馬上比較上一次 render 的 state,發現有不同就 render
});
};
effect batching update with react18+
effect batching update with react less 18+
setStateFunc
除了可以直接丟新的值當作 argument 之外,還可以丟一個完整的函式。被丟入的函式有一個 parameter 可以使用,React 會保證 paramter 為該 state 上一次的值,因此如果更新的值有需要依靠上一次的值,直接用 updater function 是最好的更新手段。(比方來說根據上一次的 state + 1 為新的 state setStateFunc((prevCounter) => prevCounter + 1)
)
直接給值就是把 state 替換成另一個 state:
其實替換 state 就等同於使用 updater function 但回傳的結果並不依賴於 argument:
setStateFunc((prevState) => newState)
使用 updater function 就是在告訴 React:「新的 state 是依賴上次 state 運算後的結果,而非用某個新的值取代舊的 state」。updater function 一樣會被塞入 queue,並等待該區塊的 handler 結束再按照順序執行,唯一不同的是,這次是整個 function 都會被塞入 queue,而且執行第一個 updater function 時,React 會直接把起始 state 帶入第一個 updater function 的 parameter 叫用,接下來便會以上一個 updater function return 的結果當作下一個 updater function parameter 叫用,使每一個 updater function 都可以正確地得到上一次的 state,並依賴上一次 state 計算新的 state。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
const clickHandler = () => {
setNumber(number => number + 1);
// handler 內還有程式碼需要執行,因此 updater function 被塞入 queue
setNumber(number => number + 1);
// handler 內還有程式碼需要執行,因此 updater function 被塞入 queue
setNumber(number => number + 1);
// 此 updater function 被塞入 queue 中,因為 handler 沒有其他的程式碼需要執行了,所以開始依照 queue 內的順序執行更新 state 的流程
// STEP1: (number) => number + 1,為第一個執行的任務,因為是 updater function,所以起始 state 0 會被帶入 paramter,當作任務函式的 argument 叫用函式,並得到 0 + 1 = 1;
// STEP2: (number) => number + 1,以上一次 queueItem 的結果當作 argument 叫用此次任務的 updater function,並得到 1 + 1 = 2;
// STEP3: (number) => number + 1,以上一次 queueItem 的結果當作 argument 叫用此次任務的 updater function,並得到 2 + 1 = 3;
// STEP4: queue 內的任務都執行完畢,最終 number 為 3,並統一只 re-render 1 次,UI 呈現 3
};
return (
<>
<h1>{number}</h1>
<button onClick={clickHandler}>+3</button>
</>
);
}
Updater functions run during rendering, so updater functions must be pure and only return the result. Don’t try to set state from inside of them or run other side effects.
切記,updater function 必須為 pure function(因此只需要處理回傳的結果,別在裡面又 set new state 或者做一些有 side effects 的東西)
import { useState } from 'react';
export default function Counter() {
console.log('re-render counter');
const [number, setNumber] = useState(0);
const clickHandler = () => {
setNumber(number => number + 1);
// updater function 被推入 queue
setNumber(number => number + 1);
// updater function 被推入 queue
setNumber(100);
// 把 state 替換成 100 被推入 queue
setNumber(number => number + 1);
// updater function 被推入 queue,且因 handler 無其他需要處理的程式碼了,因此會依照 queue 的順序執行 setStateFunc
// STEP1: (number) => number + 1,起始 state 被帶入為 argument 叫用函式,得到 0 + 1 = 1;
// STEP2: (number) => number + 1,上一次 updater function 的結果被帶入為 argument 叫用函式,得到 1 + 1 = 2;
// STEP3: state 被替換成 100
// STEP4: (number) => number + 1,上一次替換的結果 100 被帶入為 argument 叫用函式,得到 100 + 1 = 101;
// STEP5: state 更新完畢,並統一 re-render 一次更新 UI
};
return (
<>
<h1>{number}</h1>
<button onClick={clickHandler}>increase</button>
</>
);
}
以官方的練習為例子,因為 batch update 必須要等到 handler 內的程式碼執行完畢才會開始更新 state,所以如果這時候塞入非同步且新 state 也仰賴於舊 state 為計算基礎的話,不使用 updater function 更新 state 就會發生非同步程式碼抓到錯誤 state 的問題:
目標:點擊按鈕後 pending 會按照點擊的次數新增,且三秒後 pending 會遞減,completed 則是遞增。
// fork from (Queueing a Series of State Updates)[https://beta.reactjs.org/learn/queueing-a-series-of-state-updates]
import { useState } from 'react';
export default function RequestTracker() {
const [pending, setPending] = useState(0);
const [completed, setCompleted] = useState(0);
async function handleClick() {
setPending(pending + 1);
await delay(3000);
setPending(pending - 1);
setCompleted(completed + 1);
}
return (
<>
<h3>
Pending: {pending}
</h3>
<h3>
Completed: {completed}
</h3>
<button onClick={handleClick}>
Buy
</button>
</>
);
}
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
以當前的程式碼點擊按鈕後發現以下問題:
因為 scope、React batch update 以及沒使用 updater function 更新值:
setStateFunc
會依序被推入 queue,React 會等到 handler 內的程式碼執行完畢才會更新 state 並渲染,不過,如果 handler 內有非同步執行的程式碼,那麼 React queue 就會開始著手更新 state,非同步執行的程式碼會待 call stack 清空,event loop 就會把非同步執行任務推入 call stack所以實際執行的樣子應該是:
setPending(pending + 1)
推入 queue 內asyc / await
的協助,在該 handler 內會確保 await
的任務結束才會繼續往下執行setStateFunc
,並依照順序執行更新 state 的行為setPending(pending + 1)
將 pending
替換成 pending + 1
async / await
之後的程式碼setPending(pending - 1)
推入 queuesetCompleted(completed + 1)
推入 queuesetStateFuncs
setPending(pending - 1)
,因為這個是setPending(pending + 1)
re-render 以前的 state,所以setPending(pending - 1)
中會抓到過去的pending
,並將pending
取代成pending - 1
(也就是 0 - 1)
setCompleted(completed + 1)
setCompleted(completed + 1)
,因為這個是setPending(pending + 1)
re-render 以前的 state,所以setCompleted(completed + 1)
中會抓到過去的completed
,並將completed
取代成completed + 1
(也就是 0 + 1)
之後點擊按鈕後就會重複執行上方的流程。
可以發現 await
之後的程式碼都會抓到上方 setPending
上一次未更新的 state,進而導致 await
之後的 setStateFunc
都更新錯誤的資訊。
這時候就會發現 updater function 的好處了:
// ...
async function handleClick() {
// 以 updater function 改變單純地 replace
setPending((pending) => pending + 1);
await delay(3000);
// 以 updater function 改變單純地 replace
setPending((pending) => pending - 1);
// 以 updater function 改變單純地 replace
setCompleted((completed) => completed + 1);
}
// ...
即便 await
後面的 setStateFuncs
還是拿到上一次 setPending
更新前的值(比如說點擊按鈕,UI 顯示 0 -> 1,但是 await
後面還是只拿到 0),但是因為使用 updater function,react 會把上一次正確的 state 塞入 argument 叫用 updater function,所以就可以達成目標!
setPending(pending + 1)
推入 queue 內asyc / await
的協助,在該 handler 內會確保 await
的任務結束才會繼續往下執行setStateFunc
,並依照順序執行更新 state 的行為setPending((pending) => pending + 1)
,因為是 updater function,所以 react 會把上一次正確的 pending
帶入 updater function 之內,並執行 updater function,其結果為新的 pending
async / await
之後的程式碼setPending((pending) => pending - 1)
推入 queuesetCompleted((completed) => completed + 1)
推入 queuesetStateFuncs
setPending((pending) => pending - 1)
,react 會將上一次正確的pending
塞入 updater function 更新 state,並更新pending
(上一次pending
- 1)
setCompleted((completed) => completed + 1)
setCompleted((completed) => completed + 1)
,react 會將上一次正確的completed
塞入 updater function 更新 state,並更新completed
(上一次completed
+ 1)
因為是 updater function,所以可以安心地取得正確地上一次 state。
import { Dispatch, SetStateAction } from "react";
type Props = {
onCounterChange: Dispatch<SetStateAction<number>>;
};
const Test = ({ onCounterChange }: Props) => {
/** 透過傳遞 updater function 直接拿取 state setter,這樣子就不用額外傳 count 當作 props 讓 Test 可以讀取 */
const hanldeIncrease = () => {
onCounterChange((prev) => prev + 1);
};
const handleDecrease = () => {
onCounterChange((prev) => prev - 1);
};
const handleReset = () => {
onCounterChange(0);
};
return (
<div>
<button onClick={hanldeIncrease}>Incrase in Test</button>
<button onClick={handleDecrease}>Decrese in Test</button>
<button onClick={handleReset}>Reset in Test</button>
</div>
);
};
export default Test;
[Note] 子層 component 也需要依賴原資料更新 state
// ref.(Queueing a Series of State Updates)[https://beta.reactjs.org/learn/queueing-a-series-of-state-updates]
export function getFinalState(baseState, queue) {
let finalState = baseState;
for (const q of queue) {
if (typeof q === 'function') {
finalState = q(finalState);
} else {
finalState = q;
}
}
return finalState;
}
setStateFunc
不會直接更改當前存在的 state,而是會觸發更新 state,以便下次 render 更新 UIsetStateFunc
的順序,setStateFunc
會先被推入 queue,handler 內沒有其他需要執行的程式碼時,React 會依照其順序更新 state,並統一 re-render,避免多次的 re-render 所導致的效能問題(但不會橫跨不同的 handler,亦即第一次點擊跟第二次點擊不會合併成單次 re-render)setStateFunc
,React 也會堆疊 batch update
ReactDOM.unstable_batchedUpdates
React 才會 batch update