Menu

forwardRef в React: передача ref и ref как пропс в React 19

forwardRef позволяет компоненту принять ref от родителя и прикрепить его к узлу DOM внутри. В React 19 функциональные компоненты получают ref как обычный пропс, поэтому новому коду forwardRef больше не нужен. Обе версии в работе, а также useImperativeHandle и типы TypeScript.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

forwardRef позволяет родительскому компоненту передать ref через ваш компонент к элементу DOM внутри него, чтобы родитель мог вызывать на этом элементе focus(), scrollIntoView() и похожие методы. В React 19 он больше не нужен: функциональные компоненты получают ref как обычный пропс. forwardRef по-прежнему работает, и вы увидите его в большей части существующего кода, поэтому эта страница показывает оба варианта.

forwardRef принимает функцию рендера с двумя аргументами: props и ref, переданный родителем. ref не входит в props; он приходит отдельно. Нажмите Focus, и консоль подтвердит, что inputRef.current это настоящий узел <input>.

Способ React 19: ref это пропс

Начиная с React 19 функциональный компонент получает ref в пропсах, как любой другой пропс. Ни обёртки, ни второго аргумента:

Напишите что-нибудь и нажмите Log value: родитель читает текущий текст поля прямо из узла DOM. Удалите ref={ref} у <input> и нажмите Focus: консоль покажет ошибку, потому что inputRef.current теперь null.

forwardRef (все версии)Пропс ref (React 19+)
Как компонент получает refВторым аргументом, (props, ref)props.ref
Нужна обёрткаДаНет
Работает с useImperativeHandleДаДа
СтатусРаботает, планируется к устареваниюТак пишут новый код

Ref, который дочерний компонент игнорирует, остаётся null

Передача ref в компонент сама по себе ничего не делает. Компонент должен поставить его на элемент. Если он этого не делает, ref.current остаётся null:

Консоль показывает Forgetful ref: null и Careful ref: INPUT. Это самая частая причина «ref.current is null» на пользовательском компоненте. До React 19 Careful тоже был бы null, если бы не был обёрнут в forwardRef, потому что ref вообще не передавался в функцию.

useImperativeHandle: открыть меньший API

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

Последняя кнопка выводит ["focus", "clear"]: родитель получает эти два метода и больше ничего, не узел поля. Добавьте в объект метод select() и кнопку, которая его вызывает, и родитель получит ровно одну новую возможность.

Дочерний компонент хранит собственный ref, inputRef, для настоящего поля, а handle оборачивает его. Третий аргумент это массив зависимостей, как у useEffect: handle пересоздаётся, когда эти значения меняются.

Прибегайте к этому экономно. Большинство того, что нужно родителю (открыть, закрыть, показать ошибку), лучше делать пропсами, например isOpen или error. Императивные методы нужны для действий, у которых нет естественного пропса: фокус, прокрутка, воспроизведение видео, запуск анимации.

Использование ref и внутри дочернего компонента

Иногда дочернему компоненту нужен тот же узел DOM для собственной работы, например чтобы измерить его или поставить фокус после ошибки, а родитель тоже держит на него ref. Атрибут ref может принимать только одно значение, поэтому объедините два через ref-колбэк:

function AutoGrowTextarea({ ref, ...props }) {
    const localRef = useRef(null);

    function setRefs(node) {
        localRef.current = node;
        if (typeof ref === 'function') ref(node);
        else if (ref) ref.current = node;
    }

    return <textarea ref={setRefs} {...props} />;
}

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

Передача ref через несколько уровней

Ref передаётся по одному компоненту за раз. Если Form рендерит Field, который рендерит TextInput, который рендерит <input>, каждый из этих компонентов должен передать ref следующему. В React 19 это ещё один пропс для передачи (<TextInput ref={ref} />); с forwardRef каждый уровень приходилось оборачивать. Spread пропсов ({...props}) в старых версиях его не переносит, потому что до React 19 ref никогда не был частью props.

Обычно это важно в компонентах дизайн-системы: Button, Input или Select, которые оборачивают нативный элемент, должны передавать свой ref, чтобы использующее их приложение могло поставить на них фокус, измерить их или отдать библиотеке, которая позиционирует всплывающие окна.

Миграция с forwardRef

Изменение механическое: уберите обёртку и читайте ref из пропсов.

// Before
const Button = forwardRef(function Button({ variant, ...props }, ref) {
    return <button ref={ref} className={variant} {...props} />;
});

// After (React 19)
function Button({ variant, ref, ...props }) {
    return <button ref={ref} className={variant} {...props} />;
}

Спешить некуда. forwardRef продолжает работать в React 19, а библиотека, которая должна поддерживать React 18, обязана его сохранить, потому что React 18 не передаёт ref как пропс. Классовых компонентов это не касается: ref на классовом компоненте по-прежнему указывает на экземпляр компонента.

TypeScript

С forwardRef аргументы типа идут в порядке: тип ref, затем пропсы:

import { forwardRef } from 'react';

type FancyInputProps = { label: string };

const FancyInput = forwardRef<HTMLInputElement, FancyInputProps>(
    function FancyInput({ label }, ref) {
        return <input ref={ref} aria-label={label} />;
    }
);

В React 19 типизируйте ref как любой другой пропс. ComponentProps<'input'> его уже включает:

import { useImperativeHandle, useRef, type ComponentProps, type Ref } from 'react';

function FancyInput(props: ComponentProps<'input'>) {
    return <input {...props} />;
}

type SearchHandle = { focus: () => void; clear: () => void };

function SearchBox({ ref }: { ref?: Ref<SearchHandle> }) {
    const inputRef = useRef<HTMLInputElement>(null);
    useImperativeHandle(ref, () => ({
        focus: () => inputRef.current?.focus(),
        clear: () => {
            if (inputRef.current) inputRef.current.value = '';
        },
    }));
    return <input ref={inputRef} />;
}

// In the parent
const searchRef = useRef<SearchHandle>(null);

Страница о useRef разбирает сами рефы: доступ к DOM, значения, которые сохраняются без рендера, и ref-колбэки. О типизации компонентов в целом см. React с TypeScript.

Часто задаваемые вопросы

Что делает forwardRef в React?

Он оборачивает функциональный компонент так, что ref, переданный родителем, доходит до компонента вторым аргументом, (props, ref). Затем компонент ставит этот ref на узел DOM, чтобы родитель мог вызывать на нём методы вроде focus().

forwardRef устарел в React 19?

Пока нет, и он по-прежнему работает. React 19 передаёт ref функциональным компонентам как обычный пропс, поэтому новому коду он не нужен, а команда React сообщила, что планирует объявить forwardRef устаревшим в будущей версии.

Почему мой ref равен null на пользовательском компоненте?

Компонент получил ref, но не поставил его ни на один элемент. Прикрепите его к узлу DOM внутри: <input ref={ref} />. До React 19 ref вообще не передавался, если компонент не был обёрнут в forwardRef.

Для чего нужен useImperativeHandle?

Он заменяет то, что родитель видит в ref.current. Вместо всего узла DOM вы возвращаете объект только с выбранными методами, например focus и clear.

Получают ли классовые компоненты ref как пропс в React 19?

Нет. Ref на классовом компоненте по-прежнему указывает на экземпляр компонента. Изменение касается только функциональных компонентов.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ