Мы уже изучили множество концепций, но сегодня нас ждет особенный инструмент, хук useRef. Он не такой «громкий», как useState или useEffect, но в умелых руках творит настоящие чудеса.
Готовься узнать, как напрямую «общаться» с DOM-элементами и хранить данные между рендерами, не вызывая при этом повторный рендер.
Зачем нужен useRef ?
Представь себе ситуацию: тебе нужно программно установить фокус на поле ввода, проскроллить страницу до определенного элемента или измерить его размеры. Как это сделать в React? С помощью пропсов и состояния? Не всегда удобно, а иногда и вовсе невозможно. Именно для таких сценариев и был создан хук useRef.
По своей сути, useRef это функция, которая возвращает нам мутируемый (изменяемый) JavaScript-объект, у которого есть одно свойство current. В этот current мы можем записать что угодно: ссылку на DOM-узел, значение примитивного типа, массив, объект, даже функцию. Самое главное, что этот объект сохраняется между рендерами нашего компонента. В отличие от состояния, созданного через useState, обновление ref не заставляет компонент перерисовываться. Он живет своей жизнью, тихо и спокойно, в памяти.
Можно провести такую аналогию: если состояние (state) это официальная, публичная память компонента, которая при изменении вызывает его «перерождение» (рендер), то ref это его личный, секретный блокнот. Компонент может в него заглянуть, что-то записать, стереть и никто об этом не узнает, потому что внешне (в интерфейсе) ничего не изменится. Мы будем рассматривать два основных применения этого «секретного блокнота»: прямой доступ к DOM-элементам и хранение мутируемых значений, не влияющих на рендер.
Прямой доступ к DOM-элементам
Это, пожалуй, самая частая и очевидная причина для использования useRef. React использует виртуальный DOM и декларативный подход: мы описываем, как интерфейс должен выглядеть в зависимости от состояния. Мы не командуем браузеру: «Эй, найди мне этот input и дай ему фокус!». Вместо этого мы говорим: «Интерфейс должен отображать поле ввода и если значение isFocused равно true, то этот input должен быть в фокусе».
Но иногда «императивный» подход, прямой приказ DOM-элементу, проще и эффективнее. Например, для управления фокусом, выделения текста или интеграции со сторонними библиотеками (например, для графиков или карт), которым нужно передать непосредственно DOM-узел.
Как это работает?
-
Мы создаем
ref-объект с помощью хукаuseRef(). -
Мы «прикрепляем» этот объект к JSX-элементу с помощью специального атрибута
ref. -
После того как компонент отрендерился и React создал реальный DOM-узел, он автоматически запишет ссылку на этот узел в свойство
ref.current.
Теперь у нас в руках прямая ссылка на живой DOM-элемент и мы можем делать с ним все, что разрешает браузерный JavaScript.
Практический пример: фокус на поле ввода
Давай создадим компонент, который при нажатии на кнопку автоматически переводит фокус на поле ввода. Это классическая задача.
import React, { useRef } from 'react'; function FocusInput() { // 1. Создаем ref-объект. Изначально его current равен null. const inputRef = useRef(null); const handleClick = () => { // 3. В обработчике получаем доступ к DOM-элементу через inputRef.current const inputElement = inputRef.current; // Проверяем, что элемент существует (на всякий случай) if (inputElement) { // Императивная команда браузеру: установить фокус на input! inputElement.focus(); // Можем также сделать что-то еще, например, выделить текст // inputElement.select(); } }; return ( <div> {/* 2. "Прикрепляем" наш inputRef к тегу input с помощью атрибута ref */} <input ref={inputRef} type="text" placeholder="Нажми на кнопку, чтобы я получил фокус!" /> <button onClick={handleClick}>Установить фокус на поле</button> </div> ); } export default FocusInput;
Давай разберем по шагам, что здесь происходит:
-
const inputRef = useRef(null);. Внутри компонента мы создаем наш «секретный блокнот»ref-объект. Мы инициализируем его значениемnull, потому что изначально, до рендера, DOM-элемента еще не существует. -
<input ref={inputRef} ... />. В JSX мы передаем этот объект в атрибутrefтегаinput. Это указание для React: «Как только этот элемент будет создан в реальном DOM, положи ссылку на него в свойствоinputRef.current». -
inputRef.current.focus();. Когда пользователь нажимает на кнопку, вызываетсяhandleClick. Внутри него мы обращаемся кinputRef.currentи вот он, наш настоящий DOM-элементinput! Дальше мы просто вызываем его нативный метод.focus().
Обрати внимание. Мы не используем здесь никакое состояние. Компонент не перерендеривается когда ref обновляется. Все происходит мгновенно и «за кадром».
Хранение мутируемых значений между рендерами
Вторая, не менее важная суперспособность useRef, это возможность хранить любые изменяемые данные, которые не должны вызывать повторный рендер компонента при их обновлении.
«Погоди, скажешь ты, но для хранения данных у нас же есть useState и переменные внутри компонента!» Да, но у них есть нюансы.
-
useState. При каждом изменении состояния компонент перерисовывается. Если мы храним в состоянии значение, которое используется только в логике (например, ID таймера, индекс массива для цикла, кешированные данные), то его обновление будет вызывать лишний и дорогой рендер. -
Переменные внутри компонента. Они инициализируются заново при каждом рендере! У них нет постоянства.
А вот useRef дает нам «коробку», в которой данные сохраняются на протяжении всей жизни компонента и при этом обновление содержимого этой коробки не трогает рендеры.
Практический пример: счетчик рендеров и хранение предыдущего значения
Давай создадим компонент, который показывает текущее значение из useState и подсвечивает, изменилось ли оно по сравнению с предыдущим рендером. А заодно мы посчитаем, сколько раз компонент уже отрендерился.
import React, { useState, useRef, useEffect } from 'react'; function CounterWithHistory() { const [count, setCount] = useState(0); // 1. Ref для хранения счетчика рендеров const renderCount = useRef(0); // 2. Ref для хранения предыдущего значения count const prevCount = useRef(null); // useEffect, который срабатывает после каждого рендера useEffect(() => { // 3. Увеличиваем счетчик рендеров. Это НЕ вызовет новый рендер! renderCount.current = renderCount.current + 1; console.log(`Компонент отрендерился ${renderCount.current} раз`); }); // Нет массива зависимостей - срабатывает после каждого рендера // Другой useEffect для отслеживания предыдущего значения useEffect(() => { // 4. Обновляем ref с предыдущим значением prevCount.current = count; }, [count]); // Сработает только когда изменится count const increment = () => { setCount(count + 1); }; return ( <div> <h2>Текущий счет: {count}</h2> {/* 5. Отображаем предыдущее значение из ref */} <p>Предыдущий счет: {prevCount.current ?? 'еще не было'}</p> {/* 6. Отображаем количество рендеров */} <p>Компонент обновлялся: {renderCount.current} раз</p> <button onClick={increment}>+1</button> </div> ); } export default CounterWithHistory;
Давай проанализируем логику:
-
const renderCount = useRef(0);. Мы создаемrefдля хранения количества рендеров. Начальное значение 0. -
const prevCount = useRef(null);. Создаем еще одинrefдля хранения предыдущего значенияcount. -
В
useEffectбез зависимостей (который запускается после каждого рендера) мы обновляемrenderCount.current. Ключевой момент: это обновлениеrefне вызовет новый рендер, иначе мы бы попали в бесконечный цикл (рендер -> useEffect -> обновление ref -> рендер…). Этого не происходит! Счетчик тико обновляется в памяти. -
Во втором
useEffect, который зависит от[count], мы записываем текущее значениеcountвprevCount.current. Обрати внимание: в момент этогоuseEffectзначениеprevCount.currentвсе еще содержит значениеcountс предыдущего рендера. Потому что этотuseEffectзапускается после того, как компонент уже отобразил актуальноеcount. -
В JSX мы отображаем
prevCount.current. Когдаcountменяется, компонент рендерится заново и в этот моментprevCount.currentуже содержит старое значение, которое мы и показываем.
Таким образом можно хранить не только предыдущие значения состояния, но и любые другие «вспомогательные» данные, которые нужны для логики, но не должны «светиться» в интерфейсе и управлять им.
useRef или useState: когда что использовать?
Давай четко разграничим эти два инструмента, чтобы у тебя не было каши в голове.
| Признак | useState |
useRef |
|---|---|---|
| Триггерит рендер | Да. При обновлении значения компонент перерисовывается. | Нет. Обновление .current происходит без перерисовки. |
| Изменяемость | Иммутабельное обновление через функцию-сеттер (setValue(newValue)). |
Мутабельное обновление через прямое присвоение (ref.current = newValue). |
| Доступ к значению | Через саму переменную состояния (value). |
Через свойство .current (ref.current). |
| Использование | Для хранения данных, которые напрямую влияют на то, что видит пользователь (UI). | 1. Прямой доступ к DOM. 2. Хранение значений, которые не должны влиять на UI (таймеры, инстансы классов, кеш). |
| Чтение во время рендера | Можно и нужно. | Можно, но не следует изменять. Изменение ref во время рендера может сделать компонент непредсказуемым. Меняйте ref только в обработчиках событий или эффектах. |
Если данные должны отображаться в JSX или их изменение должно немедленно привести к обновлению интерфейса используй useState. Во всех остальных случаях, когда тебе нужно просто «запомнить» что-то между рендерами, не трогая UI, смело бери useRef.
Важные предупреждения и лучшие практики
Работа с useRef дает большую силу, но и требует ответственности.
-
Не изменяй
ref.currentво время рендера. Рендер должен быть «чистой» функцией, он не должен иметь побочных эффектов. Если ты будешь менятьrefв теле компонента, твой код станет сложным для понимания и отладки. Все измененияrefделай внутриuseEffect,useCallbackили обработчиков событий (например,onClick).Плохо:
function MyComponent() { const myRef = useRef(0); myRef.current = 123; // Побочный эффект прямо во время рендера! Так делать не стоит. return <div>...</div>; }
Хорошо:
function MyComponent() { const myRef = useRef(0); useEffect(() => { myRef.current = 123; // Изменение в эффекте - безопасно. }); const handleClick = () => { myRef.current = 456; // Изменение в обработчике события - безопасно. }; return <div>...</div>; }
-
refэто не состояние. Помни об этом. Если ты используешьrefдля хранения данных, которые все-таки влияют на то, что ты видишь на экране, ты идешь по кривой дорожке. Интерфейс не будет обновляться синхронно с изменениемref. -
React не уведомляет о изменении
ref. Поскольку обновлениеrefне вызывает рендер, у React нет встроенного способа «отреагировать» на это изменение. Если тебе нужна реакция на изменение какого-либо значения, это почти всегда случай дляuseState.
Практические задачи для закрепления
Чтобы знания улеглись намертво, давай попрактикуемся. Попробуй решить эти задачи самостоятельно, прежде чем смотреть на подсказки.
Задача 1: Плейер для видео
Создай компонент видеоплеера с двумя кнопками: «Play» и «Pause». Используй useRef для получения прямого доступа к DOM-элементу <video>, чтобы управлять воспроизведением.
Подсказка:
-
Используй
useRefи атрибутrefна теге<video>. -
У DOM-элемента
<video>есть методы.play()и.pause().
Решение задачи 1:
import React, { useRef } from 'react'; function VideoPlayer() { const videoRef = useRef(null); const handlePlay = () => { videoRef.current?.play(); }; const handlePause = () => { videoRef.current?.pause(); }; return ( <div> <video ref={videoRef} width="600" controls src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" // Используй любое тестовое видео type="video/mp4" > Ваш браузер не поддерживает видео тег. </video> <br /> <button onClick={handlePlay}>Play</button> <button onClick={handlePause}>Pause</button> </div> ); } export default VideoPlayer;
Задача 2: Секундомер
Создай простой секундомер, который показывает прошедшее время в секундах. Должны быть кнопки «Старт», «Стоп» и «Сброс». Используй useRef для хранения ID интервала, чтобы его можно было корректно очистить.
Подсказка:
-
Время (в секундах) храни в состоянии (
useState), так как оно должно отображаться. -
ID таймера из
setIntervalхрани вuseRef. Почему? Потому что если бы он был в состоянии, его обновление вызывало бы лишний рендер. -
Не забудь очищать интервал в
useEffectпри размонтировании компонента.
Решение задачи 2:
import React, { useState, useRef, useEffect } from 'react'; function Stopwatch() { const [time, setTime] = useState(0); const intervalRef = useRef(null); // Храним ID интервала const start = () => { // Если секундомер уже запущен, выходим if (intervalRef.current) return; intervalRef.current = setInterval(() => { setTime(prevTime => prevTime + 1); }, 1000); }; const stop = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; // Обязательно обнуляем! } }; const reset = () => { stop(); setTime(0); }; // Очистка при размонтировании компонента (очень важно!) useEffect(() => { // Функция очистки эффекта return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []); return ( <div> <h2>Секундомер: {time} сек.</h2> <button onClick={start}>Старт</button> <button onClick={stop}>Стоп</button> <button onClick={reset}>Сброс</button> </div> ); } export default Stopwatch;
Задача 3: Поле поиска с автоматическим фокусом при монтировании
Сделай так, чтобы поле поиска получало фокус автоматически сразу после того, как компонент появится на странице.
Подсказка:
-
Используй
useRefдля получения доступа кinput. -
Используй
useEffectс пустым массивом зависимостей, чтобы командаfocus()выполнилась только один раз, при первом монтировании компонента.
Решение задачи 3:
import React, { useRef, useEffect } from 'react'; function SearchBox() { const searchInputRef = useRef(null); useEffect(() => { // Этот эффект выполнится только после первого рендера (монтирования) if (searchInputRef.current) { searchInputRef.current.focus(); console.log('Поле поиска получило фокус!'); } }, []); // Пустой массив зависимостей = эффект только при монтировании return ( <div> <input ref={searchInputRef} type="text" placeholder="Начните вводить запрос..." /> </div> ); } export default SearchBox;
Итоги
Ты только что освоил один из ключевых, хоть и не всегда очевидных, хуков React useRef. Давай резюмируем:
-
useRefвозвращает мутируемый объект с единственным свойством.current. -
Основное назначение №1. Прямой доступ к DOM-элементам через атрибут
refв JSX. Это твой портал в императивный мир браузера для задач вроде фокуса, скролла, анимаций и интеграции со сторонними библиотеками. -
Основное назначение №2. Хранение мутируемых значений, которые должны сохраняться между рендерами, но не должны их вызывать. Это твой «секретный блокнот» для таймеров, предыдущих значений, кеша и прочих служебных данных.
-
Главное правило. Не используй
useRefдля данных, которые должны рендериться в UI. Для этого естьuseState. -
Изменяй
ref.currentтолько в обработчиках событий или эффектах, но не во время самого рендера.
Хук useRef дает контроль над «реальным» миром DOM и памятью компонента. Используй ее с умом и твои приложения станут гораздо мощнее и отзывчивее.
Чтобы пройти весь путь от новичка до уверенного React-разработчика, изучи полный курс по React для начинающих. Там тебя ждут все 30 уроков, углубленные практические задания, реальные проекты и многое другое.
Удачи в изучении!
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


