Przekazywanie referencji
Przekazywanie referencji (ang. ref forwarding) to technika, w której referencję do komponentu “podajemy dalej” do jego dziecka. Dla większości komponentów w aplikacji nie jest to potrzebne, jednak może okazać się przydatne w niektórych przypadkach, zwłaszcza w bibliotekach udostępniających uniwersalne komponenty. Najczęstsze scenariusze opisujemy poniżej.
Przekazywanie referencji do komponentów DOM
Rozważmy komponent FancyButton
, który renderuje natywny element DOM - przycisk:
function FancyButton(props) {
return (
<button className="FancyButton">
{props.children}
</button>
);
}
Komponenty reactowe ukrywają szczegóły swojej implementacji, w tym także wyrenderowany HTML.
Inne komponenty używające FancyButton
z reguły nie potrzebują mieć dostępu do referencji do wewnętrznego elementu button
.
Jest to korzystne, gdyż zapobiega sytuacji, w której komponenty są za bardzo uzależnione od struktury drzewa DOM innych komponentów.
Taka enkapsulacja jest pożądana na poziomie aplikacji, w komponentach takich jak FeedStory
czy Comment
. Natomiast może się okazać to niewygodne w przypadku komponentów wielokrotnego użytku, będących “liśćmi” drzewa. Np. FancyButton
albo MyTextInput
. Takie komponenty często używane są w wielu miejscach aplikacji, w podobny sposób jak zwyczajne elementy DOM typu button
i input
. W związku z tym, bezpośredni dostęp do ich DOM może okazać się konieczy, aby obsłużyć np. fokus, zaznaczenie czy animacje.
Przekazywanie referencji jest opcjonalną funkcjonalnością, która pozwala komponentom wziąć przekazaną do nich referencję i “podać ją dalej” do swojego dziecka.
W poniższym przykładzie FancyButton
używa React.forwardRef
, by przejąć przekazaną do niego referencję i przekazać ją dalej do elementu button
, który renderuje:
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children}
</button>
));
// Możesz teraz otrzymać bezpośrednią referencję do elementu „button”:
const ref = React.createRef();
<FancyButton ref={ref}>Kliknij mnie!</FancyButton>;
Tym sposobem komponenty używające FancyButton
mają referencję do elementu button
znajdującego się wewnątrz. Mogą więc, w razie potrzeby, operować na komponencie tak, jakby operowały bezpośrednio na natywnym elemencie DOM.
Oto wyjaśnienie krok po kroku, opisujące, co wydarzyło się w przykładzie powyżej:
- Tworzymy referencję reactową wywołując
React.createRef
i przypisujemy ją do stałejref
. - Przekazujemy
ref
do<FancyButton ref={ref}>
przypisując ją do atrybutu JSX. - Wewnątrz
forwardRef
React przekazujeref
do funkcji(props, ref) => ...
jako drugi argument. - Podajemy argument
ref
dalej do<button ref={ref}>
przypisując go do atrybutu JSX. - Gdy referencja zostanie zamontowana,
ref.current
będzie wskazywać na element DOM<button>
.
Uwaga
Drugi argument
ref
istnieje tylko, gdy definiujesz komponent przy pomocy wywołaniaReact.forwardRef
. Zwyczajna funkcja lub klasa nie dostanie argumenturef
, nawet jako jednej z właściwości (props
).Przekazywanie referencji nie jest ograniczone do elementów drzewa DOM. Możesz także przekazywać referencje do instancji komponentów klasowych.
Uwaga dla autorów bibliotek komponentów
Kiedy zaczniesz używać forwardRef
w swojej bibliotece komponentów, potraktuj to jako zmianę krytyczną (ang. breaking change). W efekcie biblioteka powinna zostać wydana w nowej “wersji głównej” (ang. major version, major release). Należy tak postąpić, ponieważ najprawdopodobniej twoja biblioteka zauważalnie zmieniła zachowanie (np. inaczej przypinając referencje i eksportując inne typy). Może to popsuć działanie aplikacji, które są zależne od dawnego zachowania.
Stosowanie React.forwardRef
warunkowo, gdy ono istnieje, także nie jest zalecane z tego samego powodu: zmienia to zachowanie biblioteki i może zepsuć działanie aplikacji użytkowników, gdy zmienią wersję Reacta.
Przekazywanie referencji w komponentach wyższego rzędu
Omawiana technika może okazać się wyjątkowo przydatna w komponentach wyższego rzędu (KWR; ang. Higher-Order Components lub HOC). Zacznijmy od przykładu KWR-a, który wypisuje w konsoli wszystkie właściwości komponentu:
function logProps(WrappedComponent) { class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('poprzednie właściwości:', prevProps);
console.log('nowe właściwości:', this.props);
}
render() {
return <WrappedComponent {...this.props} />; }
}
return LogProps;
}
KWR logProps
przekazuje wszystkie atrybuty do komponentu, który opakowuje, więc wyrenderowany wynik będzie taki sam. Na przykład, możemy użyć tego KWRa do logowania atrybutów, które zostaną przekazane do naszego komponentu FancyButton
:
class FancyButton extends React.Component {
focus() {
// ...
}
// ...
}
// Zamiast FancyButton eksportujemy LogProps.
// Jednak wyrenderowany zostanie FancyButton.
export default logProps(FancyButton);
Powyższe rozwiązanie ma jeden minus: referencje nie zostaną przekazane do komponentu. Dzieje się tak, ponieważ ref
nie jest atrybutem. Tak jak key
, jest on obsługiwany przez Reacta w inny sposób. Referencja będzie w tym wypadku odnosiła się do najbardziej zewnętrznego kontenera, a nie do opakowanego komponentu.
Oznacza to, że referencje przeznaczone dla naszego komponentu FancyButton
będą w praktyce wskazywać na komponent LogProps
.
import FancyButton from './FancyButton';
const ref = React.createRef();
// Komponent FancyButton, który zaimportowaliśmy, jest tak naprawdę KWR-em LogProps.
// Mimo że wyświetlony rezultat będzie taki sam,
// nasza referencja będzie wskazywała na LogProps zamiast na komponent FancyButton!
// Oznacza to, że nie możemy wywołać np. metody ref.current.focus()
<FancyButton
label="Kliknij mnie"
handleClick={handleClick}
ref={ref}/>;
Na szczęście możemy jawnie przekazać referencję do wewnętrznego komponentu FancyButton
używając API React.forwardRef
. React.forwardRef
przyjmuje funkcję renderującą, która otrzymuje parametry props
oraz ref
, a zwraca element reactowy. Na przykład:
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('poprzednie właściwości:', prevProps);
console.log('nowe właściwości:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 2. Przypiszmy nasz atrybut "forwardedRef" jako referencję
return <Component ref={forwardedRef} {...rest} />; }
}
// 1. Zwróć uwagę na drugi parametr "ref" dostarczony przez React.forwardRef.
// Możemy go przekazać dalej do LogProps jako zwyczajny atrybut, np. "forwardedRef".
// Następnie może on zostać przypisany do komponentu wewnątrz.
return React.forwardRef((props, ref) => { return <LogProps {...props} forwardedRef={ref} />; });}
Wyświetlanie własnej nazwy w narzędziach deweloperskich
React.forwardRef
przyjmuje funkcję renderującą. Narzędzia deweloperskie Reacta (ang. React DevTools) używają tej funkcji do określenia, jak wyświetlać komponent, który przekazuje referencję.
Przykładowo, następujący komponent w narzędziach deweloperskich wyświetli się jako ”ForwardRef“:
const WrappedComponent = React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
Jeśli nazwiesz funkcję renderującą, narzędzia deweloperskie uwzględnią tę nazwę (np. ”ForwardRef(myFunction)”):
const WrappedComponent = React.forwardRef(
function myFunction(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
);
Możesz nawet ustawić właściwość displayName
funkcji tak, aby uwzględniała nazwę opakowanego komponentu:
function logProps(Component) {
class LogProps extends React.Component {
// ...
}
function forwardRef(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
// Nadajmy temu komponentowi nazwę, która będzie bardziej czytelna w narzędziach deweloperskich.
// np. "ForwardRef(logProps(MyComponent))"
const name = Component.displayName || Component.name; forwardRef.displayName = `logProps(${name})`;
return React.forwardRef(forwardRef);
}