memo

memo дозволяє пропустити повторний рендеринг компонента, якщо його пропси не змінено.

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

Довідник

memo(Component, arePropsEqual?)

Обгорнути компонент у memo, щоб отримати запам'ятовану версію цього компонента. Ця запам'ятована версія вашого компонента зазвичай не буде повторно рендеритися, коли рендериться її батьківський компонент, якщо її пропси не змінилися. Але React все одно може відрендерити її повторно: запам'ятовування - це оптимізація продуктивності, а не гарантія.

import { memo } from 'react';

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

Дивіться більше прикладів нижче.

Параметри

  • Component: Компонент, який ви хочете запам'ятати. memo не змінює цей компонент, але повертає новий, запам'ятований компонент. Приймається будь-який дійсний React-компонент, включаючи функції та forwardRef-компоненти.

  • опціонально arePropsEqual: Функція, яка приймає два аргументи: попередні пропси компонента та його нові пропси. Вона має повернути true, якщо старі та нові пропси рівні: тобто, якщо компонент виводитиме те саме виведення і поводитиметься так само зі старими пропсами, як і з новими. В іншому випадку слід повернути false. Зазвичай ви не вказуєте цю функцію. За замовчуванням React буде порівнювати кожен проп з Object.is.

Повернення

memo повертає новий React-компонент. Він поводиться так само, як і компонент, наданий memo, за винятком того, що React не завжди буде повторно рендерити його, коли повторно рендериться його батько, якщо тільки його пропси не змінилися.


Використання

Пропуск повторного рендерингу при незмінних пропсах

React зазвичай перерендерить компонент щоразу, коли перерендериться його батько. За допомогою memo ви можете створити компонент, який React не буде повторно рендерити, коли рендериться його батько, якщо його нові пропси не відрізняються від старих. Такий компонент називається запам'ятованим.

Щоб запам'ятати компонент, оберніть його у memo і використовуйте значення, яке він повертає, замість вашого оригінального компонента:

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

export default Greeting;

React-компонент завжди повинен мати чисту логіку рендерингу. Це означає, що він повинен повертати те саме виведення, якщо його пропси, стан та контекст не змінилися. Використовуючи memo, ви повідомляєте React, що ваш компонент відповідає цій вимозі, тому React не потрібно повторно рендерити, поки його пропси не змінилися. Навіть з memo ваш компонент буде повторно рендерити, якщо зміниться його власний стан або контекст, який він використовує.

У цьому прикладі зверніть увагу, що компонент Greeting повторно рендериться щоразу, коли змінюється name (оскільки це один з його пропсів), але не тоді, коли змінюється address (оскільки він не передається 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>;
});
label {
  display: block;
  margin-bottom: 16px;
}

Вам слід покладатися лише на memo як на оптимізацію продуктивності. Якщо ваш код не працює без нього, спочатку знайдіть основну проблему і виправте її. Після цього ви можете додати memo для покращення продуктивності.

Чи потрібно скрізь додавати примітку?

Якщо ваша програма подібна до цього сайту, і більшість взаємодій є грубими (наприклад, заміна сторінки або цілого розділу), запам'ятовування зазвичай не потрібне. З іншого боку, якщо ваша програма більше схожа на редактор малюнків, і більшість взаємодій є деталізованими (наприклад, переміщення фігур), то запам'ятовування може виявитися дуже корисним.

Оптимізація за допомогою memo має сенс лише тоді, коли ваш компонент часто повторно рендериться з тими самими пропсами, а його логіка повторного рендерингу складна. Якщо при повторному рендерингу вашого компонента немає відчутної затримки, memo не потрібен. Майте на увазі, що memo є абсолютно марним, якщо пропси, передані вашому компоненту, є завжди різними, наприклад, якщо ви передаєте об'єкт або просту функцію, визначену під час рендеру. Ось чому вам часто знадобляться useMemo та useCallback разом з memo.

В інших випадках немає жодної користі від обгортання компонента у memo. Істотної шкоди від цього також немає, тому деякі команди вирішують не думати про окремі випадки і запам'ятовувати якомога більше. Недоліком такого підходу є те, що код стає менш читабельним. Крім того, не всяке запам'ятовування є ефективним: одного значення, яке є "завжди новим", достатньо, щоб порушити запам'ятовування для цілого компонента.

На практиці, ви можете зробити багато запам'ятовування непотрібним, дотримуючись кількох принципів:

  1. Коли компонент візуально обгортає інші компоненти, дозвольте йому приймати JSX як дочірні. Тоді, якщо компонент-обгортка оновлює власний стан, React знатиме, що його дочірні компоненти не потребують повторного рендерингу.
  2. Надавайте перевагу локальному стану і не піднімайте стан вище ніж це необхідно. Наприклад, не зберігайте перехідні стани, як-от форми, і незалежно від того, чи елемент наведено на вершину дерева, чи у глобальній бібліотеці станів.
  3. Зберігайте логіку рендерингу чистою. Якщо повторний рендеринг компонента викликає проблему або створює помітний візуальний артефакт, це вада у вашому компоненті! Виправте ваду замість того, щоб додавати запам'ятовування.
  4. Уникайте непотрібних ефектів, які оновлюють стан. Більшість проблем з продуктивністю у React-додатках спричинені ланцюжками оновлень від ефектів, які змушують ваші компоненти рендерити знову і знову.
  5. Спробуйте видалити непотрібні залежності з ефектів.Наприклад, замість запам'ятовування часто простіше перемістити якийсь об'єкт або функцію всередину ефекту або за межі компонента.

Якщо певна взаємодія все ще гальмує, використайте профайлер React Developer Tools, щоб побачити, які компоненти отримають найбільшу користь від запам'ятовування, і додайте запам'ятовування там, де це необхідно. Ці принципи полегшують налагодження та розуміння ваших компонентів, тому варто дотримуватися їх у будь-якому випадку. У довгостроковій перспективі ми досліджуємо можливість автоматичного використання гранульованої пам'яті, щоб вирішити цю проблему раз і назавжди.


Оновлення запам'ятованого компонента за допомогою стану

Навіть коли компонент запам'ятовується, він все одно буде повторно зображуватися при зміні його стану. Запам'ятовування стосується лише пропсів, які передаються компоненту від його батька.

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>
    </>
  );
}
label {
  display: block;
  margin-bottom: 16px;
}

Якщо ви встановите змінну стану у поточне значення, React пропустить повторний рендеринг вашого компонента навіть без memo. Ви можете побачити, що функція вашого компонента буде викликана додатковий раз, але результат буде відкинуто.


Оновлення запам'ятованого компонента за допомогою контексту

Навіть якщо компонент запам'ятовується, його все одно буде повторно відрендерено при зміні контексту, який він використовує. Запам'ятовування стосується лише пропсів, які передаються компоненту від його батька.

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>
  );
});
label {
  display: block;
  margin-bottom: 16px;
}

.light {
  color: black;
  background-color: white;
}

.dark {
  color: white;
  background-color: black;
}

Щоб ваш компонент повторно рендерився лише тоді, коли змінюється частина деякого контексту, розділіть ваш компонент на дві частини. Прочитайте те, що вам потрібно, з контексту у зовнішньому компоненті і передайте його дочірньому компоненту як проп.


Мінімізація змін пропсів

Коли ви використовуєте memo, ваш компонент повторно рендерить щоразу, коли будь-який проп не близько дорівнює тому, що було раніше. Це означає, що React порівнює кожен проп у вашому компоненті з його попереднім значенням, використовуючи порівняння Object.is. Зверніть увагу, що Object.is(3, 3) є true, але Object.is({}, {}) є false.

Щоб отримати максимальну віддачу від memo, мінімізуйте час зміни пропсів. Наприклад, якщо реквізит є об'єктом, не дозволяйте батьківському компоненту щоразу створювати цей об'єкт наново, використовуючи 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 }) {
  // ...
});

Кращий спосіб мінімізувати зміни пропсів - переконатися, що компонент приймає мінімально необхідну інформацію у своїх пропсах. Наприклад, він може приймати окремі значення замість цілого об'єкта:

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 }) {
  // ...
});

Коли вам потрібно передати функцію запам'ятовуваному компоненту, або оголосіть її поза компонентом, щоб вона ніколи не змінювалася, або useCallback кешуйте її визначення між повторними рендерингами.


Вказівка користувацької функції порівняння

У рідкісних випадках може бути неможливо мінімізувати зміни пропсів запам'ятованого компонента. У такому випадку ви можете створити власну функцію порівняння, яку React буде використовувати для порівняння старого і нового пропсів замість поверхневої рівності. Ця функція передається як другий аргумент до memo. Вона має повертати 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;
    })
  );
}

Якщо ви це зробите, скористайтеся панеллю Performance в інструментах розробника вашого браузера, щоб переконатися, що ваша функція порівняння насправді працює швидше, ніж перерендеринг компонента. Ви можете бути здивовані.

Під час вимірювання продуктивності переконайтеся, що React працює у режимі виробництва.

Якщо ви надаєте власну arePropsEqual реалізацію, ви повинні порівнювати кожен проп, включаючи функції.Функції часто перекривають проп та стан батьківських компонентів. Якщо ви повернете true при oldProps.onClick !== newProps.onClick, ваш компонент продовжить "бачити" пропси та стан з попереднього рендерингу у своєму обробнику onClick, що призведе до дуже заплутаних помилок.

Уникайте глибоких перевірок на рівність всередині arePropsEqual, якщо ви не впевнені на 100%, що структура даних, з якою ви працюєте, має відому обмежену глибину. Глибока перевірка на рівність може стати неймовірно повільною і заморозити вашу програму на багато секунд, якщо хтось пізніше змінить структуру даних.


Налагодження

Мій компонент повторно рендерить, коли проп є об'єктом, масивом або функцією

React порівнює старі та нові пропси за неглибокою рівністю: тобто перевіряє, чи кожен новий проп є рівним за рефом до старого. Якщо ви створюєте новий об'єкт або масив кожного разу, коли батько рендериться повторно, навіть якщо окремі елементи будуть однаковими, React все одно вважатиме їх зміненими. Аналогічно, якщо ви створюєте нову функцію під час рендерингу батьківського компонента, React вважатиме його зміненим, навіть якщо функція має те саме визначення. Щоб уникнути цього, спростіть пропси або запам'ятайте пропси в батьківському компоненті.