Какие особенности зависимостей useCallback важно учитывать на практике и почему?
Хуки
Зависимости useCallback определяют, когда функция пересоздаётся. Ошибки здесь дают либо бесполезную мемоизацию, либо баги с устаревшими данными.
Полнота обязательна
Всё реактивное, что использует функция (props, state, другие функции компонента), должно быть в deps:
const buy = useCallback(() => {
order(productId, qty);
}, [productId]); // ❌ qty пропущен → всегда старое количествоСсылка будет стабильной, но замыкание — устаревшим (stale closure). ESLint exhaustive-deps ловит; подавлять — значит прятать баг.
Нестабильные зависимости обнуляют смысл
const options = { currency: 'RUB' }; // новый объект каждый рендер
const buy = useCallback(() => order(options), [options]); // новая функция каждый рендерЗависимости сами должны быть стабильны: примитивы, useMemo-объекты, стабильные колбэки. Иначе useCallback лишь добавляет накладные расходы.
Что можно не указывать
Сеттеры useState, dispatch из useReducer, ref-объект, значения из модульной области — они стабильны по определению.
Как честно сократить deps
const increment = useCallback(() => setCount((c) => c + 1), []); // без countФункциональный сеттер убирает state из зависимостей. Для «свежего» значения без пересоздания функции — ref (latestRef.current), обновляемый в эффекте.
Смысл зависимостей — не только оптимизация
deps задают, как долго живёт ссылка: от этого зависят пропуски рендеров у memo-детей и перезапуски эффектов, где функция стоит в deps. Слишком широкие deps — мемоизация не работает; слишком узкие — работает с неверными данными.
Коротко для собеседования
Зависимости useCallback должны быть полными (иначе stale closure) и стабильными (иначе функция пересоздаётся каждый рендер и мемоизация бессмысленна). Сеттеры/dispatch/ref указывать не нужно; сокращать deps правильно через функциональные сеттеры или ref, а не отключением линтера.