useRef: DOM-элементы и память без перерисовки
Зачем нужен useRef
У состояния есть два свойства: оно сохраняется между отрисовками и его изменение вызывает перерисовку. Иногда нужно только первое:
- запомнить id таймера, чтобы потом его остановить;
- помнить прошлое значение prop'а;
- держать ссылку на DOM-элемент, чтобы поставить в него курсор, прокрутить к нему или узнать его размер.
Ни одно из этих значений не показывается на экране, так что перерисовка при их изменении не нужна.
Для этого есть useRef.
Как устроен ref
const { useRef } = React;
function Example() {
const clicks = useRef(0);
function handleClick() {
clicks.current = clicks.current + 1;
console.log("Кликов: " + clicks.current);
}
return <button onClick={handleClick}>Жми</button>;
}
useRef(начальное)возвращает объект{ current: начальное }.- Это один и тот же объект во всех отрисовках компонента — React хранит его, как состояние.
ref.currentможно читать и менять напрямую. React об этом не знает и ничего не перерисовывает.
В примере консоль покажет 1, 2, 3…, а кнопка на экране не изменится ни разу.
Ref и состояние: в чём разница
useState |
useRef |
|
|---|---|---|
| Сохраняется между отрисовками | да | да |
| Изменение вызывает отрисовку | да | нет |
| Как изменить | setValue(новое) |
ref.current = новое |
| Новое значение видно | в следующей отрисовке | сразу |
| Для чего | всё, что показано на экране | служебные данные и DOM-элементы |
Почему нельзя просто взять обычную переменную let timer? Потому что функция-компонент
вызывается при каждой отрисовке заново, и все её локальные переменные создаются заново. Переменная
со значением из прошлой отрисовки пропадает. Ref — это «ящик», который React хранит за тебя.
Ссылка на DOM-элемент
Самое частое применение — доступ к настоящему элементу страницы. Передай ref в атрибут ref, и
после отрисовки React положит в ref.current сам DOM-узел:
function SearchBar() {
const inputRef = useRef(null);
return (
<div>
<input ref={inputRef} placeholder="Поиск" />
<button onClick={() => inputRef.current.focus()}>Найти</button>
</div>
);
}
Что можно делать с элементом:
| Задача | Код |
|---|---|
| Поставить курсор | ref.current.focus() |
| Выделить текст | ref.current.select() |
| Прокрутить к элементу | ref.current.scrollIntoView({ behavior: "smooth" }) |
| Узнать размер | ref.current.getBoundingClientRect() |
| Запустить видео | ref.current.play() |
Во время отрисовки ref.current для DOM ещё null — элемента пока нет. Обращайся к нему в
обработчиках событий и в эффектах:
function AutoFocus() {
const ref = useRef(null);
useEffect(() => {
ref.current.focus(); // элемент уже на странице
}, []);
return <input ref={ref} />;
}
Не используй ref, чтобы менять то, чем управляет React (текст, классы, дочерние элементы): React перезапишет твои изменения при следующей отрисовке. Ref для DOM — это «запасной выход» для того, чего нельзя выразить через JSX: фокус, прокрутка, размеры, сторонние библиотеки.
Таймер в ref
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current !== null) return; // уже идёт
intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
}
function stop() {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
useEffect(() => stop, []); // при удалении компонента — остановить
return (
<div>
<p>{seconds} с</p>
<button onClick={start}>Старт</button>
<button onClick={stop}>Стоп</button>
</div>
);
}
seconds — в состоянии: число показывается на экране. id интервала — в ref: его никто не видит,
но он нужен кнопке «Стоп».
Прошлое значение
Ref, обновлённый в эффекте, «отстаёт» на одну отрисовку — так удобно помнить прошлое значение:
function Score({ value }) {
const prev = useRef(value);
useEffect(() => {
prev.current = value; // после показа запоминаем текущее
});
return <p>Было {prev.current}, стало {value}</p>;
}
Во время отрисовки prev.current ещё хранит значение с прошлого раза, а после показа эффект
записывает туда новое.
Ref на свой компонент: forwardRef
Атрибут ref работает на HTML-тегах. Чтобы передать ref в свой компонент и дальше на элемент
внутри него, в React 18 компонент оборачивают в forwardRef:
const { forwardRef } = React;
const FancyInput = forwardRef(function FancyInput(props, ref) {
return <input ref={ref} className="fancy" {...props} />;
});
function Form() {
const ref = useRef(null);
return (
<>
<FancyInput ref={ref} placeholder="Имя" />
<button onClick={() => ref.current.focus()}>Фокус</button>
</>
);
}
(В React 19 ref можно принимать как обычный prop, без forwardRef.)
Частые ошибки
- Читать
ref.currentпри отрисовке, чтобы показать на экране. Изменение ref не перерисует компонент — на экране останется старое. Всё, что видно пользователю, — в состоянии. - Обращаться к DOM-ref до показа. При первой отрисовке там
null. Используй эффект или обработчик. letвместо ref. Обычная переменная обнуляется при каждой отрисовке.- Менять DOM через ref вместо состояния.
ref.current.textContent = …React перезапишет.
Итог
useRef(x)→{ current: x }, один объект на всё время жизни компонента.- Изменение
ref.currentне вызывает отрисовку. Видимое — в состоянии, служебное — в ref. <input ref={ref} />— после показа вref.currentDOM-элемент:focus(),scrollIntoView(), размеры.- id таймеров, прошлые значения, флаги «уже загружено» — тоже в ref.
- Чтобы передать ref в свой компонент —
forwardRef.