В чём разница между useImperativeHandle и обычным ref?
useImperativeHandle позволяет родителю вызывать конкретные методы дочернего компонента через ref, не открывая весь DOM-узел. Обычный ref возвращает сырой DOM-элемент; useImperativeHandle возвращает объект с явно заданным публичным API.
Обычный ref и его ограничения
Когда родитель передаёт ref на <input>, он получает прямой доступ к DOM-узлу и может вызвать любой метод: ref.current.focus(), изменить value, вызвать click() и т.д. Для нативных элементов это приемлемо, но для составных компонентов — означает утечку деталей реализации.
useImperativeHandle — явный контракт
useImperativeHandle(ref, factory, deps) заменяет то, что видит родитель через ref.current, объектом, возвращённым из factory. Это позволяет предоставить минимальный, стабильный API.
// FancyInput.tsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
export interface FancyInputHandle {
focus: () => void;
clear: () => void;
}
export const FancyInput = forwardRef<FancyInputHandle, { label: string }>(
function FancyInput({ label }, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
clear() {
if (inputRef.current) inputRef.current.value = '';
},
}), []);
return (
<label>
{label}
<input ref={inputRef} type="text" />
</label>
);
}
);
Использование в родителе
import { useRef } from 'react';
import { FancyInput, FancyInputHandle } from './FancyInput';
export function Form() {
const inputRef = useRef<FancyInputHandle>(null);
return (
<>
<FancyInput ref={inputRef} label="Поиск" />
<button onClick={() => inputRef.current?.focus()}>Фокус</button>
<button onClick={() => inputRef.current?.clear()}>Очистить</button>
</>
);
}
Родитель может вызвать только focus() и clear() — весь DOM-узел скрыт.
React 19: ref как обычный prop
В React 19 forwardRef больше не нужен — ref передаётся как обычный prop. useImperativeHandle остаётся, но синтаксис упрощается:
// React 19 — без forwardRef
import { useImperativeHandle, useRef } from 'react';
export function FancyInput({
label,
ref,
}: {
label: string;
ref?: React.Ref<FancyInputHandle>;
}) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { if (inputRef.current) inputRef.current.value = ''; },
}), []);
return <input ref={inputRef} placeholder={label} />;
}
Когда это оправдано
- Медиаплееры:
play(),pause(),seek(time). - Модальные окна:
open(),close(). - Сложные редакторы:
undo(),insertText(str). - Анимации через imperative API (GSAP, Framer Motion imperatives).
Подводные камни
- Злоупотребление imperative API: React декларативен — imperativeHandle нужен только тогда, когда декларативный подход неудобен (медиа, фокус, анимации). Бизнес-логику туда не кладут.
- Забыли deps: если фабрика использует пропы или состояние, их нужно перечислить в deps; иначе
ref.currentбудет содержать устаревшее замыкание. - Тип ref в TypeScript: родитель должен объявить
useRef<FancyInputHandle>, а неuseRef<HTMLInputElement>; несовпадение типов приводит к ошибкам компиляции. - Работает только с forwardRef (до React 19): без
forwardRefref не дойдёт до дочернего компонента. - Нет реактивности: изменение
ref.current.someValueне вызывает ре-рендер — для реактивных данных используйте state/props. - Тестирование сложнее: RTL рекомендует тестировать через пользовательские взаимодействия, а не через ref-методы.
Common mistakes
- Выставлять императивное API там, где достаточно пропсов.
- Забыть зависимости и оставлять stale-замыкания в методах.
- Возвращать в handle обычные значения вместо стабильных функций.
What the interviewer is testing
- Понимает ли мотив инкапсуляции.
- Знает ли изменение API ref в React 19.
- Может ли назвать сценарии, где обычный ref достаточен.