React forwardRef — передача рефов дочерним компонентам

17 июля 2026
Автор

Антон Ларичев

Зачем нужен forwardRef

В React рефы (ref) — это способ получить прямой доступ к DOM-элементу или к экземпляру компонента. Когда вы создаёте реф через useRef и передаёте его в JSX-атрибут ref, React сам подключает его к нужному узлу.

Проблема возникает, когда вы хотите передать реф в пользовательский компонент, а не напрямую в DOM-элемент. По умолчанию React не проксирует ref внутрь компонента — он зарезервирован и недоступен через props.

function Input(props) {
  // props.ref здесь undefined — реф не пробрасывается
  return <input {...props} />;
}

function Form() {
  const inputRef = useRef(null);

  return <Input ref={inputRef} />; // ref до Input не дойдёт
}

Для решения этой проблемы React предоставляет forwardRef — обёртку, которая явно указывает, что компонент умеет принимать и пробрасывать ref.

Базовый синтаксис

forwardRef принимает функцию рендера с двумя аргументами: props и ref. Внутри функции вы можете прикрепить полученный ref к любому DOM-элементу или другому компоненту.

import { forwardRef, useRef } from 'react';

const Input = forwardRef(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});

function Form() {
  const inputRef = useRef(null);

  function handleFocus() {
    inputRef.current.focus();
  }

  return (
    <>
      <Input ref={inputRef} placeholder="Введите текст" />
      <button onClick={handleFocus}>Сфокусировать</button>
    </>
  );
}

Теперь inputRef.current указывает на DOM-элемент <input> внутри компонента Input. Родитель получает прямой доступ к нему.

Как это работает под капотом

forwardRef возвращает React-компонент с особым типом ($$typeof: REACT_FORWARD_REF_TYPE). При монтировании React видит этот тип и передаёт ref вторым аргументом в функцию рендера, а не помещает его в props.

Важно понимать: ref из forwardRef — это не обычный prop. Он не попадает в объект props и не виден снаружи через деструктуризацию. Это намеренное разделение ответственности.

Использование с TypeScript

При работе с TypeScript forwardRef требует явной типизации через два дженерик-параметра: тип элемента (куда прикрепляется реф) и тип props.

import { forwardRef, useRef } from 'react';

interface InputProps {
  placeholder?: string;
  disabled?: boolean;
  onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
  { placeholder, disabled, onChange },
  ref
) {
  return (
    <input
      ref={ref}
      placeholder={placeholder}
      disabled={disabled}
      onChange={onChange}
    />
  );
});

function Form() {
  const inputRef = useRef<HTMLInputElement>(null);

  function handleSubmit() {
    if (inputRef.current) {
      console.log('Значение:', inputRef.current.value);
    }
  }

  return (
    <>
      <Input ref={inputRef} placeholder="Имя пользователя" />
      <button onClick={handleSubmit}>Отправить</button>
    </>
  );
}

Первый дженерик HTMLInputElement определяет тип, к которому будет привязан реф. Второй дженерик InputProps — интерфейс props компонента. TypeScript проверит совместимость при использовании inputRef.current.

Стрелочные функции с forwardRef

Альтернативный стиль — стрелочная функция. Компонент при этом получает имя через displayName, что важно для удобочитаемости в React DevTools.

const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});

Input.displayName = 'Input';

Без displayName в DevTools компонент отображается как ForwardRef, что затрудняет отладку. Всегда устанавливайте displayName при использовании анонимных стрелочных функций.

useImperativeHandle — управление тем, что видит родитель

По умолчанию forwardRef раскрывает весь DOM-узел. Иногда нужно ограничить API: предоставить только определённые методы, скрыв детали реализации. Для этого используют useImperativeHandle.

import { forwardRef, useRef, useImperativeHandle } from 'react';

interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seek: (seconds: number) => void;
}

interface VideoPlayerProps {
  src: string;
}

const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  function VideoPlayer({ src }, ref) {
    const videoRef = useRef<HTMLVideoElement>(null);

    useImperativeHandle(ref, () => ({
      play() {
        videoRef.current?.play();
      },
      pause() {
        videoRef.current?.pause();
      },
      seek(seconds: number) {
        if (videoRef.current) {
          videoRef.current.currentTime = seconds;
        }
      },
    }));

    return <video ref={videoRef} src={src} />;
  }
);

function App() {
  const playerRef = useRef<VideoPlayerHandle>(null);

  return (
    <>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <button onClick={() => playerRef.current?.play()}>Воспроизвести</button>
      <button onClick={() => playerRef.current?.pause()}>Пауза</button>
      <button onClick={() => playerRef.current?.seek(30)}>+30 сек</button>
    </>
  );
}

Теперь родитель видит только три метода (play, pause, seek). Весь DOM-узел <video> скрыт — инкапсуляция соблюдена. Обратите внимание: тип рефа в родителе — VideoPlayerHandle, а не HTMLVideoElement.

Третий аргумент useImperativeHandle — зависимости

useImperativeHandle принимает необязательный третий аргумент — массив зависимостей, аналогично useEffect. Если объект дескриптора зависит от props или state, укажите их.

useImperativeHandle(
  ref,
  () => ({
    focus() {
      inputRef.current?.focus();
    },
  }),
  [] // пересоздавать объект не нужно — методы не зависят от переменных
);

Практические сценарии применения

Управление фокусом в составных компонентах

Когда форма содержит кастомный компонент поля ввода, родителю часто нужно управлять фокусом — например, после ошибки валидации.

const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
  function TextField({ label, error, ...inputProps }, ref) {
    return (
      <div className="field">
        <label>{label}</label>
        <input ref={ref} className={error ? 'error' : ''} {...inputProps} />
        {error && <span className="error-text">{error}</span>}
      </div>
    );
  }
);

function RegistrationForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const [emailError, setEmailError] = useState('');

  function validate() {
    if (!emailRef.current?.value.includes('@')) {
      setEmailError('Введите корректный email');
      emailRef.current?.focus(); // фокус на поле с ошибкой
    }
  }

  return (
    <form>
      <TextField
        ref={emailRef}
        label="Email"
        type="email"
        error={emailError}
      />
      <button type="button" onClick={validate}>
        Проверить
      </button>
    </form>
  );
}

Интеграция с анимационными библиотеками

Библиотеки вроде GSAP или Framer Motion требуют прямого доступа к DOM-узлам для управления анимациями.

import { forwardRef, useRef, useEffect } from 'react';
import gsap from 'gsap';

const AnimatedCard = forwardRef<HTMLDivElement, { children: React.ReactNode }>(
  function AnimatedCard({ children }, ref) {
    return (
      <div ref={ref} className="card">
        {children}
      </div>
    );
  }
);

function Gallery() {
  const cardRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (cardRef.current) {
      gsap.from(cardRef.current, {
        opacity: 0,
        y: 50,
        duration: 0.6,
      });
    }
  }, []);

  return (
    <AnimatedCard ref={cardRef}>
      <p>Контент карточки</p>
    </AnimatedCard>
  );
}

Цепочка рефов через несколько уровней

Если нужно передать реф через несколько уровней компонентов, каждый промежуточный компонент должен использовать forwardRef.

const BaseInput = forwardRef<HTMLInputElement, InputProps>(
  function BaseInput(props, ref) {
    return <input ref={ref} className="base-input" {...props} />;
  }
);

const StyledInput = forwardRef<HTMLInputElement, InputProps>(
  function StyledInput(props, ref) {
    // передаём ref дальше в BaseInput
    return <BaseInput ref={ref} className="styled-input" {...props} />;
  }
);

function Page() {
  const ref = useRef<HTMLInputElement>(null);
  // ref.current укажет на <input> внутри BaseInput
  return <StyledInput ref={ref} />;
}

Распространённые ошибки

Забыть forwardRef при передаче ref

Если передать ref в компонент без forwardRef, React выдаст предупреждение в режиме разработки, а ref.current останется null.

// Неправильно — ref не дойдёт до input
function Input(props) {
  return <input {...props} />;
}

// Правильно
const Input = forwardRef(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});

Не использовать ref из forwardRef

Получить ref вторым аргументом — лишь половина дела. Его нужно передать в JSX.

// Неправильно — ref принят, но не использован
const Input = forwardRef(function Input(props, ref) {
  return <input {...props} />; // ref потерян
});

// Правильно
const Input = forwardRef(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});

Мутировать ref напрямую без useImperativeHandle

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

// Неправильно
const Modal = forwardRef(function Modal(props, ref) {
  const [isOpen, setIsOpen] = useState(false);
  // Не делайте так — присвоение ref.current напрямую
  if (ref) ref.current = { open: () => setIsOpen(true) };
  return isOpen ? <div>...</div> : null;
});

// Правильно
const Modal = forwardRef(function Modal(props, ref) {
  const [isOpen, setIsOpen] = useState(false);

  useImperativeHandle(ref, () => ({
    open() { setIsOpen(true); },
    close() { setIsOpen(false); },
  }));

  return isOpen ? <div>...</div> : null;
});

forwardRef и React 19

В React 19 forwardRef объявлен устаревшим. Начиная с этой версии, ref передаётся как обычный prop и доступен в props.ref. Это упрощает код.

// React 19 — ref как обычный prop
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// Использование — без изменений
function Form() {
  const inputRef = useRef(null);
  return <Input ref={inputRef} />;
}

Если вы работаете с React 18 и ниже, forwardRef остаётся единственным способом пробросить ref. При обновлении на React 19 можно постепенно убирать forwardRef — обратная совместимость сохраняется, но появятся предупреждения о депрекации.

Итого

forwardRef решает конкретную задачу: позволяет родительскому компоненту получить прямой доступ к DOM-узлу или кастомному API внутри дочернего компонента. Используйте его, когда:

  • Вам нужно управлять фокусом, скроллом или воспроизведением из родителя.
  • Вы интегрируете сторонние библиотеки, требующие прямого доступа к DOM.
  • Вы строите переиспользуемую библиотеку компонентов, где пользователи рассчитывают работать с рефами.

Сочетайте forwardRef с useImperativeHandle, когда хотите скрыть детали реализации и предоставить чистый публичный API вместо голого DOM-узла.

Чтобы глубоко разобраться в React, его хуках и паттернах работы с компонентами, пройдите курс на PurpleSchool: React — полный курс.

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий