Перейти к содержимому
Тема 7 из 13
Теория

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.)

Частые ошибки

  1. Читать ref.current при отрисовке, чтобы показать на экране. Изменение ref не перерисует компонент — на экране останется старое. Всё, что видно пользователю, — в состоянии.
  2. Обращаться к DOM-ref до показа. При первой отрисовке там null. Используй эффект или обработчик.
  3. let вместо ref. Обычная переменная обнуляется при каждой отрисовке.
  4. Менять DOM через ref вместо состояния. ref.current.textContent = … React перезапишет.

Итог

  • useRef(x) → { current: x }, один объект на всё время жизни компонента.
  • Изменение ref.current не вызывает отрисовку. Видимое — в состоянии, служебное — в ref.
  • <input ref={ref} /> — после показа в ref.current DOM-элемент: focus(), scrollIntoView(), размеры.
  • id таймеров, прошлые значения, флаги «уже загружено» — тоже в ref.
  • Чтобы передать ref в свой компонент — forwardRef.

Фокус по кнопке

Лёгкая+10 XP

Напиши компонент FocusForm: поле <input> и кнопка Edit. Клик по кнопке ставит курсор в поле (вызывает у поля focus()).

[          ] [Edit]   → клик по Edit → курсор мигает в поле

Подключи коннектор, чтобы решать задачи

Код задач выполняется на твоём компьютере — так быстрее и надёжнее, чем в браузере. Для этого нужна бесплатная программа-коннектор CodeWithMe. Подключить её нужно один раз.

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.