Gernar
Frontend DeveloperReact

Интервью-вопрос

Что такое хук useRef?

useRef хранит мутабельное значение между рендерами и не запускает рендер при изменении current. Главный риск в том, что ref легко перепутать со state и получить устаревший UI или обращение к DOM до монтирования.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 50 правильно

Как лучше кратко объяснить useRef на интервью?

Вы отвечаете на базовый вопрос и хотите сразу показать отличие от state.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

useRef возвращает один и тот же объект на протяжении жизни компонента. Обычно он выглядит так: { current: value }. React сохраняет этот объект между рендерами. Вы можете положить туда значение и прочитать его позже.

Главное для ответа: изменение ref.current не запускает новый render. React не считает такую мутацию изменением состояния. Поэтому ref хорош для данных, которые нужны коду, но не должны напрямую менять то, что пользователь видит на экране.

Короткая формулировка для интервью:

useRef хранит мутабельное значение между рендерами. Его current можно менять без ререндера. Поэтому ref используют для DOM-элементов и служебных данных. Если значение должно обновить UI, я выберу state, а не ref.

Где useRef полезен на практике

Самый понятный сценарий это доступ к DOM-элементу после монтирования. Например, можно поставить фокус в input по клику. Не вызывайте метод DOM во время render. Элемента еще может не быть.

import { useRef } from "react";

export function SearchBox() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  function focusSearch() {
    inputRef.current?.focus();
  }

  return (
    <>
      <label htmlFor="search">Search</label>
      <input id="search" ref={inputRef} type="search" />
      <button type="button" onClick={focusSearch}>
        Focus search
      </button>
    </>
  );
}

Здесь inputRef.current сначала равен null, а после монтирования React запишет туда DOM-узел. Проверка через ?. защищает от ситуации, когда элемент условно не отрисован или уже размонтирован. У поля есть видимый label, поэтому фокус не ломает доступность для клавиатуры и скринридеров.

Второй частый сценарий это служебное значение, которое не нужно показывать в UI. Например, id таймера для debounce и AbortController для отмены старого запроса. Если хранить их в state, вы получите лишний render. Если хранить в обычной переменной, значение потеряется при следующем render.

import { useEffect, useRef } from "react";

export function Autosave({ draft }: { draft: string }) {
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const abortRef = useRef<AbortController | null>(null);

  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;

    timeoutRef.current = setTimeout(() => {
      void fetch("/api/draft", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ draft }),
        signal: controller.signal,
      }).catch((error) => {
        if (error instanceof DOMException && error.name === "AbortError") {
          return;
        }

        console.error("Autosave failed", error);
      });
    }, 500);

    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      controller.abort();
    };
  }, [draft]);

  return null;
}

Практический вывод: ref помогает не терять id таймера и контроллер отмены между рендерами, но не освобождает от cleanup. Без очистки старый таймер может отправить лишний запрос. Без отмены запроса старый ответ может победить более новый, перезаписать UI устаревшими данными или сработать после размонтирования компонента. Отмену отличайте от настоящей ошибки. В реальном интерфейсе ошибку автосохранения стоит показать пользователю или повторить безопасно.

Как выбрать между ref и другими хуками

На интервью полезно не просто дать определение, а показать критерий выбора. Так вы показываете, что различаете реактивные данные и служебную мутацию.

Что выбрать вместо угадывания

1Значение должно быть видно в UI?
Используйте useState или внешний store, потому что нужен рендер.
2Нужно сохранить служебное значение между рендерами?
useRef подходит для таймера, предыдущего значения, AbortController или флага актуальности.
3Нужно вызвать метод DOM-элемента?
Создайте ref, передайте его в JSX и обращайтесь к current после монтирования.
4Нужно кешировать дорогое вычисление для render?
Чаще нужен useMemo, а не ref, чтобы зависеть от входных данных явно.
5Нужно передать управление дочернему компоненту?
Используйте forwardRef и при необходимости useImperativeHandle.

Главная ловушка: ref не обновляет интерфейс

Вот типичная ошибка: счетчик хранится в ref, а вы ждете, что число на экране изменится.

import { useRef } from "react";

export function BadCounter() {
  const countRef = useRef(0);

  return (
    <button
      type="button"
      onClick={() => {
        countRef.current += 1;
      }}
    >
      Count: {countRef.current}
    </button>
  );
}

Клик меняет countRef.current, но render не запускается. Пользователь продолжит видеть старое число. Исправление простое. Если счетчик является частью UI, используйте useState.

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button type="button" onClick={() => setCount((value) => value + 1)}>
      Count: {count}
    </button>
  );
}

Жизненный цикл ref и безопасные действия

Ref на DOM-элемент заполняется после того, как React привязал узел к дереву. При размонтировании или исчезновении элемента из-за условного render значение снова может стать null. Поэтому в сильном ответе есть проверка current и правильное место для побочного действия.

Для служебных ресурсов правило похожее. Ref хранит ссылку на ресурс, но сам ресурс нужно завершать вручную. Это особенно важно для таймеров, подписок, запросов и сторонних виджетов.

Безопасно
  1. 1Создать ref с понятным начальным значением.
  2. 2Передать ref в DOM-элемент или хранить в нем служебное значение.
  3. 3Обращаться к ref.current в обработчике или эффекте.
  4. 4Проверять null перед вызовом DOM-методов.
  5. 5Очищать таймеры, подписки и запросы в cleanup.
Опасно
  1. 1Менять ref.current и ждать обновления интерфейса.
  2. 2Вызывать метод DOM во время render.
  3. 3Не проверять ref.current перед focus или scroll.
  4. 4Хранить в ref данные формы, которые должен видеть пользователь.
  5. 5Оставлять старый таймер или запрос после размонтирования.

Предыдущее значение через useRef

Ref часто используют, чтобы запомнить предыдущее значение prop или state. Это не замена state, потому что предыдущее значение обычно нужно только для сравнения или эффекта, а не как самостоятельный источник UI.

import { useEffect, useRef } from "react";

export function PriceChange({ price }: { price: number }) {
  const previousPriceRef = useRef<number | null>(null);

  useEffect(() => {
    previousPriceRef.current = price;
  }, [price]);

  const previousPrice = previousPriceRef.current;

  return (
    <p>
      Current: {price}, previous: {previousPrice ?? "unknown"}
    </p>
  );
}

Здесь важно понимать порядок. Во время render вы видите значение ref из прошлого commit. После commit эффект запишет новое значение. Это нормальный паттерн, если вам нужен именно previous value. Но не превращайте его в скрытое состояние всего компонента.

SSR и собственные компоненты

Сам useRef можно вызывать при серверном рендеринге, как и другие хуки. Но DOM на сервере отсутствует. Значит, нельзя рассчитывать, что ref.current содержит DOM-узел до монтирования на клиенте.

Если вам нужно работать с window, измерять элемент, вызывать focus или подключать сторонний DOM-виджет, делайте это после монтирования. Обычно подходит обработчик события или useEffect. На SSR это снижает риск ошибки из-за отсутствия браузерного API.

Еще один нюанс: ref не проходит внутрь вашего компонента автоматически так же, как обычный prop. Если родитель должен получить доступ к внутреннему DOM-элементу, дочерний компонент должен явно поддержать это через forwardRef.

import { forwardRef } from "react";

const TextInput = forwardRef<HTMLInputElement, { label: string }>(
  function TextInput({ label }, ref) {
    return (
      <label>
        {label}
        <input ref={ref} />
      </label>
    );
  }
);

На интервью достаточно сказать, что обычный ref на DOM и ref на пользовательский компонент это не одно и то же. Для публичного imperative API есть useImperativeHandle. Применяйте его точечно, когда props не подходят.

Практический вывод

Хороший ответ про useRef строится вокруг контракта. Ref сохраняет значение между рендерами, не вызывает render при изменении и подходит для мутабельных служебных данных. Это делает его полезным. Но ref опасен, если использовать его как скрытый state.

Если хотите звучать сильнее, добавьте границы. ref.current может быть null. DOM доступен только после монтирования. Мутации ref не отслеживаются зависимостями useEffect. Ресурсы в ref требуют cleanup. Такой ответ показывает не только знание API, но и понимание багов из реального frontend-кода.

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

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Использовать ref вместо состояния UI

    Если вы храните в useRef значение инпута, счетчик или открытость модального окна, интерфейс не обновится после изменения current. Пользователь увидит старое состояние. На деле React не узнает, что JSX нужно пересчитать. Для данных, которые влияют на JSX, используйте useState или store.
  2. 2

    Обращаться к DOM-ref слишком рано

    На первом render ref.current для DOM-элемента еще может быть null. Ошибка часто всплывает при вызове inputRef.current.focus() без проверки. Делайте это в обработчике, эффекте после монтирования или проверяйте if (inputRef.current).
  3. 3

    Ждать, что ref запустит useEffect

    Изменение ref.current не является реактивным сигналом для React. Если положить ref.current в зависимости эффекта, это не сделает ref полноценным состоянием. Для реактивного поведения используйте state, reducer или событие, которое явно запускает обновление.
  4. 4

    Не чистить ресурсы, сохраненные в ref

    Таймер, подписка или AbortController в ref переживают рендеры, но не должны жить без контроля после размонтирования. Иначе возможны утечки, лишние запросы и вызовы устаревших callback. Возвращайте cleanup из useEffect и сбрасывайте ресурс, если он больше не нужен.
  5. 5

    Передавать ref в обычный компонент как будто это DOM

    Ref на собственный компонент не попадет внутрь DOM-элемента автоматически. Если родителю нужен доступ к внутреннему input, дочерний компонент должен использовать forwardRef. Если нужно открыть только несколько методов, добавьте useImperativeHandle. Так вы не раскрываете всю внутреннюю разметку.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание useRef, DOM, state и жизненного цикла React.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

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

Содержание