Вчера снова наступил на грабли: компонент с анимацией дергал DOM-элемент при каждом обновлении пропсов, хотя визуально ничего не менялось. Вот тогда и вспомнил про useRef — хуковый аналог createRef, который не триггерит ререндеры при изменении своего .current.
Как работает useRef под капотом
useRef возвращает обычный JS-объект с единственным свойством current. Магия в том, что этот объект сохраняется между рендерами компонента:
const intervalRef = useRef(null); // { current: null }
// Где-то в эффекте
intervalRef.current = setInterval(() => {...}, 1000);
// При следующем рендере intervalRef.current сохранит значение
Важное правило: не трогайте ref.current во время рендера. React ожидает, что рендеры остаются чистыми функциями. Вместо этого работайте с рефами в useEffect, обработчиках событий или других side-effect-областях.
Три реальных сценария из моей практики
1. Работа с DOM-нодами
Самый частый кейс — когда нужно напрямую взаимодействовать с элементом:
const inputRef = useRef(null);
const focusInput = () => inputRef.current?.focus();
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
Здесь реф — единственно правильный способ, потому что:
querySelectorломает React-модель- State для фокуса — overengineering (вызовет лишний ререндер)
2. Хранение мутабельных значений
Бывают значения, которые должны сохраняться между рендерами, но не влияют на отрисовку. Например, ID таймера:
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setTimeout(...);
return () => clearTimeout(timerRef.current);
}, []);
Если бы хранили в state — при очистке таймера получили бы ненужный ререндер.
3. Сравнение предыдущих пропсов
Иногда нужно понять, изменилось ли конкретное значение:
const prevValueRef = useRef();
useEffect(() => {
if (prevValueRef.current !== someProp) {
// Логика при изменении
}
prevValueRef.current = someProp;
}, [someProp]);
Где useRef может подвести
-
Несинхронные обновления: изменение
ref.currentне вызывает мгновенного обновления компонента. Если нужно отреагировать на изменение — сочетайте с state. -
Утечки памяти: объект рефа живет до unmount компонента. Если храните тяжелые ресурсы (WebSocket, большие объекты) — не забывайте очищать в
useEffectcleanup. -
Тестирование: мокировать рефы в тестах сложнее, чем пропсы. Для тестируемых компонентов иногда лучше выносить логику в кастомные хуки.
Альтернативы и похожие инструменты
- useState: когда изменение должно триггерить ререндер
- useMemo: для дорогих вычислений, но не для мутабельных значений
- Context: если данные нужны глубоко вложенным компонентам
- Zustand/Jotai: для глобального состояния вне компонентов
На практике я часто комбинирую useRef с useEffect для управления сторонними библиотеками (карты, графики) или сложными таймаутами. Главное — помнить, что это именно escape hatch, а не замена state-менеджменту.
Если ваш компонент начал обрастать кучей useRef — возможно, пришло время вынести логику в кастомный хук или пересмотреть архитектуру. Как и с любым мощным инструментом, важно чувствовать момент, когда его применение из бенефита превращается в технический долг.