ItIron2023
react
我們前兩天將重點放在React.memo使用上的一些情境與錯誤,接著我們將繼續探討其他造成不必要重複渲染的情況,你也許會覺得有點煩,但相信我這類的情境極端的常見,一直沒有注意的話往往會讓你的頁面在不自覺的情況下越來越慢,到時候要找病徵可就麻煩多了,預防勝於治療!我們馬上來看今天的題目吧!
首先請你觀察這個codesandbox以及下方的gif。
今天我們有個大組件Parent
底下則有另外三個組件,由於SiblingUsing
&ContextAnotherComponent
兩個組件皆需要使用來自App組件的state,為了避免props drilling的情況你用了useContext建立起一個context讓Parent
底下的組件可以共享state與set function,根據diff你可以看出來這招確實有效,來自App組件的value確實順利的共享於那兩個組件,但奇怪的地方在於明明Expensive Component
並沒有用到來自App的value,這個組件很明顯不需要重新渲染!觀察以下的程式碼,請試著解釋這個情況並修復此問題。
const AppContext = createContext();
const ExpensiveComponent = () => {
const startTime = performance.now();
// Simulate expensive calculation
for (let i = 0; i < 1000000000; i++) {}
const endTime = performance.now();
console.log(`ExpensiveComponent took ${endTime - startTime} ms to render.`);
return <div>Expensive Component</div>;
};
const SiblingUsingContext = () => {
const { value, setValue } = useContext(AppContext);
return (
<div style={{ border: "1px solid red", margin: "4px" }}>
<button onClick={() => setValue((prev) => prev + 1)}>Increment</button>
<div>{`Sibling using context value: ${value}`}</div>
</div>
);
};
const AnotherComponent = () => {
const { value } = useContext(AppContext);
return (
<div
style={{ border: "1px solid green", margin: "4px" }}
>{`Another component using context value: ${value}`}</div>
);
};
const Parent = () => {
return (
<>
<SiblingUsingContext />
<AnotherComponent />
<ExpensiveComponent />
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<>
<h1>Unintended Re-renders: The Pitfalls of useContext</h1>
<AppContext.Provider value={{ value, setValue }}>
<Parent />
</AppContext.Provider>
</>
);
}
首先,若你覺得上述的情況很奇怪,那麼請先自打一巴掌,因為這是最最最基本、我們也不斷反覆說過的概念,今天App的state更新了,那麼底下的child components重新渲染是再正常也不過的問題了,這部分甚至與useContext一點關係都沒有。
釐清這點之後我們才可以正式開始我們的解題,建立一個context在你需要跨組件共享某些state時絕對是個好點子,但你要知道的是當該context更新,所有被包在context內的組件全都會重新渲染(畢竟裡面的state更新了),所以正確的選擇你context的範圍極端的重要,以這個例子來說,ExpensiveComponent
很明顯他並沒有用到任何來自context的值,那麼它就不應該被包在這個context內,你的第一步便是修改context包裹的範圍,將整個結構做簡單的改寫。
const Parent = () => {
return (
<>
<SiblingUsingContext />
<AnotherComponent />
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<div>
<AppContext.Provider value={{ value, setValue }}>
<Parent />
</AppContext.Provider>
<ExpensiveComponent />
</div>
);
}
但這樣並沒有完全解決我們的問題,畢竟context用的值都來自App裡面的state,App組件仍然會在state更新時重新渲染,最終造成一樣的結果,因此你這邊還需要做一點小小的加強,我們需要再次請出React.memo讓整個組件不會做不必要的重新渲染,稍稍修改一下ExpensiveComponent
組件就行了
const ExpensiveComponent = memo(() => { // 加上memo
const startTime = performance.now();
// Simulate expensive calculation
for (let i = 0; i < 1000000000; i++) {}
const endTime = performance.now();
console.log(`ExpensiveComponent took ${endTime - startTime} ms to render.`);
return <div>Expensive Component</div>;
});
今天這個題目稍微複雜了一點,由於我們限制了context的state是來自App組件去模擬實務上更多層的情況,因此你可能會覺得這例子似乎不夠有說服力,畢竟理論上來說這個情境有更好的解法,像是我們直接把state在Parent
組件處理即可,那麼甚至不需要用到context。
但就像我剛說的,實務上往往會有更多層的結構,很多時候需要的state就是來自一個較遠的地方,在這類的情況下要跨組件共享就需要特別的小心,必要時你甚至需要重構整個結構讓你有辦法切出乾淨的context,希望今天的例子能讓你之後再使用useContext時多思考一下,那麽我們明天見囉!
本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!