← Назад к вопросам

Как useMemo сверяет пропсы?

1.7 Middle🔥 241 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как useMemo сверяет зависимости

Это важный вопрос для понимания производительности React. useMemo не сверяет пропсы — он сверяет dependency array.

Основной механизм

const memoizedValue = useMemo(
  () => expensiveComputation(a, b),
  [a, b] // Dependency array
);

useMemo сравнивает значения в dependency array с предыдущей итерацией, используя Object.is().

Object.is() - это не ===

// Object.is() vs ===

Object.is(NaN, NaN); // true (в отличие от NaN === NaN)
Object.is(0, -0); // false (в отличие от 0 === -0)
Object.is('hello', 'hello'); // true
Object.is({}, {}); // false (разные объекты)

Для примитивных типов Object.is() ведёт себя почти как ===, но для объектов и функций — это сравнение по ссылке.

Практический пример: когда useMemo помогает

function Parent() {
  const [count, setCount] = useState(0);
  
  // Проблема: новый объект создаётся при каждом рендере
  const userConfig = { name: 'John', age: 30 };
  
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child config={userConfig} />
    </>
  );
}

function Child({ config }) {
  console.log('Child rendered with config:', config);
  return <div>{config.name}</div>;
}

// Результат: Child рендерится при каждом клике на кнопку
// Потому что userConfig — новый объект каждый раз

Решение с useMemo:

function Parent() {
  const [count, setCount] = useState(0);
  
  // useMemo создаёт объект один раз
  const userConfig = useMemo(
    () => ({ name: 'John', age: 30 }),
    [] // Пусто = создать один раз при монтировании
  );
  
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child config={userConfig} />
    </>
  );
}

// Теперь Child не рендерится при каждом клике
// Потому что userConfig остаётся тот же объект

Как React сравнивает зависимости

// Упрощённая логика React

function useMemo(fn, dependencies) {
  const [prevDeps, setPrevDeps] = useState(null);
  const [memoizedValue, setMemoizedValue] = useState(null);
  
  // Проверить, изменились ли зависимости
  if (!prevDeps || hasDepenciesChanged(prevDeps, dependencies)) {
    // Пересчитать значение
    setMemoizedValue(fn());
    setPrevDeps(dependencies);
  }
  
  return memoizedValue;
}

function hasDepenciesChanged(prevDeps, currentDeps) {
  // Сравнить каждую зависимость
  for (let i = 0; i < prevDeps.length; i++) {
    if (!Object.is(prevDeps[i], currentDeps[i])) {
      return true; // Зависимость изменилась
    }
  }
  return false; // Ничего не изменилось
}

Примеры сравнения зависимостей

function Example() {
  const [count, setCount] = useState(0);
  
  // Пример 1: Примитивные типы
  const value1 = useMemo(() => count * 2, [count]);
  // Пересчитывается когда count меняется
  
  // Пример 2: Объекты (опасно!)
  const value2 = useMemo(
    () => ({ count }), // Новый объект каждый раз
    [count] // Это примитив, всё OK
  );
  
  // Пример 3: Объект в зависимости (проблема!)
  const config = { count }; // Новый объект каждый раз
  const value3 = useMemo(
    () => expensiveCalc(config),
    [config] // Object.is(prevConfig, config) = false
  );
  // Итог: useMemo не мемоизирует! config меняется каждый раз
  
  // Решение: мемоизировать config
  const memoConfig = useMemo(() => ({ count }), [count]);
  const value4 = useMemo(
    () => expensiveCalc(memoConfig),
    [memoConfig]
  );
}

Сравнение с React.memo и useCallback

// useMemo - мемоизирует ЗНАЧЕНИЕ
const result = useMemo(() => heavyComputation(), [dependency]);

// useCallback - мемоизирует ФУНКЦИЮ
const handleClick = useCallback(() => {
  console.log('clicked');
}, [dependency]);

// React.memo - мемоизирует КОМПОНЕНТ (сравнивает пропсы)
const MemoComponent = React.memo(({ prop }) => {
  return <div>{prop}</div>;
});

// Все используют Object.is() для сравнения!

Практический пример: список фильтров

function FilteredList({ items, filterBy }) {
  // Проблема: новый отфильтрованный массив создаётся каждый раз
  const filtered = items.filter(item => item.type === filterBy);
  
  return (
    <List data={filtered} />
  );
}

// Решение с useMemo
function FilteredList({ items, filterBy }) {
  const filtered = useMemo(
    () => items.filter(item => item.type === filterBy),
    [items, filterBy] // Пересчитать если items или filterBy изменились
  );
  
  return (
    <List data={filtered} />
  );
}

// Теперь List не рендерится если items и filterBy не изменились

Частая ошибка: неправильная зависимость

// Плохо: зависимость — объект, который меняется каждый раз
function Parent({ userData }) {
  const memoUser = useMemo(
    () => processUser(userData),
    [userData] // userData — новый объект каждый раз?
  );
  return <Child user={memoUser} />;
}

// Хорошо: зависимость — примитивное значение
function Parent({ userId }) {
  const memoUser = useMemo(
    () => processUser(userId),
    [userId] // userId — string или number
  );
  return <Child user={memoUser} />;
}

Правило: когда использовать useMemo

// Используй useMemo если:
// 1. Вычисление дорогое
const sorted = useMemo(
  () => expensiveSort(largeArray),
  [largeArray]
);

// 2. Результат передаётся дочернему компоненту с React.memo
const memoConfig = useMemo(() => ({}), []);
return <MemoChild config={memoConfig} />;

// 3. Результат — зависимость для другого хука
const memoArray = useMemo(() => [a, b], [a, b]);
const effect = useEffect(() => {}, [memoArray]);

// НЕ используй useMemo если:
// 1. Вычисление простое
const doubled = useMemo(() => count * 2, [count]); // Не нужно

// 2. Результат не используется в зависимостях
const user = useMemo(() => fetchUser(id), [id]);
// Если user не передаётся в React.memo компонент

Механизм под капотом

// React хранит для каждого useMemo:
// 1. Предыдущие зависимости [prevDeps]
// 2. Мемоизированное значение [memoizedValue]
// 3. Функцию, создающую значение [fn]

// При рендере:
// if (Object.is(prevDeps[0], currentDeps[0]) &&
//     Object.is(prevDeps[1], currentDeps[1]) && ...)
//   return memoizedValue; // Вернуть старое значение
// else
//   memoizedValue = fn(); // Пересчитать
//   prevDeps = currentDeps;
//   return memoizedValue;

Заключение

useMemo сверяет каждый элемент массива зависимостей с помощью Object.is(). Для примитивов (number, string, boolean) это работает интуитивно. Для объектов и массивов — сравнение по ссылке. Это часто причина неожиданного поведения, когда useMemo не мемоизирует.

Как useMemo сверяет пропсы? | PrepBro