useMemo

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

const cachedValue = useMemo(calculateValue, dependencies)

Довідник

useMemo(calculateValue, dependencies)

Викличте useMemo на верхньому рівні вашого компонента, щоб кешувати обчислення між повторними рендерами:

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

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

Параметри

  • calculateValue: Функція, що обчислює значення, яке ви хочете кешувати. Вона повинна бути чистою, не приймати жодних аргументів і повертати значення будь-якого типу. React викличе вашу функцію під час першого рендерингу. При наступних рендерингах React знову поверне те саме значення, якщо залежності не змінилися з моменту останнього рендерингу. В іншому випадку він викличе calculateValue, поверне результат і збереже його, щоб його можна було використати пізніше.

  • залежності: список усіх реактивних значень, на які посилається код calculateValue. Реактивні значення включають пропси, стан і всі змінні та функції, оголошені безпосередньо у тілі вашого компонента. Якщо ваш лінтер налаштований на React, він буде перевіряти, що кожне реактивне значення правильно вказане як залежність. Список залежностей повинен мати постійну кількість елементів і бути записаний в лінію як [dep1, dep2, dep3]. React порівняє кожну залежність з попереднім значенням за допомогою Object.is порівняння.

Повернення

При початковому рендерингу useMemo повертає результат виклику calculateValue без аргументів.

Під час наступних рендерингів він або повертатиме вже збережене значення з останнього рендерингу (якщо залежності не змінилися), або знову викликатиме calculateValue і повертатиме результат, який повернув calculateValue.

Застереження

  • useMemo є хуком, тому ви можете викликати його лише на верхньому рівні вашого компонента або ваших власних хуків. Ви не можете викликати його всередині циклів або умов. Якщо вам це потрібно, витягніть новий компонент і перемістіть стан до нього.
  • У суворому режимі React викличе вашу обчислювальну функцію двічідля того, щоб допомогти вам знайти випадкові домішки. Це поведінка лише для розробки і не впливає на виробництво. Якщо ваша функція обчислення чиста (як і має бути), це не повинно вплинути на вашу логіку. Результат одного з викликів буде проігноровано.
  • React не викидатиме кешоване значення, якщо для цього немає конкретної причини. Наприклад, під час розробки React викидає кеш, коли ви редагуєте файл вашого компонента. Як в розробці, так і у виробництві, React викидає кеш, якщо ваш компонент призупиняється під час початкового монтування. У майбутньому React може додати більше можливостей, які використовують переваги відкидання кешу - наприклад, якщо в майбутньому React додасть вбудовану підтримку віртуалізованих списків, то має сенс відкидати кеш для елементів, які прокручуються за межами області перегляду віртуалізованої таблиці. Це буде добре, якщо ви покладаєтесь на useMemo виключно як на оптимізацію продуктивності. В іншому випадку, змінна стану або посилання можуть бути більш доречними.

Кешування значень, що повертаються, також відоме як запам'ятовування, тому цей хук називається useMemo.


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

Пропуск складних повторних обчислень

.

Щоб кешувати обчислення між повторними рендерингами, обгорніть його у виклик useMemo на верхньому рівні вашого компонента:

 filterTodos(todos, tab)"], [2, 4, "[todos, tab]"]]" class="language-js">import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
}

Вам потрібно передати дві речі до useMemo:

  1. А функція обчислення яка не приймає аргументів, як () =>, і повертає те, що ви хотіли обчислити.
  2. Список залежностей Список залежностей включаючи кожне значення з вашого компонента, що використовується у ваших обчисленнях.

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

При кожному наступному рендерингу React буде порівнювати залежності з з залежностями, які ви передали під час останнього рендерингу. Якщо жодна з залежностей не змінилася (порівняно з Object.is), useMemo поверне значення, яке ви вже обчислювали раніше. В іншому випадку React перезапустить обчислення і поверне нове значення.

Інакше кажучи, useMemo кешує результат обчислення між повторними рендерингами, доки не зміняться його залежності.

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

За замовчуванням, React буде повторно запускати все тіло вашого компонента щоразу при повторному рендері. Наприклад, якщо цей TodoList оновить свій стан або отримає нові пропси від свого батька, функція filterTodos буде перезапущена:

function TodoList({ todos, tab, theme }) {
  const visibleTodos = filterTodos(todos, tab);
  // ...
}

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

Цей тип кешування називається мемоїзацією.

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

Як визначити, чи обчислення є дорогим?

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

console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');

Виконайте взаємодію, яку ви вимірюєте (наприклад, введення даних). Після цього у вашій консолі ви побачите записи на кшталт filter array: 0.15ms. Якщо загальний час, зафіксований у логах, складає значну цифру (скажімо, 1ms або більше), може мати сенс запам'ятати це обчислення. В якості експерименту ви можете обгорнути обчислення у useMemo, щоб перевірити, чи зменшився загальний час, записаний для цієї взаємодії, чи ні:

console.time('filter array');
const visibleTodos = useMemo(() => {
  return filterTodos(todos, tab); // Skipped if todos and tab haven't changed
}, [todos, tab]);
console.timeEnd('filter array');

useMemo не пришвидшать рендеринг першого . Це лише допоможе вам уникнути непотрібної роботи з оновленнями.

Майте на увазі, що ваш комп'ютер, ймовірно, швидший за комп'ютери ваших користувачів, тому варто перевірити продуктивність за допомогою штучного уповільнення. Наприклад, Chrome пропонує для цього опцію Дроселювання процесора.

Також зауважте, що вимірювання продуктивності в процесі розробки не дасть вам найточніших результатів. (Наприклад, якщо увімкнено Суворий режим, ви побачите, що кожен компонент рендериться двічі, а не один раз). Щоб отримати найточніші таймінги, створіть свій застосунок для виробництва і протестуйте його на пристрої, подібному до того, який мають ваші користувачі.

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

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

Оптимізація за допомогою useMemo має сенс лише у кількох випадках:

  • Обчислення, яке ви вкладаєте у useMemo, є помітно повільним, і його залежності рідко змінюються.
  • Ви передаєте його як проп до компонента, обгорнутого у memo. Ви хочете пропустити повторний рендер, якщо значення не змінилося. Запам'ятовування дозволяє перерендерити компонент лише у разі зміни залежностей.
  • Значення, яке ви передаєте, пізніше використовується як залежність деякого хука. Наприклад, від нього може залежати інше значення обчислення useMemo. Або, можливо, ви залежите від цього значення з useEffect.

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

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

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

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

Пропуск повторного обчислення з useMemo

У цьому прикладі реалізацію filterTodos штучно сповільнена, щоб ви могли побачити, що відбувається, коли якась функція JavaScript, яку ви викликаєте під час рендерингу, дійсно працює повільно. Спробуйте перемикати вкладки і перемикати тему.

Перемикання вкладок відбувається повільно, оскільки це змушує повторно виконувати уповільнений filterTodos. Це очікувано, оскільки tab змінилася, і тому все обчислення потребує повторного запуску . (Якщо вам цікаво, чому він виконується двічі, це пояснюється тут.)

Перемикання теми. Завдяки useMemo вона швидка, незважаючи на штучне сповільнення! Повільний виклик filterTodos було пропущено, оскільки todos та tab (які ви передали як залежності до useMemo) не змінилися з часу останнього рендерингу.

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import { useMemo } from 'react';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // Do nothing for 500 ms to emulate extremely slow code
  }

  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

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

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

Завжди перераховує значення

У цьому прикладі реалізацію filterTodos також штучно сповільнена , щоб ви могли побачити, що відбувається, коли якась функція JavaScript, яку ви викликаєте під час рендерингу, дійсно працює повільно. Спробуйте перемикати вкладки і перемикати тему.

На відміну від попереднього прикладу, перемикання теми тепер також відбувається повільно! Це пов'язано з тим, що у цій версії немає виклику useMemo, тому штучно сповільнений filterTodos викликається під час кожного повторного рендерингу. Він викликається, навіть якщо змінено лише тему.

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <ul>
        <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // Do nothing for 500 ms to emulate extremely slow code
  }

  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

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

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

Втім, ось той самий код з вилученим штучним сповільненням. Відсутність useMemo відчутна чи ні?

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  console.log('Filtering ' + todos.length + ' todos for "' + tab + '" tab.');

  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

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

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

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

Ви можете спробувати збільшити кількість пунктів todo у utils.js і подивитися, як зміниться поведінка. Цей конкретний розрахунок був не дуже вагомим на початку, але якщо кількість тодосів значно зросте, більша частина накладних витрат буде пов'язана з повторним рендерингом, а не з фільтруванням. Продовжуйте читати нижче, щоб дізнатися, як можна оптимізувати повторний рендеринг за допомогою useMemo.

.

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

У деяких випадках useMemo також може допомогти вам оптимізувати продуктивність повторного рендерингу дочірніх компонентів. Щоб проілюструвати це, скажімо, компонент TodoList передає visibleTodos як проп дочірньому компоненту List:

export default function TodoList({ todos, tab, theme }) {
  // ...
  return (
    <div className={theme}>
      <List items={visibleTodos} />
    </div>
  );
}

Ви помітили, що перемикання пропсу theme на мить призупиняє роботу програми, але якщо ви видалите <List /> з вашого JSX, то програма працює швидко. Це говорить про те, що варто спробувати оптимізувати компонент List.

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

import { memo } from 'react';

const List = memo(function List({ items }) {
  // ...
});

З цією зміною List пропустить повторний рендеринг, якщо всі його пропси будуть такими самими, як і під час останнього рендерингу. Ось де кешування обчислень стає важливим! Припустимо, що ви обчислили visibleTodos без useMemo:

export default function TodoList({ todos, tab, theme }) {
  // Every time the theme changes, this will be a different array...
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      {/* ... so List's props will never be the same, and it will re-render every time */}
      <List items={visibleTodos} />
    </div>
  );
}

У наведеному вище прикладі функція filterTodos завжди створює різний масив, подібно до того, як об'єктний літерал {} завжди створює новий об'єкт. Зазвичай це не було б проблемою, але це означає, що пропси List ніколи не будуть однаковими, і ваша memo оптимізація не працюватиме. Ось де useMemo стає у нагоді:

export default function TodoList({ todos, tab, theme }) {
  // Tell React to cache your calculation between re-renders...
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab] // ...so as long as these dependencies don't change...
  );
  return (
    <div className={theme}>
      {/* ...List will receive the same props and can skip re-rendering */}
      <List items={visibleTodos} />
    </div>
  );
}

Загортаючи обчислення visibleTodos у useMemo, ви гарантуєте, що воно матиме те саме значення між повторними рендерами (доки не зміняться залежності). Вам не потрібно обертати обчислення у useMemo, якщо тільки ви не робите цього з якоїсь конкретної причини. У цьому прикладі причиною є те, що ви передаєте його компоненту, обгорнутому у memo,, і це дозволяє йому пропустити повторний рендеринг. Існує декілька інших причин для додавання useMemo, які описано далі на цій сторінці.

Запам'ятовування окремих вузлів JSX

Замість того, щоб загортати List у memo, ви можете загортати сам вузол <List /> JSX у useMemo:

export default function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
  return (
    <div className={theme}>
      {children}
    </div>
  );
}

Поведінка буде однаковою. Якщо visibleTodos не було змінено, List не буде перемальовано.

Вузол JSX типу <List items={visibleTodos} /> є об'єктом типу { type: List, props: { items: visibleTodos } }. Створення такого об'єкту є дуже дешевим, але React не знає, чи його вміст є таким самим, як минулого разу. Ось чому за замовчуванням React повторно відрендерить компонент List.

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

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

Пропуск повторного рендерингу з useMemo та memo

У цьому прикладі List компонент штучно сповільнено, щоб ви могли побачити, що відбувається, коли React-компонент, який ви рендерите, дійсно повільний. Спробуйте перемикати вкладки та перемикати тему.

Перемикання вкладок відбувається повільно, оскільки це змушує повторно виконувати уповільнений List. Це очікувано, оскільки tab змінилася, і вам потрібно відобразити новий вибір користувача на екрані.

Далі спробуйте переключити тему. Завдяки useMemo та memo це працює швидко, незважаючи на штучне сповільнення! List пропустив повторний рендеринг, оскільки масив visibleTodos не змінився з часу останнього рендера. Масив visibleTodos не змінився, оскільки todos і tab (які ви передали як залежності до useMemo) не змінилися з часу останнього рендеру.

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import { useMemo } from 'react';
import List from './List.js';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>List</code> is artificially slowed down!</b></p>
      <List items={visibleTodos} />
    </div>
  );
}
import { memo } from 'react';

const List = memo(function List({ items }) {
  console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // Do nothing for 500 ms to emulate extremely slow code
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.completed ?
            <s>{item.text}</s> :
            item.text
          }
        </li>
      ))}
    </ul>
  );
});

export default List;
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

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

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

Завжди перемальовувати компонент

.

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

На відміну від попереднього прикладу, перемикання теми тепер також відбувається повільно! Це пов'язано з тим, що у цій версії немає виклику useMemo, тому visibleTodos завжди є іншим масивом, і уповільнений List компонент не може пропустити повторний рендеринг.

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import List from './List.js';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <p><b>Note: <code>List</code> is artificially slowed down!</b></p>
      <List items={visibleTodos} />
    </div>
  );
}
import { memo } from 'react';

const List = memo(function List({ items }) {
  console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
  let startTime = performance.now();
  while (performance.now() - startTime < 500) {
    // Do nothing for 500 ms to emulate extremely slow code
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.completed ?
            <s>{item.text}</s> :
            item.text
          }
        </li>
      ))}
    </ul>
  );
});

export default List;
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

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

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

Однак, ось той самий код з прибраним штучним сповільненням. Відсутність useMemo відчутна чи ні?

import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';

const todos = createTodos();

export default function App() {
  const [tab, setTab] = useState('all');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <button onClick={() => setTab('all')}>
        All
      </button>
      <button onClick={() => setTab('active')}>
        Active
      </button>
      <button onClick={() => setTab('completed')}>
        Completed
      </button>
      <br />
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <hr />
      <TodoList
        todos={todos}
        tab={tab}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
import List from './List.js';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <List items={visibleTodos} />
    </div>
  );
}
import { memo } from 'react';

function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.completed ?
            <s>{item.text}</s> :
            item.text
          }
        </li>
      ))}
    </ul>
  );
}

export default memo(List);
export function createTodos() {
  const todos = [];
  for (let i = 0; i < 50; i++) {
    todos.push({
      id: i,
      text: "Todo " + (i + 1),
      completed: Math.random() > 0.5
    });
  }
  return todos;
}

export function filterTodos(todos, tab) {
  return todos.filter(todo => {
    if (tab === 'all') {
      return true;
    } else if (tab === 'active') {
      return !todo.completed;
    } else if (tab === 'completed') {
      return todo.completed;
    }
  });
}
label {
  display: block;
  margin-top: 10px;
}

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

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

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

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


Запам'ятовування залежності іншого хука

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

function Dropdown({ allItems, text }) {
  const searchOptions = { matchMode: 'whole-word', text };

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
  // ...

Така залежність від об'єкта суперечить сенсу запам'ятовування. При повторному рендерингу компонента весь код безпосередньо у тілі компонента виконується знову. Рядки коду, що створюють об'єкт searchOptions, також будуть виконуватися при кожному повторному рендерингу. Оскільки searchOptions є залежністю від вашого виклику useMemo, а він щоразу інший, React знає, що залежності різні, і щоразу переобчислює searchItems.

Щоб виправити це, ви могли б запам'ятати об'єкт searchOptions сам перед передачею його як залежність:

function Dropdown({ allItems, text }) {
  const searchOptions = useMemo(() => {
    return { matchMode: 'whole-word', text };
  }, [text]); // ✅ Only changes when text changes

  const visibleItems = useMemo(() => {
    return searchItems(allItems, searchOptions);
  }, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
  // ...

У наведеному вище прикладі, якщо текст не змінився, об'єкт searchOptions також не зміниться. Однак, ще кращим виправленням є переміщення оголошення об'єкта searchOptions всередину функції обчислення useMemo:

function Dropdown({ allItems, text }) {
  const visibleItems = useMemo(() => {
    const searchOptions = { matchMode: 'whole-word', text };
    return searchItems(allItems, searchOptions);
  }, [allItems, text]); // ✅ Only changes when allItems or text changes
  // ...

Тепер ваше обчислення залежить безпосередньо від тексту (який є рядком і не може "випадково" стати іншим).


Запам'ятовування функції

Припустімо, що компонент Form обгорнуто у memo. Ви хочете передати йому функцію як проп:

export default function ProductPage({ productId, referrer }) {
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails
    });
  }

  return <Form onSubmit={handleSubmit} />;
}

Так само, як {} створює інший об'єкт, оголошення функцій на кшталт function() {} та вирази на кшталт () => {} створюють іншу функцію при кожному повторному рендерингу. Саме по собі створення нової функції не є проблемою. Цього не варто уникати! Однак, якщо компонент Form запам'ятовується, ймовірно, ви хочете пропустити його повторний рендеринг, якщо жодні пропси не змінилися. Проп, який завжди відрізняється, не матиме сенсу для запам'ятовування.

Щоб запам'ятати функцію з useMemo, ваша обчислювальна функція повинна повернути іншу функцію:

export default function Page({ productId, referrer }) {
  const handleSubmit = useMemo(() => {
    return (orderDetails) => {
      post('/product/' + productId + '/buy', {
        referrer,
        orderDetails
      });
    };
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}

Це виглядає незграбно! Запам'ятовування функцій є досить поширеним явищем, і React має вбудований хук спеціально для цього. Обгортайте ваші функції у useCallback замість useMemo, щоб уникнути необхідності писати додаткову вкладену функцію:

export default function Page({ productId, referrer }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails
    });
  }, [productId, referrer]);

  return <Form onSubmit={handleSubmit} />;
}

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


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

Мої обчислення запускаються двічі під час кожного повторного рендерингу

У Суворому режимі React викликатиме деякі ваші функції двічі замість одного:

function TodoList({ todos, tab }) {
  // This component function will run twice for every render.

  const visibleTodos = useMemo(() => {
    // This calculation will run twice if any of the dependencies change.
    return filterTodos(todos, tab);
  }, [todos, tab]);

  // ...

Це очікувано і не повинно зламати ваш код.

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

Наприклад, ця нечиста функція обчислення мутує масив, який ви отримали як проп:

const visibleTodos = useMemo(() => {
    // 🚩 Mistake: mutating a prop
    todos.push({ id: 'last', text: 'Go for a walk!' });
    const filtered = filterTodos(todos, tab);
    return filtered;
  }, [todos, tab]);

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

const visibleTodos = useMemo(() => {
    const filtered = filterTodos(todos, tab);
    // ✅ Correct: mutating an object you created during the calculation
    filtered.push({ id: 'last', text: 'Go for a walk!' });
    return filtered;
  }, [todos, tab]);

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

Також ознайомтеся з настановами щодо оновлення об'єктів та оновлення масивів без мутації.


Мій виклик useMemo мав би повернути об'єкт, але повертає undefined

Цей код не працює:

// 🔴 You can't return an object from an arrow function with () => {
  const searchOptions = useMemo(() => {
    matchMode: 'whole-word',
    text: text
  }, [text]);

У JavaScript () => { починає тіло функції стрілки, тому дужка { не є частиною вашого об'єкта. Тому вона не повертає об'єкт і призводить до помилок. Ви можете виправити це, додавши дужки на зразок ({ і }):

// This works, but is easy for someone to break again
  const searchOptions = useMemo(() => ({
    matchMode: 'whole-word',
    text: text
  }), [text]);

Втім, це все ще заплутано і занадто легко для когось, щоб зламати, видаливши дужки.

Щоб уникнути цієї помилки, напишіть інструкцію return явно:

// ✅ This works and is explicit
  const searchOptions = useMemo(() => {
    return {
      matchMode: 'whole-word',
      text: text
    };
  }, [text]);

Кожного разу, коли мій компонент рендерить, обчислення в useMemo запускаються повторно

Переконайтеся, що ви вказали масив залежностей як другий аргумент!

Якщо ви забудете масив залежностей, useMemo щоразу запускатиме обчислення наново:

function TodoList({ todos, tab }) {
  // 🔴 Recalculates every time: no dependency array
  const visibleTodos = useMemo(() => filterTodos(todos, tab));
  // ...

Це виправлена версія, яка передає масив залежностей як другий аргумент:

function TodoList({ todos, tab }) {
  // ✅ Does not recalculate unnecessarily
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...

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

const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  console.log([todos, tab]);

Після цього ви можете клацнути правою кнопкою миші на масивах з різних рендерингів у консолі і вибрати "Зберегти як глобальну змінну" для обох. Якщо припустити, що перший масив було збережено як temp1, а другий - як temp2, ви можете скористатися консоллю браузера, щоб перевірити, чи кожна залежність в обох масивах однакова:

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

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


Мені потрібно викликати useMemo для кожного елемента списку у циклі, але це не дозволено

Припустимо, що компонент Chart обгорнуто у memo. Ви хочете пропустити повторний рендеринг кожного Chart у списку при повторному рендерингу компонента ReportList. Однак, ви не можете викликати useMemo у циклі:

function ReportList({ items }) {
  return (
    <article>
      {items.map(item => {
        // 🔴 You can't call useMemo in a loop like this:
        const data = useMemo(() => calculateReport(item), [item]);
        return (
          <figure key={item.id}>
            <Chart data={data} />
          </figure>
        );
      })}
    </article>
  );
}

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

function ReportList({ items }) {
  return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}

function Report({ item }) {
  // ✅ Call useMemo at the top level:
  const data = useMemo(() => calculateReport(item), [item]);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
}

Як варіант, ви можете видалити useMemo і натомість обгорнути Report у memo. Якщо проп item не буде змінено, Report пропустить повторний рендеринг, тому Chart також пропустить повторний рендеринг:

function ReportList({ items }) {
  // ...
}

const Report = memo(function Report({ item }) {
  const data = calculateReport(item);
  return (
    <figure>
      <Chart data={data} />
    </figure>
  );
});