memo 讓你可以在元件的 props 沒有變更時跳過重新渲染。

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

參考

memo(Component, arePropsEqual?)

將元件包在 memo 中可以獲得該元件的 *memoized* 版本。 只要 props 沒有改變,這個 memoized 版本的元件通常不會在其父元件重新渲染時重新渲染。 但 React 仍然可能重新渲染它:記憶化是一種效能優化,而不是保證。

import { memo } from 'react';

const SomeComponent = memo(function SomeComponent(props) {
// ...
});

請參閱下面的更多範例。

參數

  • Component: 你想要記憶化的元件。 memo 不會修改這個元件,而是傳回一個新的、memoized 的元件。 接受任何有效的 React 元件,包括函式和 forwardRef 元件。

  • **選用** arePropsEqual: 一個接受兩個參數的函式:元件先前的 props 和新的 props。 如果舊的和新的 props 相等,它應該傳回 true: 也就是說,如果元件使用新的 props 렌더링的輸出和行為與舊的相同。 否則它應該傳回 false。 通常,你不會指定這個函式。 根據預設,React 會使用 Object.is 比較每個 prop。

回傳值

memo 會傳回一個新的 React 元件。 它的行為與提供給 memo 的元件相同,除非它的 props 發生變化,否則 React 不會始終在其父元件重新渲染時重新渲染它。


用法

當 props 不變時,跳過重新渲染

React 通常會在父組件重新渲染時,重新渲染其所有子組件。使用 memo,您可以建立一個組件,只要它的新 props 與舊 props 相同,React 就不會在父組件重新渲染時重新渲染它。這樣的組件被稱為已*記憶*。

要記憶一個組件,請將其包裝在 memo 中,並使用它返回的值來代替您的原始組件。

const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});

export default Greeting;

一個 React 組件應該始終具有純粹的渲染邏輯。 這表示如果它的 props、狀態和上下文沒有改變,它必須返回相同的輸出。通過使用 memo,您是在告訴 React 您的組件符合此要求,因此只要 props 沒有改變,React 就不需要重新渲染。即使使用 memo,如果組件自身的狀態發生變化,或者它正在使用的上下文發生變化,您的組件仍然會重新渲染。

在此範例中,請注意,每當 name 更改時(因為它是其 props 之一),Greeting 組件都會重新渲染,但當 address 更改時則不會(因為它沒有作為 prop 傳遞給 Greeting)。

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  return <h3>Hello{name && ', '}{name}!</h3>;
});

注意事項

您應該僅將 memo 作為效能優化手段。 如果您的程式碼在沒有它的情況下無法正常工作,請先找到根本問題並修復它。然後,您可以新增 memo 來提高效能。

深入探討

是否應該到處都加上 memo?

如果您的應用程式像這個網站一樣,大多數互動都是粗略的(例如替換頁面或整個區塊),通常不需要記憶。另一方面,如果您的應用程式更像一個繪圖編輯器,大多數互動都是細微的(例如移動形狀),那麼您可能會發現記憶非常有幫助。

僅當您的組件經常使用完全相同的 props 重新渲染,且其重新渲染邏輯很複雜時,使用 memo 進行優化才有價值。如果您的組件重新渲染時沒有明顯的延遲,則不需要 memo。請記住,如果傳遞給組件的 props *始終不同*,例如如果您傳遞在渲染期間定義的物件或普通函式,則 memo 將完全無用。這就是為什麼您經常需要將 useMemouseCallbackmemo 一起使用的原因。

在其他情況下,將組件包裝在 memo 中沒有好處。這樣做也沒有什麼顯著的壞處,因此有些團隊選擇不去考慮個別情況,而是盡可能地記憶。這種方法的缺點是程式碼變得難以閱讀。此外,並非所有記憶都有效:單個“始終是新的”值就足以破壞整個組件的記憶。

在實務中,您可以通過遵循一些原則來避免許多不必要的記憶。

  1. 當一個組件在視覺上包裝其他組件時,讓它接受 JSX 作為子組件。 這樣,當包裝器組件更新其自身狀態時,React 知道其子組件不需要重新渲染。
  2. 盡量使用局部狀態,並且不要將狀態提升到比所需更高的層級。例如,不要將臨時狀態(如表單和項目是否懸停)保留在樹的頂部或全域狀態庫中。
  3. 保持您的渲染邏輯純粹。 如果重新渲染組件導致問題或產生一些明顯的視覺錯誤,那就是組件中的錯誤!修復錯誤,而不是新增記憶。
  4. 避免不必要的副作用更新狀態。React 應用程式中的大多數效能問題是由於源自副作用的更新鏈導致您的組件不斷地重新渲染。
  5. 嘗試從您的副作用中移除不必要的依賴項。 例如,與其記憶,通常更簡單的方法是將某些物件或函式移動到副作用內或組件外。

如果特定的互動仍然感覺延遲,請使用 React 開發者工具分析器來查看哪些組件將從記憶中受益最多,並在需要的地方新增記憶。這些原則使您的組件更易於除錯和理解,因此在任何情況下都應遵循這些原則。從長遠來看,我們正在研究自動執行精細記憶,以一勞永逸地解決這個問題。


使用狀態更新 memoized 元件

即使元件已 memoized,當它自身的狀態改變時,它仍然會重新渲染。Memoization 只與從父元件傳遞到元件的 props 有關。

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log('Greeting was rendered at', new Date().toLocaleTimeString());
  const [greeting, setGreeting] = useState('Hello');
  return (
    <>
      <h3>{greeting}{name && ', '}{name}!</h3>
      <GreetingSelector value={greeting} onChange={setGreeting} />
    </>
  );
});

function GreetingSelector({ value, onChange }) {
  return (
    <>
      <label>
        <input
          type="radio"
          checked={value === 'Hello'}
          onChange={e => onChange('Hello')}
        />
        Regular greeting
      </label>
      <label>
        <input
          type="radio"
          checked={value === 'Hello and welcome'}
          onChange={e => onChange('Hello and welcome')}
        />
        Enthusiastic greeting
      </label>
    </>
  );
}

如果您將狀態變數設定為其目前的值,即使沒有 memo,React 也會跳過重新渲染您的元件。您可能仍然會看到您的元件函式被額外呼叫一次,但結果將會被捨棄。


使用 context 更新 memoized 元件

即使元件已 memoized,當它使用的 context 改變時,它仍然會重新渲染。Memoization 只與從父元件傳遞到元件的 props 有關。

import { createContext, memo, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('dark');

  function handleClick() {
    setTheme(theme === 'dark' ? 'light' : 'dark'); 
  }

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={handleClick}>
        Switch theme
      </button>
      <Greeting name="Taylor" />
    </ThemeContext.Provider>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  const theme = useContext(ThemeContext);
  return (
    <h3 className={theme}>Hello, {name}!</h3>
  );
});

若要讓您的元件僅在某些 context 的*一部分*變更時重新渲染,請將您的元件拆分為兩個。在外層元件中讀取您需要的 context 內容,並將其作為 prop 傳遞給 memoized 子元件。


最小化 props 的變更

當您使用 memo 時,只要任何 prop 與之前的數值*淺層不等於 (shallowly unequal)*,您的元件就會重新渲染。這表示 React 會使用 Object.is 比較來比較元件中每個 prop 與其先前的值。請注意,Object.is(3, 3)true,但 Object.is({}, {})false

要充分利用 memo,請盡量減少 props 變更的次數。例如,如果 prop 是一個物件,請使用 useMemo 防止父元件每次都重新建立該物件:

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);

const person = useMemo(
() => ({ name, age }),
[name, age]
);

return <Profile person={person} />;
}

const Profile = memo(function Profile({ person }) {
// ...
});

最小化 props 變更的更好方法是確保元件在其 props 中接收最少必要的資訊。例如,它可以接收個別的值,而不是整個物件。

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}

const Profile = memo(function Profile({ name, age }) {
// ...
});

即使是單獨的值,有時也可以投影到變更頻率較低的值。例如,這裡有一個元件接收一個布林值來指示值是否存在,而不是值本身。

function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}

const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});

當您需要將函式傳遞給 memoized 元件時,請在元件外部宣告它,使其永遠不會改變,或者使用 useCallback快取它在重新渲染之間的定義。


指定自訂比較函式

在極少數情況下,可能無法最小化 memoized 元件的 props 變更。在這種情況下,您可以提供一個自訂比較函式,React 將使用它來比較新舊 props,而不是使用淺層相等性。此函式作為第二個參數傳遞給 memo。 只有當新的 props 會產生與舊 props 相同的輸出時,它才應該傳回 true;否則它應該傳回 false

const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}

如果您這樣做,請使用瀏覽器開發人員工具中的效能面板來確保您的比較函式實際上比重新渲染元件更快。你可能會感到驚訝。

進行效能測量時,請確保 React 以生產模式執行。

陷阱

如果您提供自訂的 arePropsEqual 實作,您**必須比較每個 prop,包括函式。** 函式通常會 封閉 (close over) 父元件的 props 和狀態。如果您在 oldProps.onClick !== newProps.onClick 時傳回 true,您的元件將會在其 onClick 處理函式內持續「看到」先前渲染的 props 和狀態,導致非常令人困惑的錯誤。

避免在 arePropsEqual 內進行深度相等性檢查,除非您 100% 確定您正在處理的資料結構具有已知的有限深度。**深度相等性檢查會變得非常慢**,如果有人稍後更改資料結構,可能會讓您的應用程式凍結數秒鐘。


疑難排解

當 prop 是物件、陣列或函式時,我的組件會重新渲染

React 會透過淺層比較來比較舊的和新的 props:也就是說,它會考慮每個新的 prop 是否與舊的 prop 參考相等。如果您每次父組件重新渲染時都建立一個新的物件或陣列,即使個別元素都相同,React 仍然會認為它已更改。同樣地,如果您在渲染父組件時建立一個新的函式,即使該函式具有相同的定義,React 也會認為它已更改。為了避免這種情況,請簡化 props 或在父組件中記憶 props