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: функция, которая принимает два аргумента: предыдущие пропсы и новые пропсы компонента. Функция 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, что ваш компонент подчиняется этим требованиям, поэтому в повторном рендере нет необходимости до тех пор, пока пропсы не изменились. Но даже при использовании 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>;
});

Note

Следует полагаться на memo только в качестве оптимизации производительности. Если ваш код не работает без memo, найдите основную проблему и сперва устраните её. После этого можете добавить memo, чтобы повысить производительность.

Deep Dive

Нужно ли везде добавлять memo?

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

Оптимизация с memo является очень ценной, когда ваш компонент повторно рендерится с абсолютно одинаковыми пропсами, и повторная отрисовка очень дорогостоящая. Если при повторном рендере нет заметной задержки, использовать memo необязательно. Учтите, что memo будет абсолютно бесполезным решением, если передаваемые пропсы всегда разные, например, при передаче объектов или функций, которые создаются каждый раз с нуля. Именно поэтому, чаще всего вам нужно использовать useMemo и useCallback в паре с memo.

В любых других случаях нет никаких преимуществ использования memo. Так же как и нет существенного вреда, поэтому некоторые команды выбирают не думать о конкретных случаях и использовать мемоизацию как можно чаще. Обратная сторона такого подхода—менее читаемый код. Также мемоизация не будет эффективна абсолютно во всех случаях: одного «всегда нового» значения достаточно, чтобы нарушить мемоизацию для всего компонента.

На практике, вы можете избавиться от излишней мемоизации, следуя нескольким принципам:

  1. Когда компонент визуально оборачивает другой компонент, позвольте ему принимать JSX в качестве children. Это подход, когда оборачиваемый компонент обновляет собственное состояние и React знает, что дочерние компоненты не нуждаются в повторном рендере.
  2. Предпочитайте использование внутреннего состояния и не поднимайте состояние выше чаще, чем это необходимо. Например, не сохраняйте кратковременное состояние как формы, независимо от того, находится ли ваш компонент на верхнем уровне вашего дерева или в глобальной библиотеке состояний.
  3. Сохраняйте чистой вашу логику рендера. Если повторный рендер является причиной проблемы или создаёт заметный визуальный дефект, это ошибка в вашем компоненте! Постарайтесь исправить ошибку вместо использования мемоизации.
  4. Избегайте ненужных Эффектов, которые обновляют состояние. Большинство проблем с производительностью в React-приложении вызвано цепочкой обновлений в useEffect, которые заставляют ваши компоненты рендериться снова и снова.
  5. Попробуйте убрать ненужные зависимости из ваших эффектов. Например, вместо мемоизации, довольно часто проще переместить некоторые объекты или функции внутрь вашего эффекта или за пределы компонента.

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


Обновление мемоизированного компонента с использованием состояния

Даже если компонент мемоизирован, он всё ещё будет повторно рендериться, когда изменяется его внутреннее состояние. Мемоизация работает только с приходящими пропсами из родительского компонента.

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;
})
);
}

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

Когда вы измеряете производительность, убедитесь, что React запущен в продакшен-режиме.

Pitfall

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

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


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

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

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