JavaRush /Курсы /Модуль 3: React /Работа с useCallback и useMemo для оптимизации функций и ...

Работа с useCallback и useMemo для оптимизации функций и значений

Модуль 3: React
10 уровень , 8 лекция
Открыта

Теория: Зачем нужны useCallback и useMemo?

Вот мы и дошли до одного из ключевых инструментов оптимизации производительности в React — хуков useCallback и useMemo. Если бы React-приложение было автомобилем, то эти хуки можно сравнить с режимами "ЭКО": они помогают экономить ресурсы и избегать лишних рендеров. Сегодня мы поговорим о том, как и зачем их оптимизировать.

Прежде чем углубляться в код, давайте разберёмся, почему вообще стоит их использовать:

  • useCallback: нужен для мемоизации функций, чтобы они не создавались заново при каждом ререндере компонента.
  • useMemo: нужен для мемоизации вычислений, чтобы тяжелые операции (например, фильтрация или сортировка данных) не выполнялись на каждом рендере.

Проблема: Ненужное создание функций

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

import React, { useState } from 'react';

const Button = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>Click me</button>;
});

export const App = () => {
  const [counter, setCounter] = useState(0);

  const increment = () => {
    setCounter(counter + 1);
  };

  console.log('App rendered');
  return (
    <div>
      <h1>Counter: {counter}</h1>
      <Button onClick={increment} />
    </div>
  );
};

Попробовав это приложение, вы удивитесь: несмотря на использование React.memo, кнопка Button будет рендериться заново при каждом изменении счётчика. Почему? Потому что increment, передаваемый как пропс, каждый раз пересоздаётся, и React.memo считает его другим.

Как это исправить: useCallback

Здесь useCallback — ваш спаситель. Он позволяет один раз создать функцию и "вспоминать" её, если зависимости не изменились. Давайте исправим наш код:

import React, { useState, useCallback } from 'react';

const Button = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>Click me</button>;
});

export const App = () => {
  const [counter, setCounter] = useState(0);

  const increment = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]); // Зависи от `counter`

  console.log('App rendered');
  return (
    <div>
      <h1>Counter: {counter}</h1>
      <Button onClick={increment} />
    </div>
  );
};

Теперь, если вы попробуете это снова, кнопка больше не будет рендериться, если её пропс onClick не изменился. Почему? Потому что теперь функция increment сохраняется в памяти и не пересоздаётся при каждом рендере.

Как использовать useMemo: оптимизация вычислений

А теперь обратимся к ситуации, когда у вас есть сложные вычисления, которые не должны выполняться при каждом рендере. Например, пусть у нас есть большая таблица данных, которую необходимо фильтровать:

import React, { useState } from 'react';

const Table = ({ data }: { data: string[] }) => {
  console.log('Table rendered');
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
};

export const App = () => {
  const [query, setQuery] = useState('');
  const [items] = useState([
    'Banana',
    'Apple',
    'Orange',
    'Pineapple',
    'Grapes',
    'Mango',
  ]);

  const filteredItems = items.filter((item) =>
    item.toLowerCase().includes(query.toLowerCase())
  );

  console.log('App rendered');
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search"
      />
      <Table data={filteredItems} />
    </div>
  );
};

Каждый раз, когда пользователь набирает текст, filteredItems создаётся заново, и это приводит к повторному выполнению фильтрации. Это может быть серьёзной проблемой, если данных много. Вот где помогает useMemo:

import React, { useState, useMemo } from 'react';

const Table = ({ data }: { data: string[] }) => {
  console.log('Table rendered');
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
};

export const App = () => {
  const [query, setQuery] = useState('');
  const [items] = useState([
    'Banana',
    'Apple',
    'Orange',
    'Pineapple',
    'Grapes',
    'Mango',
  ]);

  const filteredItems = useMemo(() => {
    return items.filter((item) =>
      item.toLowerCase().includes(query.toLowerCase())
    );
  }, [query, items]); // Выполняется только если изменился `query` или `items`

  console.log('App rendered');
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search"
      />
      <Table data={filteredItems} />
    </div>
  );
};

Теперь фильтрация происходит только тогда, когда действительно необходимо, а не при каждом ререндере.

Типизация useCallback и useMemo

Как и для всего в TypeScript, для useCallback и useMemo важна правильная типизация.

useCallback:

Функция, переданная в useCallback, автоматом подхватывает типы из переданной ей функции. Например:

const increment = useCallback(() => {
  setCounter((prev) => prev + 1);
}, []);

Если нужна явная типизация:

const increment: () => void = useCallback(() => {
  setCounter((prev) => prev + 1);
}, []);

useMemo:

Типизация результата useMemo:

const filteredItems: string[] = useMemo(() => {
  return items.filter((item) =>
    item.toLowerCase().includes(query.toLowerCase())
  );
}, [query, items]);

Главные особенности и важные советы

  1. Не используйте useCallback и useMemo ради использования. Если вы не сталкиваетесь с лишними рендерами или тяжёлыми вычислениями, скорее всего, они вам не нужны.
  2. Будьте аккуратны с зависимостями. Если вы забыли указать что-то в массиве зависимостей, это может привести к багам. TypeScript поможет отловить большинство таких ошибок.
  3. Инструмент профилирования React DevTools. Используйте его, чтобы понять, где приложение действительно "тормозит", прежде чем оптимизировать.

Теперь, когда вы вооружены знаниями про useCallback и useMemo, начните их применять в своём реальном проекте. Эти инструменты гарантированно повысят производительность вашего приложения, если использовать их с умом!

1
Задача
Модуль 3: React, 10 уровень, 8 лекция
Недоступна
Использование useCallback для предотвращения лишних рендеров
Использование useCallback для предотвращения лишних рендеров
1
Задача
Модуль 3: React, 10 уровень, 8 лекция
Недоступна
Оптимизация вычислений с помощью useMemo
Оптимизация вычислений с помощью useMemo
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ