useRef: коробка, которая переживает рендеры
useRef(initial) возвращает объект { current: initial }. Один и тот же объект на всё время жизни компонента. Менять ref.current можно напрямую, и это не вызывает рендер.
const timerRef = useRef(null);
function start() {
timerRef.current = setInterval(tick, 1000);
}
function stop() {
clearInterval(timerRef.current);
}
Когда что:
- Значение влияет на то, что видно на экране: state.
- Значение нужно только обработчикам и эффектам (id таймера, предыдущее значение, флаг, экземпляр сторонней библиотеки): ref.
- Обычная переменная
let xв теле компонента пересоздаётся на каждом рендере и для этого не подходит.
Не читайте и не пишите ref.current во время рендера (кроме ленивой инициализации): React не узнает об изменении, и результат рендера станет непредсказуемым.
Ссылка на DOM-узел
const inputRef = useRef(null);
<input ref={inputRef} />
// в обработчике или эффекте:
inputRef.current.focus();
React записывает узел в current во время коммита и возвращает null при удалении узла. В теле компонента на первом рендере inputRef.current ещё null.
Отсюда частая ошибка: в обработчике сделали setShow(true) и сразу вызвали inputRef.current.focus(). Узла ещё нет, он появится только после рендера. Решения: фокус в эффекте, autoFocus, ref-колбэк или flushSync перед обращением.
Ref-колбэк
Вместо объекта можно передать функцию. React вызовет её с узлом, когда он появится. В React 19 колбэк может вернуть функцию очистки, она вызовется при отключении ref:
<li ref={node => {
map.set(item.id, node);
return () => map.delete(item.id);
}} />
Так удобно хранить ссылки на элементы списка: хуки нельзя вызывать в цикле, поэтому держат одну Map в ref и заполняют её колбэками.
ref в свой компонент: React 19
В React 19 функциональный компонент получает ref как обычный проп:
function TextInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
<TextInput ref={inputRef} />
Раньше для этого была нужна обёртка forwardRef. Она всё ещё работает, но в новом коде не нужна.
useImperativeHandle
Если родителю нужно не весь DOM-узел, а ограниченный набор действий, отдайте свой объект:
function VideoPlayer({ ref, src }) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current.play(),
pause: () => videoRef.current.pause(),
}), []);
return <video ref={videoRef} src={src} />;
}
Императивный API оправдан для фокуса, прокрутки, медиа, анимаций. То, что можно выразить через props (isOpen), лучше выражать через props.
Частые ошибки
- Хранят в ref то, что должно отображаться, и удивляются, что экран не обновился.
- Обращаются к DOM-ref во время рендера и получают
null. - Вручную удаляют или переставляют DOM-узлы, которыми управляет React: при следующем обновлении React упадёт или испортит разметку.
Что спросят на собесе
- Чем
useRefотличается отuseStateи от обычной переменной. - Почему изменение
ref.currentне вызывает рендер. - Как передать ref в свой компонент в React 19 и как было раньше.
- Зачем нужен
useImperativeHandle. - Как получить ссылки на все элементы списка.