React

useRef и ref как prop

Форма входа должна сама поставить курсор в поле email при открытии. Видеоплеер должен запуститься по клику на кнопку. Таймер должен помнить свой id, чтобы его можно было сбросить. Ни одно из этих значений не описывает то, что видно на экране, и ни одно не должно вызывать повторный рендер. Для таких случаев в React есть отдельная ячейка памяти, живущая рядом с состоянием, но по другим правилам - useRef.

  • Автофокус: поставить курсор в первое поле формы или в строку поиска сразу при появлении - ref на input и .focus()
  • Медиа: запуск, пауза и перемотка video или audio через прямой вызов методов DOM-элемента
  • Скролл: программная прокрутка к определённому сообщению в чате или к началу списка через element.scrollIntoView()
  • Таймеры и анимации: хранение id из setInterval или requestAnimationFrame, чтобы потом корректно их отменить
  • Интеграции: монтирование чужих библиотек (карта, график, редактор кода) в конкретный DOM-узел, на который указывает ref

Предварительные знания

  • Модель рендера: компонент вызывается заново, и его локальные переменные создаются при каждом рендере с нуля
  • useState и понимание того, что изменение состояния вызывает повторный рендер
  • Базовые операции с DOM: focus(), scrollIntoView(), доступ к элементу

ref - изменяемая ячейка вне рендера

При каждом рендере React заново вызывает функцию-компонент, и все её локальные переменные создаются с нуля. Обычная let-переменная не переживёт следующий рендер - её значение потеряется. useRef решает эту задачу: он возвращает один и тот же объект { current } на протяжении всей жизни компонента. В этот объект можно писать, читать из него, и значение сохранится между рендерами. Но запись в ref.current не запускает повторный рендер.

Здесь intervalRef хранит id таймера. Если бы это была обычная переменная, при следующем рендере (из-за setSeconds) она бы создалась заново и id потерялся - остановить таймер стало бы нечем. Если бы это был useState, каждое сохранение id вызывало бы лишний рендер без всякой нужды. ref - ровно та середина: значение живёт, но не влияет на отрисовку.

  • useState — Описывает то, что видно на экране. Изменяется только через сеттер. Каждое изменение вызывает повторный рендер. Новое значение видно только в следующем рендере
  • useRef — Хранит данные вне отрисовки. Меняется прямой записью в .current. Изменение не вызывает рендера. Новое значение видно сразу же в текущем коде

Не читать и не писать ref.current во время рендера для значений, влияющих на вывод. Рендер должен быть чистым, а ref - это изменяемое состояние вне его контроля. Если значение влияет на то, что отрисовано, ему место в useState. ref - для данных, которые рендеру знать не нужно: id таймеров, ссылки на DOM, кеш.

Чем useRef принципиально отличается от useState?

ref на DOM-узел

Второе применение ref - доступ к настоящему DOM-узлу, которым обычно управляет React. Если передать объект ref в атрибут ref JSX-элемента, после коммита React запишет в ref.current ссылку на созданный DOM-элемент. Это нужно для императивных действий, которых нет в декларативной модели: поставить фокус, измерить размер, прокрутить, запустить медиа.

Доступ к узлу через ref безопасен только после коммита - когда React уже создал и вставил элемент в DOM. Поэтому работу с DOM-узлом делают в эффекте или в обработчике события, а не в теле компонента во время рендера. На первом рендере, до коммита, inputRef.current ещё равен null, поэтому опциональная цепочка ?. здесь не лишняя.

ref на DOM - это запасной выход из декларативной модели, а не основной инструмент. Большинство задач решаются через состояние и props: показать или скрыть, подсветить, изменить класс. К DOM-ref обращаются только за тем, что React не выражает декларативно: фокус, прокрутка, выделение текста, размеры, воспроизведение медиа.

Когда ref.current гарантированно указывает на DOM-узел?

ref как обычный prop в React 19

Раньше ref был особенным: передать его в свой компонент напрямую было нельзя, потому что ref не считался частью props. Чтобы пробросить ref до внутреннего DOM-узла, компонент оборачивали в forwardRef. В React 19 это упрощено: ref передаётся как обычный prop. Функциональный компонент может объявить ref в списке своих props и пробросить дальше без всякой обёртки.

Иногда наружу не хотят отдавать сам DOM-узел, а хотят дать родителю только ограниченный набор действий: например, focus() и scrollToTop(), но не весь элемент. Для этого есть useImperativeHandle - он настраивает, какой объект окажется в ref.current у родителя. Это сужает интерфейс и не даёт родителю напрямую трогать внутренний DOM.

useImperativeHandle стоит держать редким инструментом. Императивный интерфейс между компонентами усложняет поток данных, который в React по умолчанию декларативный и однонаправленный. Он оправдан для focus, scroll, воспроизведения медиа - действий, которые невозможно выразить через props. Для остального лучше props и состояние.

Что изменилось в работе с ref на свои компоненты в React 19?

Связь с другими темами

Урок про ячейку памяти вне рендера и доступ к DOM. Рядом:

  • Модель рендера — Объясняет, почему обычная переменная сбрасывается между рендерами, а ref и state - нет
  • useState — Та же идея устойчивой ячейки, но изменение state вызывает рендер, а изменение ref - нет
  • useEffect — DOM-узел доступен через ref только после коммита, поэтому работу с ним делают в эффекте

Итог

  • useRef возвращает объект { current }, который переживает все рендеры компонента и остаётся той же ссылкой
  • Изменение ref.current не вызывает повторного рендера - это главное отличие от useState
  • ref используют для двух задач: хранить изменяемое значение вне потока рендера (id таймера, предыдущее значение) и держать ссылку на DOM-узел
  • В React 19 ref передаётся как обычный prop: forwardRef больше не нужен, функциональный компонент может принять ref напрямую
  • useImperativeHandle настраивает, что именно увидит родитель через ref на компонент - вместо самого DOM-узла отдают ограниченный набор методов

Связанные уроки

  • rc-10-render-mental-model — Чтобы понять, чем ref отличается от state, нужна модель рендера: что именно вызывает повторный рендер, а что нет
  • rc-06-usestate — useRef и useState - две ячейки, переживающие рендер; разница в том, что изменение ref не перерисовывает, а изменение state перерисовывает
  • rc-11-useeffect — Работа с DOM через ref обычно происходит в эффекте, после того как React закоммитил узел
useRef и ref как prop

0

1

Войти