Почему useCallback не спасает от ре-рендеров в memo-компонентах
-
Junior’ы часто думают, что useCallback - это волшебная палочка против ре-рендеров. Обернул функцию, и memo-компонент больше не дергается. На деле это всего лишь маскировка: функция стабилизируется, но ребенок все равно рендерится из-за inline-объектов или кривых зависимостей.
Разберем, почему так происходит. Ты увидишь реальные примеры, где useCallback бесполезен, и поймешь, где копать глубже. Это сэкономит часы дебага и избавит от иллюзий про ‘оптимизированный’ код.
Как работает useCallback под капотом
useCallback кэширует функцию, сравнивая зависимости по shallow equal. Если массив deps не изменился - возвращает старую функцию. Звучит круто, но React.memo тоже использует shallow compare для пропсов. Функция стабилизируется, но если рядом inline-объект или массив - пропсы не совпадут, и рендер полетит.
Представь: родительский компонент рендерится из-за setState. Он создает новый объект { onClick, data: [1,2,3] } каждый раз inline. memo-дитя видит новые пропсы - shallow compare проваливается. useCallback на onClick тут как пластырь на перелом: функция старая, но объект новый - ре-рендер неизбежен.
Ключевые проблемы с зависимостями:
- Зависимости мутируют - React не видит изменений в объектах, если ссылка та же.
- Inline-объекты создаются заново при каждом рендере.
- Контекст или пропсы от родителя ломают всю схему.
Ситуация useCallback помогает? Почему рендерится Inline функция Да - Inline объект в пропсах Нет Shallow compare пропсов Мутация deps Нет Ссылка не меняется React.memo без deps Нет Родитель рендерит детей по умолчанию Почему memo-компоненты рендерятся несмотря на useCallback
memo оборачивает компонент, пропуская рендер, если пропсы shallow equal. useCallback стабилизирует только функцию, но не трогает объекты или массивы в пропсах. Родитель дернулся - передал { handler: useCallback(fn), items: } - новый массив каждый раз, memo срабатывает на рендер.
Пример: список пользователей. Родитель имеет state users = [{id:1, name:‘Bob’}]. Передаем в ChildList: props = { users, onDelete: useCallback(deleteUser, [users]) }. users меняется - deps меняются - новая функция. Даже если deps пустые, inline slice() или map создаст новый массив.
Типичные грабли:
- Забытые deps - eslint-plugin-react-hooks ругается зря, функция захватывает замыкания.
- Inline объекты - { id, label: name } вместо стабильного объекта из useMemo.
- Массивы из map без memo - каждый рендер новый массив ссылок.
// Плохо const Child = ({ items, onClick }) => <ul>{items.map(item => <li key={item.id} onClick={() => onClick(item)} />)}</ul>; // items новый массив -> ре-рендерУтечки через неоптимальные зависимости
Зависимости в useCallback - это shallow compare. Объект dep1 = {a:1} не изменится по ссылке, даже если внутри a поменялось. React подумает, что deps stable - вернет старую функцию. Но в замыкании fn захватит старые данные - баги в runtime.
Реальный кейс: фильтр списка. deps = [filterObj], filterObj мутируется setFilter({ …filterObj, value: newVal }). Ссылка та же - useCallback вернет старую fn с устаревшим замыканием. Дитя получит stable fn, но данные черствые.
Как фиксить:
- Хуки для объектов - useMemo для стабильных пропсов.
- Дроби deps на примитивы - id вместо объекта.
- Custom comparer для memo - React.memo с areEqual функцией.
deps Shallow equal? Проблема Примитивы Да - Новый объект Нет Ре-рендер Мутация объекта Да Устаревшее замыкание useMemo объект Да Стабильно Маскировка вместо рефакторинга
useCallback часто юзают как костыль: ‘рендерится - добавим callback’. Вместо фикса архитектуры - state lift или Context - лепят мемо. Производительность маскируется, но бандл растет, дебажить сложнее.
Под капотом: ивент-луп не блокируется, React батчит рендеры, но лишние diff’ы жрут CPU. Junior радуется React DevTools Profiler - ‘зеленые блоки!’ - не зная, что под водой утечки.
Факты начистоту:
- useCallback + memo спасает только shallow пропсы.
- Более 70% ре-рендеров от inline объектов (по бенчмаркам).
- Лучше рефакторить: выноси state в reducer, используй compound components.
Рефакторинг вместо иллюзий оптимизации
useCallback - не панацея, а инструмент в арсенале. Маскирует симптомы, но не лечит: inline-объекты и кривые deps остаются. Подумай о структуре: зачем Child зависит от всего массива? Раздели на маппинг с ключами, stable handlers через id.
Осталось за кадром: custom hooks для батчинга пропсов или Reselect-подобные селекторы. Если копнешь в scheduler - увидишь, почему даже memo не всегда спасает от parent re-renders. В следующий раз разберем, как ивент-луп рвет твои оптимизации.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.