ReactSeniorTechnical

В чём разница между 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): без forwardRef ref не дойдёт до дочернего компонента.
  • Нет реактивности: изменение ref.current.someValue не вызывает ре-рендер — для реактивных данных используйте state/props.
  • Тестирование сложнее: RTL рекомендует тестировать через пользовательские взаимодействия, а не через ref-методы.

Common mistakes

  • Выставлять императивное API там, где достаточно пропсов.
  • Забыть зависимости и оставлять stale-замыкания в методах.
  • Возвращать в handle обычные значения вместо стабильных функций.

What the interviewer is testing

  • Понимает ли мотив инкапсуляции.
  • Знает ли изменение API ref в React 19.
  • Может ли назвать сценарии, где обычный ref достаточен.

Sources

Related topics