Урок по треку React 6 мин

useRef, DOM и ref как проп в React 19

useRef, доступ к DOM, ref как проп в React 19 и useImperativeHandle.

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.
  • Как получить ссылки на все элементы списка.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.