useRef возвращает объект { current: initialValue }, который React хранит всё время жизни компонента. Изменение ref.current ничего не перерендеривает. Поэтому рефы подходят для двух задач: хранить узел DOM, чтобы вызывать на нём методы вроде focus(), и запоминать между рендерами значение, которое экрану показывать не нужно.
Передача объекта ref в ref={inputRef} говорит React положить узел DOM <input> в inputRef.current. Замените focus() на select(), напишите что-нибудь и нажмите снова: текст выделится.
Синтаксис
const myRef = useRef(initialValue);
myRef.current; // read
myRef.current = value; // write, no re-render
initialValueиспользуется только при первом рендере. Для рефов на DOM этоnull.- React возвращает один и тот же объект при каждом рендере. Меняется только
current. - Чтение и запись
currentэто обычный JavaScript. React его не отслеживает.
Значение, которое не вызывает рендеры
Состояние нужно для того, что показывает экран. Ref нужен для того, что компоненту нужно помнить, но не показывать. Этот блок хранит один счётчик в ref, а другой в состоянии, и выводит оба:
Нажмите Ref + 1 несколько раз. Консоль показывает, как растёт ref.current, но страница всё ещё показывает 0, и рендер не выводится. Теперь нажмите State + 1: компонент рендерится, и счётчик ref на экране прыгает к числу, которое ref уже хранил. Ref всё время сохранял значение; у React просто не было причины его рисовать.
Ref и состояние
useState | useRef | |
|---|---|---|
| Возвращает | [value, setValue] | { current: value } |
| Изменение | Снова рендерит компонент | Ничего видимого не делает |
| Когда новое значение можно прочитать | В следующем рендере | Сразу |
| Чем менять | Сеттером | Присваиванием, ref.current = x |
| Для чего | Всего, что показывает экран | id таймеров, узлов DOM, предыдущих значений, флагов |
Если сомневаетесь, спросите себя, читает ли значение JSX. Если да, ему место в состоянии.
Хранение id таймера
setInterval возвращает id, который позже нужен, чтобы остановить таймер. Это хорошо подходит для ref: обработчику Stop он нужен, но экран его никогда не показывает, а в обычной переменной он потерялся бы при следующем рендере.
Замените useRef(null) на let intervalRef = { current: null }; и попробуйте Start, подождите две секунды, затем Stop: таймер продолжит идти. Каждый тик рендерит компонент, что создаёт свежий объект, поэтому Stop ничего не очищает.
Запоминание предыдущего значения
Эффект выполняется после рендера, поэтому он может сравнить новое значение с сохранённым в ref, а затем сохранить новое на следующий раз:
Нажмите green, затем blue: консоль выведет changed from red to green и changed from green to blue. Клик по уже выбранному цвету ничего не выводит, потому что эффект выполняется только при изменении color.
Работа с DOM
Рефы позволяют вызывать API браузера, для которых у React нет пропса: focus(), select(), scrollIntoView(), play() у видео или чтение размера через getBoundingClientRect().
const lastItemRef = useRef(null);
function scrollToLast() {
lastItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
<li ref={lastItemRef}>Last item</li>
ref.current равен null во время первого рендера, потому что React ещё не создал узел DOM, и снова после удаления элемента. Читайте его в обработчиках событий и эффектах, где узел существует. Чтобы измерить элемент и расположить что-то до того, как браузер отрисует страницу, читайте ref в useLayoutEffect.
Не читайте и не записывайте ref.current во время рендера
React ожидает, что тело компонента это чистое вычисление: одни и те же пропсы и состояние, один и тот же JSX. Ref меняется вне этой модели, поэтому рендер, который его читает или записывает, может показывать разные результаты при одних и тех же входных данных, а React может вызывать тело чаще, чем вы ожидаете.
function Bad() {
const count = useRef(0);
count.current++; // writing during render
return <p>{count.current}</p>; // reading during render
}
Читайте и записывайте рефы в обработчиках событий и эффектах. Единственное исключение это ленивая инициализация, когда вы заполняете ref один раз при первом рендере:
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer(); // runs once
}
(Демо со счётчиком ref выше намеренно читает clicks.current в JSX, чтобы показать, что экран за ним не следует. Не копируйте это в настоящий код.)
Ref-колбэки
Вместо объекта ref в ref можно передать функцию. React вызывает её с узлом DOM, когда элемент добавляется. В React 19 функция может вернуть очистку, которую React вызывает при удалении элемента. Это удобно для списка, где нельзя вызвать useRef по разу на каждый элемент:
При загрузке консоль выводит по одной строке attached на каждый элемент. Нажмите Remove last, и появится только detached Cheese. trackNode объявлена вне компонента, поэтому при каждом рендере это одна и та же функция, и React не трогает остальные элементы. Если написать её прямо в строке как ref={(node) => { ... }}, каждый клик будет отсоединять и снова присоединять каждый элемент, потому что каждый рендер передаёт новую функцию.
Передача ref в собственный компонент
В React 19 ref это обычный пропс для функциональных компонентов. Родитель создаёт ref, а дочерний компонент ставит его на тот элемент DOM, который хочет открыть:
До React 19 для этого нужен был forwardRef, который по-прежнему работает. Страница о forwardRef показывает обе версии и то, как открыть лишь несколько методов через useImperativeHandle.
Частые ошибки
Показ значения ref в JSX. Экран за ним не следует. Если пользователь должен видеть значение, используйте состояние.
ref.current как зависимость эффекта. [ref.current] читает значение во время рендера, а React не знает, когда оно меняется, поэтому эффект не перезапускается при изменении. Чтобы реагировать на присоединение узла, используйте ref-колбэк.
Чтение DOM-ref во время первого рендера. Он равен null, пока React не создал узел. Читайте его в эффекте или обработчике.
Создание нового объекта вместо хука. const ref = { current: null } в теле компонента это свежий объект при каждом рендере. Только useRef каждый раз даёт один и тот же.
Часто задаваемые вопросы
Что возвращает useRef?
Обычный объект с одним свойством, current, равным переданному начальному значению. React возвращает один и тот же объект при каждом рендере, поэтому всё, что вы положили в current, в следующий раз будет на месте.
Чем useRef отличается от useState?
Изменение состояния снова рендерит компонент; изменение ref.current нет. Используйте состояние для значений, которые показывает экран, а ref для значений, которые нужны только обработчикам событий и эффектам, например id таймера или узла DOM.
Почему ref.current равен null?
React записывает узел DOM в ref.current после того, как создаёт узел, во время фиксации (commit). Во время первого рендера он ещё null, и снова становится null после удаления элемента. Читайте его в обработчике события или эффекте.
Вызывает ли изменение ref.current повторный рендер?
Нет. React не знает, что вы его изменили. Новое значение появится на экране, только если что-то другое, например обновление состояния, снова отрендерит компонент.
Как передать ref в дочерний компонент?
В React 19 функциональный компонент получает ref как обычный пропс, поэтому вы передаёте <MyInput ref={inputRef} />, а дочерний компонент ставит его на свой <input>. В старых версиях нужен был forwardRef.