Spec-Zone.ru › React 18

memo

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

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
  • Справочник
    • memo(Component, 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>;
});

Примечание

Вы должны использовать memo только как оптимизацию производительности. Если ваш код не работает без него, найдите первопричину проблемы и исправьте её. Затем вы можете добавить 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>
    </>
  );
}

Если вы присваиваете переменной состояния её текущее значение, 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>
  );
});

Чтобы заставить ваш компонент перерисовываться только тогда, когда часть некоторого контекста меняется, разделите ваш компонент на две части. Прочитайте необходимые данные из контекста во внешнем компоненте и передайте их мемоизированному дочернему компоненту в качестве свойства.


Минимизация изменений свойств

Когда вы используете 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;
    })
  );
}

Если вы это делаете, используйте панель производительности в инструментах разработчика вашего браузера, чтобы убедиться, что ваша функция сравнения на самом деле быстрее, чем повторная отрисовка компонента. Вас может удивить.

При измерении производительности убедитесь, что React работает в режиме производства.

Опасность

Если вы предоставляете пользовательскую реализацию arePropsEqual, вы должны сравнить каждый проп, включая функции. Функции часто ссылаются на пропы и состояние родительских компонентов. Если вы возвращаете true, когда oldProps.onClick !== newProps.onClick, ваш компонент будет продолжать «видеть» пропы и состояние из предыдущей отрисовки внутри своего обработчика onClick, что приведёт к очень запутанным ошибкам.

Избегайте глубоких проверок равенства внутри arePropsEqual, если вы на 100% уверены, что структура данных, с которой вы работаете, имеет известную ограниченную глубину. Глубокие проверки равенства могут стать невероятно медленными и могут заморозить ваше приложение на несколько секунд, если кто-то позже изменит структуру данных.


Устранение неполадок

Мой компонент перерисовывается, когда проп является объектом, массивом или функцией

React сравнивает старые и новые пропы по поверхностному равенству: то есть, он проверяет, является ли каждый новый проп ссылкой на старый проп. Если вы каждый раз при перерисовке родителя создаёте новый объект или массив, даже если отдельные элементы одинаковые, React всё равно будет считать это изменением. Аналогично, если вы создаёте новую функцию при отрисовке родительского компонента, React будет считать это изменением, даже если функция имеет такое же определение. Чтобы этого избежать, упростите пропы или кешируйте пропы в родительском компоненте.

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/reference/react/memo

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API