useMemo, useCallback, React.memo и ключи в списках
Вопрос-фильтр между junior и middle: не «что делает useMemo», а «когда он реально нужен и что будет без него».
Мемоизация в React — это сохранение ссылки или результата между рендерами. Она нужна не «для скорости вообще», а там, где важна стабильность ссылки или где вычисление действительно дорогое.
Вопрос
«Чем useMemo отличается от useCallback? Зачем нужен React.memo и почему он часто не помогает? Почему нельзя использовать индекс массива как key?»
Что на самом деле проверяет интервьюер
Он ищет инженерную осознанность. Кандидаты делятся на два лагеря: одни не используют мемоизацию вообще, другие оборачивают в useCallback каждую функцию. Оба варианта — признак того, что человек не понимает механику ссылочного равенства.
Развёрнутый ответ
Три инструмента и одна идея
useMemo(fn, deps)— запоминает результат вычисления.useCallback(fn, deps)— запоминает саму функцию. По сути этоuseMemo(() => fn, deps).React.memo(Component)— пропускает повторный рендер компонента, если props поверхностно не изменились.
Общий знаменатель — сравнение ссылок. Посмотрим на нём:
const makeHandler = () => () => console.log('press');
const h1 = makeHandler();
const h2 = makeHandler();
console.log('две одинаковые функции равны?', h1 === h2);
const stable = makeHandler();
console.log('та же самая ссылка равна себе?', stable === stable);
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
console.log('два одинаковых массива равны?', arr1 === arr2);
Вывод:
две одинаковые функции равны? false
та же самая ссылка равна себе? true
два одинаковых массива равны? false
Почему React.memo «не работает»
const Row = React.memo(function Row({ item, onPress }) {
return (
<Pressable onPress={() => onPress(item.id)}>
<Text>{item.title}</Text>
</Pressable>
);
});
function List({ items }) {
// новая функция и новый объект стиля на каждом рендере — memo бесполезен
return items.map((item) => (
<Row key={item.id} item={item} onPress={(id) => open(id)} />
));
}
Чинится стабилизацией ссылок:
const handlePress = useCallback((id) => open(id), [open]);
// ...
<Row key={item.id} item={item} onPress={handlePress} />
Когда мемоизация оправдана
- Вычисление действительно дорогое: сортировка и фильтрация тысяч элементов, разбор большого JSON, группировка.
- Значение уходит в зависимости
useEffectили другого хука — нужна стабильная ссылка. - Значение передаётся как props в
React.memo-компонент, особенно в ячейкуFlatList. - Значение кладётся в
Context(иначе все потребители перерисуются на каждый рендер провайдера).
И когда не оправдана: обычные строки и числа, лёгкие вычисления, компоненты, которые и так рендерятся редко. У мемоизации есть цена — память под кэш и сравнение зависимостей на каждом рендере.
Ключи в списках
key — это идентичность элемента для алгоритма согласования. Индекс массива означает «элемент, стоящий на позиции N», а это совсем другое утверждение. Смоделируем вставку в начало:
const before = ['Молоко', 'Хлеб', 'Сыр'];
const after = ['Яблоки', 'Молоко', 'Хлеб', 'Сыр'];
const byIndex = (list) => list.map((t, i) => i + ':' + t).join(' ');
console.log('было: ', byIndex(before));
console.log('стало:', byIndex(after));
Вывод:
было: 0:Молоко 1:Хлеб 2:Сыр
стало: 0:Яблоки 1:Молоко 2:Хлеб 3:Сыр
С точки зрения React под ключом 0 «остался тот же элемент», просто у него изменился текст. Компонент не пересоздаётся — а значит, его внутреннее состояние (введённый текст, положение чекбокса, запущенная анимация) остаётся у чужой строки. Отсюда классический баг: отметили галочку, добавили элемент сверху — галочка уехала.
// плохо — если список сортируется, фильтруется или в него вставляют элементы
<FlatList data={items} keyExtractor={(item, index) => String(index)} />
// хорошо — стабильный идентификатор из данных
<FlatList data={items} keyExtractor={(item) => item.id} />
Индекс допустим только для полностью статичного списка, который никогда не меняет порядок и не редактируется — и лучше честно сказать это на собеседовании, чем утверждать «индекс нельзя никогда».
Типичные ошибки кандидатов
- Считают, что
useCallbackсам по себе ускоряет приложение. БезReact.memoна принимающей стороне он обычно не даёт ничего. - Оборачивают в
useMemoпримитивы — расходов больше, чем пользы. - Объясняют ключи фразой «чтобы React не ругался в консоли», не понимая связи с состоянием компонента.
- Не упоминают
Context, хотя нестабильное значение в провайдере — одна из самых частых причин лишних рендеров в RN-приложениях.
Как ответить кратко
«useMemo запоминает результат, useCallback — функцию, React.memo отменяет рендер при неизменных props. Все три работают через поверхностное сравнение ссылок, поэтому React.memo бесполезен, если в props приходит новая стрелочная функция или новый объект стиля — их надо стабилизировать. Мемоизация оправдана при дорогих вычислениях и там, где нужна стабильная ссылка: зависимости хуков, props мемоизированных ячеек списка, значение Context. Ключ — это идентичность элемента; индекс означает «позицию», поэтому при вставке или сортировке состояние компонентов съезжает к чужим элементам. Нужен стабильный id из данных.»