useRef в React: быстрый способ управлять DOM и хранить mutable-данные
Хук useRef позволяет получить доступ к реальному DOM-элементу и хранить mutable-данные без лишних перерисовок. Примеры использования, типичные ошибки и сравнение с useState.
Если ваш компонент “залипает” из-за частых обновлений, а нужен доступ к конкретному элементу, хук useRef подходящее решение. Он хранит изменяемое значение, которое не участвует в жизненном цикле рендера, и не нарушает однонаправленный поток данных.
Ниже показано, где он действительно полезен, какие ошибки часто встречаются и какие альтернативы доступны. Все на живом примере, без лишних абстракций.
Что хранит useRef и как он отличается от useState
Хук | Что хранит | Триггерит перерисовку | Как читается в JSX |
---|---|---|---|
useState | Сериализуемое значение | Да (при set) | {state} |
useRef | Любой объект (mutable) | Нет | ref.current |
useRef
создает ref-объект с полем current
. Объект существует все время жизни компонента, но изменение current
не приводит к повторному рендеру. Поэтому его часто используют как “коробку” для хранения:
- ссылки на DOM-элемент;
- таймеров, подписок, предыдущих пропсов;
- любых внешних mutable-данных.
Если требуется сохранять значение между рендерами, но без запуска нового рендера, выбирайте
useRef
.
Как привязать ref к DOM-элементу
Самый простой случай получить ссылку на <input>
и управлять фокусом из кода.
import { useRef } from "react";
function SearchBox() {
const inputRef = useRef(null);
const focusInput = () => {
// current реальный DOM-элемент
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} placeholder="Поиск" />
<button onClick={focusInput}>Фокусировать поле</button>
</div>
);
}
ref
передается через ref={inputRef}
. После монтирования inputRef.current
содержит <input>
и мы можем вызывать любые методы DOM. Это классический пример применения хука.
Хранение мутируемого значения без перерисовки
Иногда нужно сохранить предыдущее значение пропса для сравнения в useEffect
.
import { useEffect, useRef } from "react";
function Counter({ value }) {
const prev = useRef(value);
useEffect(() => {
if (prev.current !== value) {
console.log("Изменилось значение:", prev.current, "→", value);
}
prev.current = value; // обновляем без рендера
}, [value]);
return <div>Текущее: {value}</div>;
}
prev.current
всегда хранит предыдущее значение, но UI не обновляется. Такой паттерн часто заменяет useState
, когда нужно лишь кэшировать данные.
Когда useRef не заменит useState
Сценарий | Правильный хук |
---|---|
Отображать новое значение в UI | useState |
Хранить ссылку на элемент | useRef |
Передать объект дочернему компоненту без рендера | useRef + React.memo |
Интеграция с таймерами, WebSocket и т.п. | useRef |
Попытка хранить в useRef
то, что должно влиять на вывод, приводит к “ловушке” новичков: UI не обновится.
Практический пример: бесконечный скролл списка
import { useEffect, useRef, useState } from "react";
function InfiniteList() {
const [items, setItems] = useState(Array.from({ length: 20 }, (_, i) => i));
const listRef = useRef(null); // контейнер списка
const loadMore = () => {
setItems((prev) => [
...prev,
...Array.from({ length: 20 }, (_, i) => prev.length + i),
]);
};
useEffect(() => {
const el = listRef.current;
if (!el) return;
const onScroll = () => {
if (el.scrollHeight - el.scrollTop === el.clientHeight) {
loadMore();
}
};
el.addEventListener("scroll", onScroll);
return () => el.removeEventListener("scroll", onScroll);
}, []);
return (
<div
ref={listRef}
style={{ height: 200, overflowY: "auto", border: "1px solid #ccc" }}
>
{items.map((item) => (
<div key={item} style={{ padding: "8px" }}>
Элемент #{item}
</div>
))}
</div>
);
}
listRef
нужен только для привязки обработчика скролла; изменение scrollTop
внутри обработчика не вызывает рендер. Если бы мы хранили позицию в useState
, каждый микроскролл приводил бы к лишнему рендеру и тормозам. Это еще один случай, когда хук помогает экономить ресурсы.
FAQ / чеклист
- Не храните в useRef то, что выводится UI не обновится без
setState
. - Инициализируйте ref (
useRef(null)
), иначеcurrent
будетundefined
до монтирования. - Для доступа к последнему пропу используйте
useRef
внутриuseEffect
. - Не путайте ref с
forwardRef
первый просто хранит значение, второй передает его вниз по иерархии. - Если нужен один и тот же ref в нескольких компонентах, вынесите его в кастомный хук или контекст; см. материал о React Context.
Хук пригодится каждый раз, когда нужно “дотянуться” до реального DOM, хранить mutable-данные вне потока рендера или интегрировать сторонние библиотеки без лишних перерисовок. В большинстве UI-задач он экономит ресурсы и упрощает код, позволяя сосредоточиться на бизнес-логике, а не на управлении состоянием.