iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Modern Web

30天React練功坊-攻克常見實務/面試問題系列 第 15

30天React練功坊-攻克常見實務/面試問題 Day15: Unintended Re-renders: The Pitfalls of useContext

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

我們前兩天將重點放在React.memo使用上的一些情境與錯誤,接著我們將繼續探討其他造成不必要重複渲染的情況,你也許會覺得有點煩,但相信我這類的情境極端的常見,一直沒有注意的話往往會讓你的頁面在不自覺的情況下越來越慢,到時候要找病徵可就麻煩多了,預防勝於治療!我們馬上來看今天的題目吧!

本日題目

首先請你觀察這個codesandbox以及下方的gif。

day15-demo-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時多思考一下,那麽我們明天見囉!

本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!


上一篇
30天React練功坊-攻克常見實務/面試問題 Day14: Optimization with React.memo the wrong way
下一篇
30天React練功坊-攻克常見實務/面試問題 Day16: React.memo not working corretly with function as props
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言