useRef: как работает и где используют на практике
Разбираем useRef в React без мифов: внутренняя механика, DOM-кейсы, production-ошибки, производительность и ответы для собеседования.
- Введение
- Что такое useRef и как он устроен
- Короткий ответ
- Почему React не ререндерит при изменении ref
- Граница ответственности useRef
- Архитектура: место useRef в жизненном цикле компонента
- Где useRef применяют на практике
- 1. Доступ к DOM без querySelector
- 2. Хранение mutable-значений между рендерами
- 3. Интеграция со сторонними библиотеками с императивным API
- Таблица сравнения: useRef и альтернативы
- Ошибки в продакшене: где useRef ломает систему
- Ошибка 1. Хранить в ref данные, которые рендерятся
- Ошибка 2. Записывать в ref в рендере как в бизнес-хранилище
- Ошибка 3. Не очищать ресурсы, связанные с ref
- Ошибка 4. Использовать ref для обхода архитектурных проблем
- Разбор производительности
- Когда useRef не нужен
- 1. Значение должно сразу отражаться в UI
- 2. Нужно вычисление на основе зависимостей
- 3. Хотите «починить» чрезмерные ререндеры в корневом компоненте
- Практики, которые работают в командах
- Частые ошибки
- Как отвечать на интервью про useRef
- Практический кейс из production: предотвращение гонок запросов
- Чек-лист внедрения useRef в существующий код
- FAQ
- useRef или useState для предыдущего значения?
- Можно ли заменить useRef обычной переменной?
- Нужно ли добавлять ref в зависимости useEffect?
- Почему в StrictMode поведение с ref иногда выглядит странно?
- useRef помогает в борьбе с гонками в асинхронном коде?
- Итоги
Введение
useRef часто воспринимают как «крючок для DOM», но в реальных проектах это слишком узкая трактовка. На практике хук решает сразу несколько задач: прямые вызовы методов API, хранение технического состояния между рендерами и защита от побочных эффектов, не вызывающих лишних перерисовок компонента.
Из-за простого API ({ current: ... }), useRef нередко используют неправильно: кладут в него данные, от которых зависит интерфейс, обновляют current в рендере, маскируют архитектурные проблемы вместо нормальной декомпозиции состояния. В итоге код кажется «оптимизированным», но становится хрупким и сложным для поддержки.
В статье разберем, как useRef работает внутри React, где он действительно полезен в production и когда от него лучше отказаться. Если хотите связать тему с общим жизненным циклом рендера, лучше ознакомиться разбор перерисовок в React, полный гид по React Hooks и практику по оптимизации компонентов.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Что такое useRef и как он устроен
Короткий ответ
useRef возвращает стабильный объект вида { current: T }, который сохраняется между циклами отрисовки компонента. Изменение ref.current не вызывает новый рендер.
Это делает useRef удобным для данных, которые:
- нужны между рендерами;
- не должны напрямую менять UI;
- относятся к технической механике компонента (DOM-узел, id таймера, внешний инстанс).
Почему React не ререндерит при изменении ref
React запускает рендер, когда меняются state/props/context или приходит явный сигнал обновления. Мутация поля current у уже существующего объекта ref не является таким сигналом.
Упрощенно это выглядит так:
function CounterDebug() {
const renderCountRef = useRef(0);
const [value, setValue] = useState(0);
renderCountRef.current += 1;
return (
<div>
<p>value: {value}</p>
<p>renders: {renderCountRef.current}</p>
<button onClick={() => setValue((v) => v + 1)}>+1</button>
</div>
);
}
renderCountRef.current изменяется, но рендер происходит не из-за ref, а из-за setValue. Если убрать useState, обновления интерфейса не будет.
Граница ответственности useRef
Хорошее правило: useRef хранит технические детали, useState хранит данные интерфейса.
Если значение влияет на JSX-ветвление, отображаемый текст, disabled-состояние кнопки, загрузочные индикаторы и т.д., это не ref-кейс. Иначе вы получите рассинхронизацию: данные уже изменились, а UI об этом не знает.
Архитектура: место useRef в жизненном цикле компонента
Рассмотрим типичный поток в React-компоненте:
render: вычисляется следующий JSX.commit: React применяет изменения в DOM.- эффекты (
useEffect/useLayoutEffect): запускается побочная логика.
useRef живет поперек всех этих фаз как стабильный контейнер. Это дает три практических преимущества:
- можно передать DOM-ссылку в эффект после commit;
- можно хранить служебные метки для async-операций;
- можно кешировать внешний императивный объект без state-шума.
Схема ответственности:
state: реактивные данные интерфейса;ref: императивные ссылки и технические переменные;effect: синхронизация с внешним миром.
Типичная точка отказа в production: ref начинает выполнять роль state, и тогда команда теряет предсказуемость. Компонент «знает» больше, чем отображает, тесты становятся флейки, а баги проявляются только под нагрузкой или при сложной последовательности действий пользователя.
Где useRef применяют на практике
1. Доступ к DOM без querySelector
Самый известный сценарий: фокус, скролл, измерение размеров, интеграция с API браузера.
function SearchBox() {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
const scrollToInput = () => {
inputRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
};
return (
<div>
<button onClick={scrollToInput}>К полю поиска</button>
<input ref={inputRef} placeholder="Введите запрос" />
</div>
);
}
Плюс подхода: отсутствует ручной поиск элемента по DOM и меньше риск сломать логику при рефакторинге разметки.
2. Хранение mutable-значений между рендерами
Кейс: id интервала, last request id, признак mounted/unmounted, предыдущее значение пропса.
function PollingWidget({ userId }: { userId: string }) {
const intervalRef = useRef<number | null>(null);
const lastUserIdRef = useRef<string | null>(null);
useEffect(() => {
lastUserIdRef.current = userId;
intervalRef.current = window.setInterval(() => {
// техническая логика опроса
console.log("polling for", lastUserIdRef.current);
}, 5000);
return () => {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
}
};
}, [userId]);
return null;
}
Здесь ref полезен тем, что хранит изменяемую техническую информацию без лишних ререндеров.
3. Интеграция со сторонними библиотеками с императивным API
Например, графики, редакторы, map SDK, медиа-плееры.
function ChartBlock({ data }: { data: Array<{ x: number; y: number }> }) {
const containerRef = useRef<HTMLDivElement | null>(null);
const chartRef = useRef<{ update: (d: unknown) => void; destroy: () => void } | null>(null);
useEffect(() => {
if (!containerRef.current) return;
if (!chartRef.current) {
chartRef.current = createChart(containerRef.current, data);
return;
}
chartRef.current.update(data);
}, [data]);
useEffect(() => {
return () => chartRef.current?.destroy();
}, []);
return <div ref={containerRef} style={{ minHeight: 280 }} />;
}
chartRef отделяет lifecycle внешнего объекта от lifecycle React-рендера. Это критично в сложных dashboard-интерфейсах.
Таблица сравнения: useRef и альтернативы
| Критерий | useRef | useState | Локальная переменная в функции | useMemo |
|---|---|---|---|---|
| Сохраняется между рендерами | Да | Да | Нет | Да (кеш значения) |
| Вызывает ререндер при обновлении | Нет | Да | Нет | Не напрямую |
| Подходит для данных UI | Ограниченно | Да | Нет | Частично |
| Подходит для DOM/imperative API | Да | Нет | Нет | Нет |
| Риск рассинхронизации с UI | Высокий при misuse | Низкий | Очень высокий | Средний |
| Типичные кейсы | ref на элемент, таймер, инстанс SDK | форма, фильтры, переключатели | временные вычисления в одном рендере | дорогие вычисления и стабильные ссылки |
Выбор обычно простой: если значение должно менять интерфейс, берите useState; если это техническое «служебное поле» между рендерами, берите useRef.
Ошибки в продакшене: где useRef ломает систему
Ошибка 1. Хранить в ref данные, которые рендерятся
Симптомы:
- UI визуально «залипает» на старом значении;
- в логах видно обновления
ref.current, но пользователь их не видит; - появляются ручные выховы
forceUpdateв качестве «костыля».
Последствие: неконсистентный интерфейс и регрессии после мелких изменений.
Профилактика: любое состояние, влияющее на JSX, держать в useState/store.
Ошибка 2. Записывать в ref в рендере как в бизнес-хранилище
Симптомы:
- сложно объяснить порядок обновлений;
- поведение расходится в StrictMode;
- тесты с моком таймеров или async становятся нестабильными.
Последствие: трудно воспроизводимые баги, особенно в конкурентных сценариях.
Профилактика: обновляйте ref.current в эффектах или обработчиках событий, а не как источник бизнес-правды внутри рендера.
Ошибка 3. Не очищать ресурсы, связанные с ref
Симптомы:
- утечки памяти после переходов между страницами;
- повторные подписки на один и тот же источник;
- рост CPU из-за «висящих» интервалов/листенеров.
Последствие: деградация производительности длинной сессии и тяжело диагностируемые инциденты.
Профилактика: всегда писать cleanup в useEffect, где ресурс создается.
Ошибка 4. Использовать ref для обхода архитектурных проблем
Симптомы:
- компонент перегружен полями
refи «техническими флагами»; - бизнес-логика размазана между эффектами;
- новые разработчики боятся трогать модуль.
Последствие: рост стоимости изменений и замедление разработки.
Профилактика: поднимать границы компонента, выделять хуки по ответственности, а ref оставлять только для императивных деталей.
Разбор производительности
useRef может уменьшить число ререндеров, но это не автоматическое ускорение. Узкое место часто не в рендерах как таковых, а в тяжелых вычислениях, работе сети, layout thrashing или неудачной композиции компонентов.
Когда useRef реально помогает:
- вы храните часто меняющийся служебный счетчик/идентификатор, который не должен трогать UI;
- вы предотвращаете лишние перевызовы эффектов, используя стабильный контейнер для внешнего инстанса;
- вы убираете промежуточный state, который раньше гонял перерисовки без пользовательской пользы.
Когда оптимизация преждевременна:
- вы не измерили проблему профилировщиком React DevTools;
- задержки идут из сети/бэкенда, а вы «оптимизируете» ref;
- вы усложняете код ради гипотетических миллисекунд.
Практический ориентир: сначала профилирование и метрики (render count, commit time, Web Vitals), потом точечный useRef в горячем пути.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks.
Когда useRef не нужен
У useRef есть зона комфорта, и выход за нее почти всегда приводит к усложнению кода. Ниже три ситуации, где хук обычно лишний.
1. Значение должно сразу отражаться в UI
Если данные влияют на текст, видимость блока, класс элемента или доступность кнопки, это реактивная задача. Используйте useState или внешний стор, чтобы интерфейс менялся предсказуемо и тестируемо.
2. Нужно вычисление на основе зависимостей
Иногда в ref пытаются кешировать результат вычислений «чтобы не ререндерить». Для этого лучше подходит useMemo, потому что он явно выражает зависимость от входных параметров и проще читается на ревью.
3. Хотите «починить» чрезмерные ререндеры в корневом компоненте
Если реальная проблема в архитектуре (слишком большой parent, неудачные границы контекста, тяжелый child без мемоизации), useRef станет временной заплаткой. Корректнее:
- разделить компонент на более узкие части;
- локализовать state рядом с местом использования;
- профилировать узкие места и только потом выбирать точечную оптимизацию.
Главный ориентир: useRef полезен, когда вы описываете связь React с императивным миром, а не для того, чтобы скрыть реактивную модель.
Практики, которые работают в командах
- В code review явно фиксируйте назначение каждого
ref: DOM, таймер, внешний инстанс, previous value. - Не смешивайте бизнес-данные и технические данные в одном контейнере.
- Для
refсо сложной логикой делайте небольшой кастомный хук (useLatest,usePrevious,useStableCallback), чтобы сократить дублирование. - Покрывайте edge-кейсы тестами: unmount во время async, повторный mount, смена props в быстрой последовательности.
- Сначала делайте корректную архитектуру состояния, потом оптимизируйте количество ререндеров.
Частые ошибки
- Пытаться через
useRef«избежать всех ререндеров» вместо нормальной декомпозиции компонентов. - Передавать
ref.currentв массив зависимостей эффекта и ждать реактивного поведения. - Забывать
null-проверку перед DOM-операциями. - Держать в
refфлаги интерфейса (isOpen,isLoading,hasError) и удивляться, что UI не обновляется. - Не очищать внешние ресурсы (
destroy,unsubscribe,clearInterval) при unmount.
Как отвечать на интервью про useRef
Рабочий шаблон ответа для junior/middle:
useRefхранит значение между рендерами и не вызывает ререндер при измененииcurrent.- Использую его для DOM-доступа и технических mutable-значений: id таймера, previous value, инстанс внешней библиотеки.
- Если данные должны отображаться в UI, выбираю
useState, а неuseRef. - В production слежу за cleanup ресурсов и не записываю бизнес-состояние в ref.
Такой ответ показывает не только знание API, но и инженерное понимание границ применения.
Подготовься к React-собеседованию на реальных кейсах
Разберем useRef, useEffect и ререндеры в формате mock-интервью с обратной связью по ответам.
Практический кейс из production: предотвращение гонок запросов
Представим экран поиска, где пользователь быстро вводит текст, а компонент отправляет запрос на сервер после каждого изменения. Без дополнительной защиты ответы могут приходить не по порядку: более старый запрос вернется позже и перезапишет свежие данные.
Частая ошибка в таких сценариях: хранить id активного запроса в state. Формально это работает, но добавляет лишние ререндеры и усложняет зависимость эффектов. Для технического маркера удобнее useRef.
function SearchUsers() {
const [query, setQuery] = useState("");
const [items, setItems] = useState<Array<{ id: string; name: string }>>([]);
const [loading, setLoading] = useState(false);
const requestIdRef = useRef(0);
useEffect(() => {
if (!query.trim()) {
setItems([]);
return;
}
const requestId = ++requestIdRef.current;
setLoading(true);
fetch(`/api/users?q=${encodeURIComponent(query)}`)
.then((r) => r.json())
.then((data) => {
// Применяем только самый свежий ответ.
if (requestId === requestIdRef.current) {
setItems(data.items ?? []);
}
})
.catch(() => {
if (requestId === requestIdRef.current) {
setItems([]);
}
})
.finally(() => {
if (requestId === requestIdRef.current) {
setLoading(false);
}
});
}, [query]);
return (
<section>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Найти пользователя"
/>
{loading && <p>Загрузка...</p>}
<ul>{items.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
</section>
);
}
Почему это работает:
requestIdRef.currentобновляется без лишнего рендера;- UI-состояние (
items,loading) остается вuseState, то есть интерфейс всегда реактивен; - старые ответы безопасно игнорируются.
Где граница подхода: ref не отменяет сетевой запрос физически, он только фильтрует устаревший результат на клиенте. Если нужно экономить трафик и серверные ресурсы, добавляйте AbortController и отмену запроса.
Чек-лист внедрения useRef в существующий код
Перед тем как добавить новый useRef, пройдите короткий инженерный чек-лист:
-
Влияет ли значение на интерфейс напрямую? Если да, это кандидат на
useState, а не наuseRef. -
Это техническое значение между рендерами? Примеры: id таймера, ссылочный инстанс библиотеки, маркер актуального async-запроса.
-
Есть ли cleanup при unmount? Если вы создаете через ref подписку, таймер, observer или внешний объект, у него должен быть явный teardown.
-
Понятна ли цель ref из кода? Название
refдолжно отражать задачу (intervalRef,editorRef,activeRequestRef), иначе через месяц никто не поймет, зачем он нужен. -
Не дублируете ли вы уже существующий источник истины? Если те же данные уже есть в state/store, второй источник в ref почти всегда ведет к расхождению.
Этот чек-лист хорошо работает в code review: он быстро отделяет оправданный useRef от «случайной оптимизации».
FAQ
useRef или useState для предыдущего значения?
Если прошлое значение нужно только для технической логики эффекта, удобен useRef. Если прошлое значение нужно показать в интерфейсе, обычно нужен useState.
Можно ли заменить useRef обычной переменной?
Нет, потому что обычная переменная пересоздается на каждом рендере. useRef сохраняет контейнер между рендерами.
Нужно ли добавлять ref в зависимости useEffect?
Обычно нет. Сам объект ref стабилен, а изменение current не триггерит эффекты. В зависимости включают данные, изменение которых требует перезапуска эффекта.
Почему в StrictMode поведение с ref иногда выглядит странно?
StrictMode в dev-режиме помогает ловить побочные эффекты двойным прогоном некоторых сценариев. Если логика на ref неидемпотентна или нет cleanup, проблема проявляется сразу.
useRef помогает в борьбе с гонками в асинхронном коде?
Да, ref часто используют как маркер «актуального запроса» или флаг отмены. Но это не замена полноценной отмены через AbortController, когда она доступна.
Итоги
useRef нужен не для «магической оптимизации», а для четких технических задач: DOM-ссылки, внешние императивные объекты и служебные mutable-значения между рендерами. Как только данные начинают определять интерфейс, переходите на реактивное состояние.
Если держать эту границу, useRef упрощает архитектуру и снижает шум ререндеров. Если границу нарушить, он превращается в источник скрытых багов и дорогой поддержки.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Автор
Lexicon Team
Читайте также
frontend
React и TypeScript: частые вопросы на интервью
Разбираем частые вопросы на интервью по React и TypeScript: типизация props, hooks, events, generics, refs, discriminated unions. А также типичные ошибки кандидатов и примеры сильных ответов.
frontend
React batching: как работает группировка обновлений
Разбираем batching в React на практике: очереди обновлений, автоматическая группировка в React 18+, flushSync, startTransition и production-ошибки.
frontend
React Strict Mode: зачем он нужен
Подробно разбираем React Strict Mode: какие проверки он включает, почему в dev все «вызывается дважды», какие баги ловит и как безопасно внедрять в production-командах.