Что такое refs в React и как ими пользоваться

Ошибка “Cannot read properties of null” при обращении к ref.current — один из самых частых симптомов, с которым сталкиваются разработчики, впервые использующие refs в React. Причина почти всегда одна: попытка прочитать ссылку на DOM-элемент до того, как компонент смонтировался, либо отсутствие проверки на null. Понимание того, что такое refs и в какой момент жизненного цикла они заполняются, устраняет эту и многие другие проблемы.

Ref (от англ. reference — «ссылка») — это механизм React для прямого доступа к DOM-узлам и хранения изменяемых значений, которые не вызывают повторный рендер компонента. В отличие от state, изменение ref не запускает перерисовку интерфейса — в этом его ключевая особенность и одновременно главный источник ошибок у новичков.

Зачем нужны refs и чем они отличаются от state

Декларативная модель React предполагает, что интерфейс описывается через состояние, а фреймворк сам синхронизирует DOM. Однако существуют задачи, где требуется императивный доступ к элементу: установить фокус в поле ввода, прокрутить список к нужному пункту, измерить размеры блока или управлять видеоплеером. Именно для таких сценариев созданы refs.

Разница между ref и state принципиальна. Изменение state через setState или сеттер из useState вызывает повторный рендер, и интерфейс обновляется. Запись в ref.current происходит «мимо» системы рендеринга: значение меняется, но React об этом не узнаёт и компонент не перерисовывается. Поэтому ref нельзя использовать для данных, которые должны отображаться на экране — интерфейс просто не обновится.

  • 🎯 Фокус и выделение текста — установка фокуса на input при открытии формы или модального окна.
  • 📏 Измерение DOM — получение координат, ширины и высоты элементов через getBoundingClientRect().
  • ⏱️ Хранение таймеров — сохранение идентификатора setInterval или setTimeout между рендерами.
  • 🎬 Управление медиа — вызов методов play() и pause() у тегов video и audio.
  • 🔗 Интеграция со сторонними библиотеками — передача DOM-узла в плагины, работающие вне React.

Хук useRef: создание и использование

В функциональных компонентах ref создаётся хуком useRef. Он принимает начальное значение и возвращает объект с единственным свойством current, которое можно свободно читать и изменять.

import { useRef, useEffect } from 'react';

function SearchForm() {

const inputRef = useRef(null);

useEffect(() => {

inputRef.current.focus();

}, []);

return <input ref={inputRef} type="text" />;

}

Обратите внимание на порядок действий: ref объявляется, передаётся в атрибут ref JSX-элемента, а обращение к inputRef.current происходит внутри useEffect. Это не случайность — до монтирования компонента current равен null, и попытка вызвать focus() в теле компонента приведёт к ошибке.

⚠️ Внимание: чтение и запись ref.current во время рендера (в теле компонента) делают поведение непредсказуемым, особенно в StrictMode и при конкурентном рендеринге. Безопасные места для работы с ref — обработчики событий и эффекты.

Второе применение useRef — хранение любых изменяемых данных, не связанных с отображением. Классический пример — идентификатор интервала, который нужно остановить позже:

const timerId = useRef(null);

const start = () => {

timerId.current = setInterval(() => tick(), 1000);

};

const stop = () => {

clearInterval(timerId.current);

};

Если бы идентификатор хранился в state, каждый его апдейт вызывал бы лишний рендер. Если бы он хранился в обычной локальной переменной — значение терялось бы при каждой перерисовке компонента. Ref решает обе проблемы.

createRef и refs в классовых компонентах

В классовых компонентах хуки недоступны, поэтому используется API React.createRef(). Ссылка создаётся в конструкторе и точно так же привязывается к элементу через атрибут ref.

class Player extends React.Component {

constructor(props) {

super(props);

this.videoRef = React.createRef();

}

handlePlay = () => {

this.videoRef.current.play();

};

render() {

return <video ref={this.videoRef} src="clip.mp4" />;

}

}

Разница между useRef и createRef тонкая, но важная. createRef при каждом вызове создаёт новый объект, поэтому в функциональных компонентах его использовать нельзя — ссылка пересоздавалась бы на каждом рендере. useRef, напротив, сохраняет один экземпляр между рендерами. В классах это не проблема, поскольку ref создаётся один раз в конструкторе.

📊 Как вы чаще всего используете refs в своих проектах?
Установка фокуса на поля ввода
Хранение таймеров и интервалов
Интеграция со сторонними библиотеками
Измерение размеров и позиций элементов

Сравнение способов создания refs

Соберём основные подходы в одну таблицу, чтобы было проще выбрать подходящий под конкретную задачу.

ПодходГде применяетсяОсобенности
useRefФункциональные компонентыОдин объект на всё время жизни компонента
createRefКлассовые компонентыНовый объект при каждом вызове
Callback refОба типа компонентовФункция получает узел или null, гибкий контроль
forwardRefПередача ref в дочерний компонентПробрасывает ссылку через обёртку
Строковые refsУстаревший кодСчитаются legacy, в новом коде не применяются

Отдельно стоит сказать о callback refs — функции, которая вызывается с DOM-узлом при монтировании и с null при размонтировании. Такой вариант удобен, когда нужно выполнить действие сразу в момент появления элемента, например при динамически меняющемся списке узлов.

<div ref={(node) => {

if (node) node.scrollIntoView();

}} />

forwardRef и доступ к элементам дочерних компонентов

По умолчанию атрибут ref нельзя просто передать кастомному компоненту как обычный проп — React его перехватывает. Чтобы родитель мог получить ссылку на DOM-элемент внутри дочернего компонента, последний оборачивается в forwardRef.

const FancyInput = React.forwardRef((props, ref) => (

<input ref={ref} className="fancy" {...props} />

));

// В родителе:

const inputRef = useRef(null);

<FancyInput ref={inputRef} />

Для случаев, когда родителю нужен не весь DOM-узел, а ограниченный набор методов, существует хук useImperativeHandle. Он позволяет «наружу» отдать только выбранные возможности — например, методы focus() и clear(), скрыв остальное. Это хорошая практика инкапсуляции для библиотечных компонентов.

⚠️ Внимание: не пробрасывайте ref «на всякий случай» во все компоненты подряд. Чем больше мест, где родитель напрямую управляет внутренностями дочернего компонента, тем хрупче архитектура и тем сложнее рефакторинг.
Что изменилось в React 19

В новых версиях React ref можно передавать функциональным компонентам как обычный проп без forwardRef — обёртка постепенно перестаёт быть обязательной. Также появилась поддержка cleanup-функции в callback refs. Проверяйте документацию той версии React, которая используется в вашем проекте.

Типичные ошибки при работе с refs

Большинство проблем с refs сводится к нескольким повторяющимся сценариям. Ниже — чек-лист для самопроверки, который помогает быстро локализовать сбой.

☑️ Диагностика проблем с ref

Выполнено: 0 / 5

Частая причина «пустого» ref — условный рендеринг. Если элемент обёрнут в условие вида {isOpen && <input ref={inputRef} />}, то в момент, когда условие ложно, ссылка равна null. Эффект с пустым массивом зависимостей в этом случае отработает до появления элемента, и фокус не установится. Решение — добавить зависимость от флага видимости или использовать callback ref.

  • 🚫 Чтение ref.current в теле компонента — значение ещё не заполнено или устарело.
  • 🔄 Ожидание рендера после изменения ref — его не будет, используйте state.
  • 🧩 Ref на компонент без forwardRefcurrent останется null.
  • Утечка таймеров — забытый clearInterval в cleanup-функции эффекта.

Когда refs использовать не стоит

Существует соблазн решать через refs всё подряд, потому что императивный стиль привычнее. Однако чрезмерное увлечение ими ломает декларативную модель React и усложняет поддержку кода. Если задачу можно выразить через состояние и пропсы — почти всегда стоит сделать именно так.

Например, скрытие и показ элемента, изменение его классов, управление значением поля ввода — всё это задачи state. Ref здесь создаст «теневое» состояние, о котором React не знает, и при следующем рендере ваши ручные изменения DOM могут быть перезаписаны. Правило простое: ref — инструмент последней инстанции для взаимодействия с DOM, а не замена состоянию.

Часто задаваемые вопросы

Почему ref.current равен null?

Чаще всего обращение происходит до монтирования элемента — например, в теле компонента или когда узел скрыт условным рендером. Перенесите логику в useEffect или обработчик события и добавьте проверку на null.

Вызывает ли изменение ref.current повторный рендер?

Нет. Это принципиальное отличие от state: React не отслеживает записи в ref и не перерисовывает компонент. Если нужно отобразить изменение на экране, используйте state.

Чем useRef отличается от createRef?

useRef сохраняет один и тот же объект между рендерами и предназначен для функциональных компонентов. createRef создаёт новый объект при каждом вызове и применяется в классовых компонентах, где он вызывается один раз в конструкторе.

Можно ли передать ref в собственный компонент?

Да, но компонент нужно обернуть в forwardRef (в актуальных версиях React ref также может передаваться как обычный проп — зависит от версии). Без этого атрибут ref не дойдёт до внутреннего DOM-элемента.

Что хранить в ref, кроме DOM-элементов?

Любые изменяемые значения, не связанные с отображением: идентификаторы таймеров, предыдущие значения пропсов, экземпляры сторонних библиотек, счётчики рендеров для отладки.