Ошибка “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
Соберём основные подходы в одну таблицу, чтобы было проще выбрать подходящий под конкретную задачу.
| Подход | Где применяется | Особенности |
|---|---|---|
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
Частая причина «пустого» ref — условный рендеринг. Если элемент обёрнут в условие вида {isOpen && <input ref={inputRef} />}, то в момент, когда условие ложно, ссылка равна null. Эффект с пустым массивом зависимостей в этом случае отработает до появления элемента, и фокус не установится. Решение — добавить зависимость от флага видимости или использовать callback ref.
- 🚫 Чтение ref.current в теле компонента — значение ещё не заполнено или устарело.
- 🔄 Ожидание рендера после изменения ref — его не будет, используйте state.
- 🧩 Ref на компонент без forwardRef —
currentостанется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-элементов?
Любые изменяемые значения, не связанные с отображением: идентификаторы таймеров, предыдущие значения пропсов, экземпляры сторонних библиотек, счётчики рендеров для отладки.