Как работает useRef в React?
Что такое useRef
useRef — это хук React, который возвращает мутабельный объект { current: initialValue }. Этот объект живёт на протяжении всего жизненного цикла компонента и сохраняет своё значение между рендерами.
Главное отличие от useState: изменение ref.current не вызывает повторный рендер компонента.
Два основных сценария использования
1. Доступ к DOM-элементу
Самый распространённый случай — получить ссылку на реальный DOM-узел, чтобы вызвать его методы напрямую.
import { useRef, useEffect } from 'react';
function FocusInput() {
// Создаём ref с начальным значением null
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// После монтирования фокусируем поле
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
React сам устанавливает inputRef.current в DOM-элемент при монтировании и обнуляет при размонтировании.
2. Хранение мутабельного значения без перерендера
useRef удобен, когда нужно хранить значение между рендерами, но его изменение не должно инициировать новый рендер.
import { useRef, useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
// Храним id интервала — менять его незачем в UI
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
return <p>Прошло секунд: {count}</p>;
}
Как useRef работает под капотом
При каждом рендере React возвращает один и тот же объект. Объект не пересоздаётся — это принципиально отличает useRef от создания объекта внутри компонента напрямую (тот создавался бы заново на каждом рендере).
function Example() {
// Этот объект создаётся заново при каждом рендере — НЕВЕРНО для хранения
const badRef = { current: 0 };
// Этот объект стабилен между рендерами — ВЕРНО
const goodRef = useRef(0);
}
Типичные паттерны
Хранение предыдущего значения
function usePrevious<T>(value: T): T | undefined {
const prevRef = useRef<T>();
useEffect(() => {
// Обновляем после рендера, поэтому в текущем рендере prevRef.current — старое значение
prevRef.current = value;
});
return prevRef.current;
}
Избежание stale closure
function SearchComponent({ onSearch }: { onSearch: (q: string) => void }) {
const onSearchRef = useRef(onSearch);
useEffect(() => {
// Обновляем ref при каждом рендере, не добавляя в deps эффекта
onSearchRef.current = onSearch;
});
useEffect(() => {
const handler = () => onSearchRef.current('query');
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []); // deps пустые — ref всегда актуален
}
Важные ограничения
- Не читайте и не пишите
ref.currentво время рендера (исключение — ленивая инициализация). Это нарушает чистоту функции рендера. - Для передачи ref в дочерний компонент используйте
forwardRef(илиrefкак обычный пропс в React 19+). - useRef не реактивен: если вам нужна реакция UI на изменение значения — используйте useState.
Что хочет услышать интервьюер
Понимание разницы между useRef и useState: изменение ref не вызывает рендер
Знание двух основных сценариев: доступ к DOM и хранение мутабельного значения между рендерами
Понимание того, что React возвращает один и тот же объект на каждом рендере
Умение объяснить проблему stale closure и как useRef помогает её решить
Знание ограничений: не читать current во время рендера, forwardRef для дочерних компонентов
Пример: Доступ к DOM-элементу
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Фокусируем поле сразу после монтирования
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Введите текст" />;
}
Пример: Хранение значения без перерендера
import { useRef, useState } from 'react';
function RenderCounter() {
const [value, setValue] = useState('');
// Счётчик рендеров — не нужен в UI, не должен триггерить рендер
const renderCount = useRef(0);
renderCount.current += 1;
return (
<div>
<input value={value} onChange={e => setValue(e.target.value)} />
<p>Количество рендеров: {renderCount.current}</p>
</div>
);
}
Пример: Паттерн usePrevious
import { useRef, useEffect } from 'react';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
// Сохраняем текущее значение ПОСЛЕ рендера
// Поэтому во время рендера ref.current содержит предыдущее значение
ref.current = value;
});
return ref.current;
}
// Использование
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<p>Текущее: {count}, предыдущее: {prevCount}</p>
);
}
Типичные ошибки
Путают useRef с useState и ожидают перерендера при изменении ref.current
Читают или пишут ref.current прямо во время рендера вместо useEffect
Используют useRef для хранения данных, которые должны отображаться в UI — вместо useState
Забывают про forwardRef при передаче ref в дочерний компонент (до React 19)
Создают объект { current: value } вручную внутри компонента, не понимая, что он будет пересоздаваться на каждом рендере


