useRef: как работает и где используют на практике

Разбираем useRef в React без мифов: внутренняя механика, DOM-кейсы, production-ошибки, производительность и ответы для собеседования.

06 марта 2026 г.17 минLexicon Team

Введение

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-компоненте:

  1. render: вычисляется следующий JSX.
  2. commit: React применяет изменения в DOM.
  3. эффекты (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 и альтернативы

КритерийuseRefuseStateЛокальная переменная в функции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:

  1. useRef хранит значение между рендерами и не вызывает ререндер при изменении current.
  2. Использую его для DOM-доступа и технических mutable-значений: id таймера, previous value, инстанс внешней библиотеки.
  3. Если данные должны отображаться в UI, выбираю useState, а не useRef.
  4. В 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, пройдите короткий инженерный чек-лист:

  1. Влияет ли значение на интерфейс напрямую? Если да, это кандидат на useState, а не на useRef.

  2. Это техническое значение между рендерами? Примеры: id таймера, ссылочный инстанс библиотеки, маркер актуального async-запроса.

  3. Есть ли cleanup при unmount? Если вы создаете через ref подписку, таймер, observer или внешний объект, у него должен быть явный teardown.

  4. Понятна ли цель ref из кода? Название ref должно отражать задачу (intervalRef, editorRef, activeRequestRef), иначе через месяц никто не поймет, зачем он нужен.

  5. Не дублируете ли вы уже существующий источник истины? Если те же данные уже есть в 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

Читайте также