Интервью-вопрос
Что такое хук useRef?
useRef хранит мутабельное значение между рендерами и не запускает рендер при изменении current. Главный риск в том, что ref легко перепутать со state и получить устаревший UI или обращение к DOM до монтирования.
- Добавлен
- Редакция
Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.
Мини-квиз
Проверка перед разбором
Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.
Вопрос 1 из 50 правильно
Разбор
Разобраться, а не зазубрить
Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.
Базовая идея
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 и другими хуками
На интервью полезно не просто дать определение, а показать критерий выбора. Так вы показываете, что различаете реактивные данные и служебную мутацию.
Что выбрать вместо угадывания
Используйте useState или внешний store, потому что нужен рендер.useRef подходит для таймера, предыдущего значения, AbortController или флага актуальности.Создайте ref, передайте его в JSX и обращайтесь к current после монтирования.Чаще нужен useMemo, а не ref, чтобы зависеть от входных данных явно.Используйте 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Создать ref с понятным начальным значением.
- 2Передать ref в DOM-элемент или хранить в нем служебное значение.
- 3Обращаться к ref.current в обработчике или эффекте.
- 4Проверять null перед вызовом DOM-методов.
- 5Очищать таймеры, подписки и запросы в cleanup.
- 1Менять ref.current и ждать обновления интерфейса.
- 2Вызывать метод DOM во время render.
- 3Не проверять ref.current перед focus или scroll.
- 4Хранить в ref данные формы, которые должен видеть пользователь.
- 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
Использовать ref вместо состояния UI
Если вы храните вuseRefзначение инпута, счетчик или открытость модального окна, интерфейс не обновится после измененияcurrent. Пользователь увидит старое состояние. На деле React не узнает, что JSX нужно пересчитать. Для данных, которые влияют на JSX, используйтеuseStateили store. - 2
Обращаться к DOM-ref слишком рано
На первом renderref.currentдля DOM-элемента еще может бытьnull. Ошибка часто всплывает при вызовеinputRef.current.focus()без проверки. Делайте это в обработчике, эффекте после монтирования или проверяйтеif (inputRef.current). - 3
Ждать, что ref запустит useEffect
Изменениеref.currentне является реактивным сигналом для React. Если положитьref.currentв зависимости эффекта, это не сделает ref полноценным состоянием. Для реактивного поведения используйте state, reducer или событие, которое явно запускает обновление. - 4
Не чистить ресурсы, сохраненные в ref
Таймер, подписка илиAbortControllerв ref переживают рендеры, но не должны жить без контроля после размонтирования. Иначе возможны утечки, лишние запросы и вызовы устаревших callback. Возвращайте cleanup изuseEffectи сбрасывайте ресурс, если он больше не нужен. - 5
Передавать ref в обычный компонент как будто это DOM
Ref на собственный компонент не попадет внутрь DOM-элемента автоматически. Если родителю нужен доступ к внутреннему input, дочерний компонент должен использоватьforwardRef. Если нужно открыть только несколько методов, добавьтеuseImperativeHandle. Так вы не раскрываете всю внутреннюю разметку.
Follow-up
Что могут спросить дальше
Короткие ответы на вопросы, которыми проверяют понимание useRef, DOM, state и жизненного цикла React.
Живые ответы
Видео с похожим вопросом
Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.
Пока видео нет. Когда появятся подходящие публичные интервью, добавим их в этот блок, чтобы можно было сравнить разбор с тем, как отвечают реальные кандидаты.
Какой опыт использования Interface
Разбор вопроса «Какой опыт использования Interface» для Frontend Developer: что проверяет интервьюер, ключевые тезисы, практические примеры и частые ошибки.
Что такое HOC в React 😎
HOC в React это функция, которая принимает компонент и возвращает новый компонент с добавленной логикой. Разбираем, когда этот паттерн уместен, чем он отличается от хуков и какие ловушки важно назвать на интервью.