Menu

React forwardRef: przekazywanie refów i ref jako prop w 19

forwardRef pozwala komponentowi przyjąć ref od rodzica i podłączyć go do węzła DOM w środku. W React 19 komponenty funkcyjne dostają ref jako zwykły prop, więc nowy kod nie potrzebuje już forwardRef. Zobacz obie wersje w działaniu, a do tego useImperativeHandle i typy TypeScript.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

forwardRef pozwala komponentowi rodzica przekazać ref przez twój komponent do elementu DOM w jego środku, żeby rodzic mógł wywoływać na tym elemencie focus(), scrollIntoView() i podobne metody. W React 19 już go nie potrzebujesz: komponenty funkcyjne dostają ref jako zwykły prop. forwardRef nadal działa i zobaczysz go w większości istniejącego kodu, więc ta strona pokazuje oba sposoby.

forwardRef przyjmuje funkcję renderującą z dwoma argumentami: props i ref przekazanym przez rodzica. ref nie jest w props; przychodzi osobno. Kliknij Focus, a konsola potwierdzi, że inputRef.current to prawdziwy węzeł <input>.

Sposób z React 19: ref to prop

Od React 19 komponent funkcyjny dostaje ref w propsach jak każdy inny prop. Bez opakowania i bez drugiego argumentu:

Wpisz coś i kliknij Log value: rodzic odczytuje bieżący tekst pola prosto z węzła DOM. Usuń ref={ref} z <input> i kliknij Focus: konsola pokazuje błąd, bo inputRef.current ma teraz wartość null.

forwardRef (wszystkie wersje)Prop ref (React 19+)
Jak komponent dostaje refDrugi argument, (props, ref)props.ref
Czy potrzebne opakowanieTakNie
Działa z useImperativeHandleTakTak
StatusDziała, planowane oznaczenie jako przestarzałeSposób na nowy kod

Ref zignorowany przez dziecko zostaje null

Samo przekazanie ref do komponentu nic nie robi. Komponent musi umieścić go na elemencie. Jeśli tego nie zrobi, ref.current zostaje null:

Konsola pokazuje Forgetful ref: null i Careful ref: INPUT. To najczęstszy powód „ref.current is null" na własnym komponencie. Przed React 19 Careful też byłby null, gdyby nie był opakowany w forwardRef, bo ref nigdy nie trafiał do funkcji.

useImperativeHandle: mniejsze API dla rodzica

Oddanie rodzicowi całego węzła DOM oznacza, że może z nim zrobić wszystko: zmieniać style, usuwać dzieci, odczytywać wartości, które chciałeś zachować prywatne. useImperativeHandle pozwala ci zamiast tego zdecydować, co zawiera ref.current:

Ostatni przycisk wypisuje ["focus", "clear"]: rodzic dostaje te dwie metody i nic więcej, a nie węzeł pola. Dodaj do obiektu metodę select() i przycisk, który ją wywołuje, a rodzic zyska dokładnie jedną możliwość więcej.

Dziecko trzyma własny ref, inputRef, do prawdziwego pola, a uchwyt (handle) go opakowuje. Trzeci argument to tablica zależności, jak w useEffect: uchwyt jest budowany od nowa, gdy te wartości się zmieniają.

Sięgaj po to oszczędnie. Większość tego, czego chce rodzic (otwórz, zamknij, pokaż błąd), lepiej sprawdza się jako propsy, takie jak isOpen albo error. Metody imperatywne są dla akcji, które nie mają naturalnego propsa: ustawienie fokusu, przewijanie, odtwarzanie wideo, uruchomienie animacji.

Używanie refa także wewnątrz dziecka

Czasem dziecko potrzebuje tego samego węzła DOM do własnej pracy, na przykład żeby go zmierzyć albo ustawić na nim fokus po błędzie, a rodzic też trzyma do niego ref. Jeden atrybut ref może przyjąć tylko jedną wartość, więc połącz oba przez ref callback:

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 rodzica może być obiektem z useRef albo funkcją, więc obsłuż oba przypadki. Gdy rodzic potrzebuje tylko kilku akcji, czystszym wyborem jest useImperativeHandle powyżej, bo dziecko zachowuje węzeł dla siebie.

Przekazywanie refa przez kilka warstw

Ref podróżuje po jednym komponencie naraz. Jeśli Form renderuje Field, który renderuje TextInput, który renderuje <input>, każdy z tych komponentów musi przekazać ref dalej. W React 19 to jeden prop więcej do przekazania (<TextInput ref={ref} />); z forwardRef każdą warstwę trzeba było opakować. Spread propsów ({...props}) nie przenosi go w starszych wersjach, bo przed React 19 ref nigdy nie był częścią props.

Zwykle ma to znaczenie w komponentach design systemu: Button, Input albo Select, który opakowuje natywny element, powinien przekazywać swój ref, żeby aplikacja, która go używa, mogła ustawić na nim fokus, zmierzyć go albo przekazać bibliotece pozycjonującej popovery.

Migracja z forwardRef

Zmiana jest mechaniczna: usuń opakowanie i odczytuj ref z propsów.

// 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} />;
}

Nie ma pośpiechu. forwardRef dalej działa w React 19, a biblioteka, która musi obsługiwać React 18, musi go zachować, bo React 18 nie przekazuje ref jako propsa. Komponentów klasowych to nie dotyczy: ref na komponencie klasowym nadal wskazuje na instancję komponentu.

TypeScript

Przy forwardRef argumenty typu podaje się w kolejności: typ refa, potem propsy:

import { forwardRef } from 'react';

type FancyInputProps = { label: string };

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

W React 19 typujesz ref jak każdy inny prop. ComponentProps<'input'> już go zawiera:

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);

Strona o useRef omawia same refy: dostęp do DOM, wartości, które trwają bez renderowania, i ref callback. O typowaniu komponentów ogólnie przeczytasz na stronie React z TypeScriptem.

Najczęściej zadawane pytania

Co robi forwardRef w React?

Opakowuje komponent funkcyjny tak, że ref podany przez rodzica trafia do komponentu jako drugi argument, (props, ref). Komponent umieszcza potem ten ref na węźle DOM, więc rodzic może wywoływać na nim metody takie jak focus().

Czy forwardRef jest przestarzałe w React 19?

Jeszcze nie i nadal działa. React 19 przekazuje ref do komponentów funkcyjnych jako zwykły prop, więc nowy kod go nie potrzebuje, a zespół Reacta zapowiedział, że w przyszłej wersji planuje oznaczyć forwardRef jako przestarzałe.

Dlaczego mój ref na własnym komponencie ma wartość null?

Komponent dostał ref, ale nie umieścił go na żadnym elemencie. Podłącz go do węzła DOM w środku: <input ref={ref} />. Przed React 19 ref w ogóle nie był przekazywany, chyba że komponent był opakowany w forwardRef.

Do czego służy useImperativeHandle?

Zastępuje to, co rodzic widzi w ref.current. Zamiast całego węzła DOM zwracasz obiekt tylko z wybranymi metodami, na przykład focus i clear.

Czy komponenty klasowe dostają ref jako prop w React 19?

Nie. Ref na komponencie klasowym nadal wskazuje na instancję komponentu. Zmiana dotyczy tylko komponentów funkcyjnych.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ