useRef в React — создание ссылок на DOM и значения

16 марта 2026
Автор

Олег Марков

Введение

В React данные обычно текут сверху вниз через пропсы, а обновление состояния через useState вызывает перерисовку компонента. Но иногда нужно взаимодействовать с чем-то напрямую — обратиться к DOM-элементу, сохранить значение между рендерами без перерисовки или запомнить предыдущее состояние.

Для всего этого в React существует хук useRef. Он предоставляет изменяемый контейнер, который живёт на протяжении всего жизненного цикла компонента и не вызывает ре-рендер при обновлении.

В этой статье вы разберётесь, что такое useRef, как работать с DOM через рефы, когда использовать useRef вместо useState и какие ошибки чаще всего допускают разработчики. Хотите углублённо изучить React и научиться правильно работать с хуками? Приходите на наш курс Основы React, React Router и Redux Toolkit. На курсе 177 уроков и 17 упражнений, AI-тренажеры для безлимитной практики с кодом и задачами 24/7, решение задач с живым ревью наставника, еженедельные встречи с менторами.

Что такое useRef

useRef — это хук React, который возвращает изменяемый объект с единственным свойством .current. Этот объект сохраняется между рендерами компонента и не вызывает ре-рендер при изменении.

import { useRef } from 'react';

const ref = useRef(initialValue);
// ref.current === initialValue при первом вызове

Ключевые свойства useRef:

  • ref.current — текущее значение рефа
  • Изменение ref.current не вызывает ре-рендер
  • Объект ref остаётся стабильным между рендерами (та же ссылка)
  • Может хранить любое значение: DOM-элемент, число, строку, объект, функцию

Синтаксис useRef

import { useRef } from 'react';

const ref = useRef<T>(initialValue);

Параметры:

  • initialValue — начальное значение ref.current. После первого рендера игнорируется.

Возвращаемое значение: Объект { current: T }.

Использование 1: Работа с DOM-элементами

Самое распространённое применение useRef — прямой доступ к DOM-узлам: управление фокусом, прокруткой, анимациями и интеграция со сторонними библиотеками.

Управление фокусом

import { useRef } from 'react';

function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    // Прямой доступ к DOM-элементу
    inputRef.current?.focus();
  };

  return (
    <div>
      {/* Связываем ref с DOM-элементом через атрибут ref */}
      <input ref={inputRef} placeholder="Поиск..." />
      <button onClick={focusInput}>Сфокусироваться</button>
    </div>
  );
}

После монтирования компонента inputRef.current будет указывать на DOM-элемент <input>. При размонтировании React автоматически устанавливает ref.current = null.

Управление воспроизведением медиа

import { useRef } from 'react';

function VideoPlayer({ src }: { src: string }) {
  const videoRef = useRef<HTMLVideoElement>(null);

  const handlePlay = () => videoRef.current?.play();
  const handlePause = () => videoRef.current?.pause();

  const handleSeek = (time: number) => {
    if (videoRef.current) {
      videoRef.current.currentTime = time;
    }
  };

  return (
    <div>
      <video ref={videoRef} src={src} />
      <button onClick={handlePlay}>▶ Воспроизвести</button>
      <button onClick={handlePause}>⏸ Пауза</button>
      <button onClick={() => handleSeek(0)}>⏮ В начало</button>
    </div>
  );
}

Измерение размеров элемента

import { useRef, useState, useEffect } from 'react';

function ResizablePanel({ children }: { children: React.ReactNode }) {
  const panelRef = useRef<HTMLDivElement>(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (!panelRef.current) return;

    const observer = new ResizeObserver(entries => {
      const { width, height } = entries[0].contentRect;
      setDimensions({ width, height });
    });

    observer.observe(panelRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div ref={panelRef} style={{ resize: 'both', overflow: 'auto', border: '1px solid #ccc' }}>
      <p>Размер: {dimensions.width.toFixed(0)} × {dimensions.height.toFixed(0)}</p>
      {children}
    </div>
  );
}

Использование 2: Хранение значений без ре-рендера

useRef — это не только про DOM. Он отлично подходит для хранения любых значений, изменение которых не должно вызывать перерисовку компонента.

Сравнение useState и useRef

// useState — вызывает ре-рендер при изменении
const [count, setCount] = useState(0);
setCount(count + 1); // Компонент перерисуется

// useRef — НЕ вызывает ре-рендер при изменении
const countRef = useRef(0);
countRef.current += 1; // Компонент НЕ перерисуется

Хранение идентификатора таймера

import { useRef, useState } from 'react';

function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (intervalRef.current !== null) return; // Уже запущен

    intervalRef.current = setInterval(() => {
      setElapsed(prev => prev + 1);
    }, 1000);
  };

  const stop = () => {
    if (intervalRef.current === null) return;
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  const reset = () => {
    stop();
    setElapsed(0);
  };

  return (
    <div>
      <p>Прошло: {elapsed} сек</p>
      <button onClick={start}>Старт</button>
      <button onClick={stop}>Стоп</button>
      <button onClick={reset}>Сброс</button>
    </div>
  );
}

Если бы мы использовали useState для intervalId, каждое обновление таймера вызывало бы лишний ре-рендер. useRef решает это элегантно.

Запоминание предыдущего значения

import { useRef, useEffect } from 'react';

function usePrevious<T>(value: T): T | undefined {
  const prevRef = useRef<T | undefined>(undefined);

  useEffect(() => {
    // Обновляем реф ПОСЛЕ рендера
    prevRef.current = value;
  });

  // Возвращаем предыдущее значение (до текущего рендера)
  return prevRef.current;
}

// Использование
function PriceTracker({ price }: { price: number }) {
  const prevPrice = usePrevious(price);

  return (
    <div>
      <p>Текущая цена: {price} ₽</p>
      {prevPrice !== undefined && (
        <p style={{ color: price > prevPrice ? 'green' : 'red' }}>
          {price > prevPrice ? '▲' : '▼'} Прошлая цена: {prevPrice} ₽
        </p>
      )}
    </div>
  );
}

Флаг монтирования компонента

import { useRef, useEffect, useState } from 'react';

function AsyncComponent({ url }: { url: string }) {
  const [data, setData] = useState(null);
  const isMountedRef = useRef(true);

  useEffect(() => {
    isMountedRef.current = true;

    fetch(url)
      .then(res => res.json())
      .then(data => {
        // Проверяем, что компонент ещё смонтирован
        if (isMountedRef.current) {
          setData(data);
        }
      });

    return () => {
      isMountedRef.current = false; // Компонент размонтирован
    };
  }, [url]);

  return <div>{data ? JSON.stringify(data) : 'Загрузка...'}</div>;
}

Использование с TypeScript

При работе с DOM-элементами TypeScript требует указания типа элемента:

// DOM-элементы — начальное значение null
const divRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const formRef = useRef<HTMLFormElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);

// Обычные значения — указываем тип данных
const countRef = useRef<number>(0);
const nameRef = useRef<string>('');
const listRef = useRef<string[]>([]);

// Обнуляемые значения
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

Важный нюанс: для DOM-рефов используйте useRef<HTMLElement>(null) — значение будет null до монтирования. При обращении используйте опциональную цепочку или проверку:

const inputRef = useRef<HTMLInputElement>(null);

// ✅ Безопасно через опциональную цепочку
inputRef.current?.focus();

// ✅ Безопасно через проверку
if (inputRef.current) {
  inputRef.current.value = '';
}

// ❌ Опасно — может быть null
inputRef.current.focus(); // TypeError!

Forwarding Refs (форвардинг рефов)

По умолчанию рефы не передаются в дочерние компоненты. Для этого используется React.forwardRef:

import { forwardRef, useRef } from 'react';

// Компонент с поддержкой рефов
const CustomInput = forwardRef<HTMLInputElement, { placeholder?: string }>(
  ({ placeholder }, ref) => {
    return (
      <input
        ref={ref}
        placeholder={placeholder}
        style={{ border: '2px solid #6366f1', borderRadius: 4, padding: '4px 8px' }}
      />
    );
  }
);

// Использование
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = () => {
    console.log('Значение:', inputRef.current?.value);
    inputRef.current?.focus();
  };

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="Введите имя" />
      <button onClick={handleSubmit}>Отправить</button>
    </div>
  );
}

Распространённые ошибки

Ошибка 1: Читать ref во время рендера

// ❌ Ошибка — ref.current может быть null при рендере
function Component() {
  const divRef = useRef<HTMLDivElement>(null);
  const width = divRef.current?.offsetWidth; // null при первом рендере!

  return <div ref={divRef}>Ширина: {width}px</div>;
}

// ✅ Правильно — читаем в useEffect или обработчиках
function Component() {
  const divRef = useRef<HTMLDivElement>(null);
  const [width, setWidth] = useState(0);

  useEffect(() => {
    if (divRef.current) {
      setWidth(divRef.current.offsetWidth);
    }
  }, []);

  return <div ref={divRef}>Ширина: {width}px</div>;
}

Ошибка 2: Использовать useRef вместо useState для отображаемых данных

// ❌ Ошибка — изменение ref не вызывает ре-рендер, UI не обновится
function Counter() {
  const countRef = useRef(0);
  return (
    <div>
      <p>{countRef.current}</p> {/* Не обновится на экране! */}
      <button onClick={() => countRef.current++}>+1</button>
    </div>
  );
}

// ✅ Правильно — для отображаемых данных используйте useState
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

Ошибка 3: Изменять ref в теле компонента во время рендера

// ❌ Ошибка — изменение рефа во время рендера ведёт к непредсказуемому поведению
function Component({ value }: { value: number }) {
  const ref = useRef(0);
  ref.current = value; // Не делайте так при рендере!
  return <div>{ref.current}</div>;
}

// ✅ Исключение — если это нужно для синхронизации (например, сохранить последний пропс)
// Делайте это без условий и не в цикле
function Component({ onAction }: { onAction: () => void }) {
  const onActionRef = useRef(onAction);
  onActionRef.current = onAction; // OK — синхронизируем реф с пропсом
  // ...
}

Практический пример: автосохранение формы

import { useRef, useState, useEffect, useCallback } from 'react';

function AutoSaveForm() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved');

  // Реф для хранения таймера — не вызывает ре-рендер
  const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  // Реф для актуальных данных формы — избегаем замыкания в таймере
  const formDataRef = useRef({ title, content });

  // Синхронизируем реф с текущими данными
  formDataRef.current = { title, content };

  const save = useCallback(async () => {
    setSaveStatus('saving');
    await new Promise(resolve => setTimeout(resolve, 800)); // Имитация запроса
    console.log('Сохранено:', formDataRef.current);
    setSaveStatus('saved');
  }, []);

  // Автосохранение с дебаунсингом
  useEffect(() => {
    if (!title && !content) return;

    setSaveStatus('unsaved');

    if (saveTimerRef.current) {
      clearTimeout(saveTimerRef.current);
    }

    saveTimerRef.current = setTimeout(() => {
      save();
    }, 1500);

    return () => {
      if (saveTimerRef.current) {
        clearTimeout(saveTimerRef.current);
      }
    };
  }, [title, content, save]);

  return (
    <div style={{ maxWidth: 600, margin: '0 auto' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
        <h2>Редактор</h2>
        <span style={{ color: saveStatus === 'saved' ? 'green' : saveStatus === 'saving' ? 'orange' : 'gray' }}>
          {saveStatus === 'saved' ? '✓ Сохранено' : saveStatus === 'saving' ? '⏳ Сохраняем...' : '● Не сохранено'}
        </span>
      </div>
      <input
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder="Заголовок"
        style={{ width: '100%', marginBottom: 8, padding: 8, fontSize: 18 }}
      />
      <textarea
        value={content}
        onChange={e => setContent(e.target.value)}
        placeholder="Содержание..."
        rows={10}
        style={{ width: '100%', padding: 8 }}
      />
    </div>
  );
}

Когда использовать useRef

Ситуация useState useRef
Данные для отображения в UI
Прямой доступ к DOM
Таймеры и интервалы
Предыдущие значения
Флаги (смонтирован/нет)
Кеш значений (не для UI)

Заключение

useRef — это универсальный инструмент для двух задач:

  1. Прямой доступ к DOM — управление фокусом, прокруткой, анимациями, интеграция со сторонними библиотеками
  2. Хранение изменяемых значений — без вызова ре-рендера при изменении

Главное правило: если значение нужно отображать в UI — используйте useState. Если значение нужно только для внутренней логики компонента и не должно вызывать перерисовку — используйте useRef.

Стрелочка влевоuseSyncExternalStore — работа с внешними сторамиuseLayoutEffect в React — эффект до отрисовкиСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с данными
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
StrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactКак работает JSX связка React и HTMLЧто такое props в React и как их правильно использоватьЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOM
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий