Komponenty wyższego rzędu: od otoczaków klas po funkcje i haki
Dowiedz się, jak działają komponenty wyższego rzędu w React, jak ten sam HOC do logowania wygląda jako klasa i jako funkcja, oraz dlaczego custom hooks zastąpiły większość przypadków użycia HOC.
Źródła: React 27: Higher Order Components (HOC) — Deep Dive (Class vs Functional Components)
Zanim pojawiły się własne hooki, deweloperzy React musieli znaleźć sposób na udostępnianie takich funkcji jak logowanie, sprawdzanie autoryzacji czy ładowanie danych pomiędzy różnymi komponentami. Rozwiązaniem były komponenty wyższego rzędu (HOC): zwykłe funkcje, które przyjmują komponent i tworzą wokół niego ulepszoną otoczkę. Poniżej ten sam HOC do logowania jest zaimplementowany zarówno jako klasa, jak i jako funkcja, a następnie porównany z własnym hookiem.
Dlaczego HOC-y pojawiły się w erze klas
Gdy większość kodu React była pisana jako klasy, ponowne wykorzystanie logiki sprawiało trudności. Wyobraźmy sobie, że Profile, Dashboard i Settings muszą wszystkie rejestrować moment montowania i aktualizacji, sprawdzać, czy użytkownik jest zalogowany, ładować dane i obsługiwać błędy. Bez wspólnej abstrakcji każda klasa powtarzała ten sam konstruktor, konfigurację stanu, metody cyklu życia oraz metody pomocnicze. HOC rozwiązywały ten problem, otaczając klasę inną klasą, która zawierała wspólną logikę cyklu życia.
HOC nie jest specjalną API React, lecz jedynie kompozycją funkcji zastosowaną do komponentów.
HOC do logowania napisane przy użyciu klas
Pisanie otulacza
Następujące HOC zwraca nową klasę, która rejestruje moment montowania otoczonego komponentu oraz każdą jego aktualizację, a następnie renderuje oryginalny komponent, przekazując wszystkie dostarczone właściwości:
import React, { Component } from "react";
export function withRenderLogger(WrappedComponent) {
return class extends Component {
componentDidMount() {
console.log(`${WrappedComponent.name} mounted`);
}
componentDidUpdate() {
console.log(`${WrappedComponent.name} updated`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Wrapper zarządza metodami cyklu życia i przekazuje this.props bez zmian, więc oryginalny komponent nie zawiera kodu do logowania.
Jego zastosowanie
Eksportuje się wersję otoczoną wrapperem zamiast surowej klasy:
import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";
class Profile extends Component {
render() {
return <h2>Profile Component</h2>;
}
}
export default withRenderLogger(Profile);
Renderowanie Profile teraz polega na renderowaniu wrappera, który z kolei renderuje rzeczywisty Profile. Zwracana klasa jest anonimowa, więc ustawienie displayName, na przykład withRenderLogger(Profile), ułatwia czytanie w React DevTools.
Koszty HOC opartych na klasach
To działało, ale z pewnymi trudnościami:
- wiązanie zagnieżdżonych definicji klas i
this - logika rozproszona między metodami cyklu życia
- głębsze drzewa komponentów, co utrudniało debugowanie poprzez konieczność przeglądania wrapperów
- kolizje nazw propów, gdy kilka HOC wstrzykuje propy o tej samej nazwie
To samo zastosowanie w komponentach funkcyjnych
Bез klas, this czy metod życiowych cyklu, HOC sprowadza się do funkcji zwracającej komponent funkcyjny:
function HOC(WrappedComponent) {
return function Enhanced(props) {
return <WrappedComponent {...props} />;
};
}
Oto logger przepisany w tym stylu:
export function withLogger(WrappedComponent) {
return function Enhanced(props) {
console.log(`${WrappedComponent.name} rendered`);
return <WrappedComponent {...props} />;
};
}
Użycie jest identyczne jak w wersji klasowej:
import React from "react";
import { withLogger } from "./withLogger";
function Dashboard() {
return <h2>Dashboard Component</h2>;
}
export default withLogger(Dashboard);
Loguje na każdym renderowaniu, ale jako efekt uboczny w treści renderowania. React może renderować częściej, niż faktycznie zapisuje zmiany, a tryb Strict Mode renderuje dwa razy w środowisku rozwojowym, więc należy się spodziewać duplikatów logów; użyj useEffect do czegokolwiek więcej niż tylko szybkiego debugowania.
Niewielka aplikacja demonstracyjna
Przycisk przepina się między Profile opartym na klasach a Dashboard opartym na funkcjach, dzięki czemu oba HOC-y rejestrują działania. Uruchamianie demonstracji poprzez renderowanie zamiast wywoływania błędów zapewnia przewidywalność w trybie Fast Refresh:
import React, { useState } from "react";
import Profile from "./Profile";
import Dashboard from "./Dashboard";
export default function App() {
const [toggle, setToggle] = useState(true);
return (
<div style={{ padding: 20 }}>
<h1>HOC Demo</h1>
<button onClick={() => setToggle(!toggle)}>
Re-render Components
</button>
{toggle ? <Profile /> : <Dashboard />}
</div>
);
}
Profile.js wykorzystuje HOC oparty na klasach:
import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";
class Profile extends Component {
render() {
return <h2>Profile Component</h2>;
}
}
export default withRenderLogger(Profile);
Dashboard.js wykorzystuje HOC oparty na funkcjach:
import React from "react";
import { withLogger } from "./withLogger";
function Dashboard() {
return <h2>Dashboard Component</h2>;
}
export default withLogger(Dashboard);
Moduł HOC oparty na klasach:
import React, { Component } from "react";
export function withRenderLogger(WrappedComponent) {
return class extends Component {
componentDidMount() {
console.log(`${WrappedComponent.name} mounted`);
}
componentDidUpdate() {
console.log(`${WrappedComponent.name} updated`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
A moduł HOC oparty na funkcjach:
export function withLogger(WrappedComponent) {
return function Enhanced(props) {
console.log(`${WrappedComponent.name} rendered`);
return <WrappedComponent {...props} />;
};
}
Te pliki działają w każdym standardowym środowisku React; przy użyciu Vite pliki JSX powinny mieć rozszerzenie .jsx.
Dlaczego custom hooks zastąpiły większość HOC-ów
Custom hooks obejmują obecnie większość dawnych przypadków użycia HOC-ów: ładowanie danych zdalnych, sprawdzania autoryzacji, śledzenie akcji użytkownika, słuchacze DOM i subskrypcje. Narzędzie do logowania staje się hookiem:
function useLogger(name) {
useEffect(() => {
console.log(`${name} rendered`);
});
}
function Dashboard() {
useLogger("Dashboard");
return <h2>Dashboard Component</h2>;
}
Haki nie dodają warstw otaczających, zwracają wartości zamiast injectować właściwości i łączą komponenty bez kolizji. Bez tablicy zależności efekt jest wykonywany po każdym commitie.
HOC-y nadal pasują do warunkowego otaczania (wyświetlania ekranu logowania zamiast komponentu), bibliotek z interfejsami HOC oraz kodu opartego na klasach; React.memo ma taką samą strukturę. Stosuj HOC-y na poziomie modułu: tworzenie otoczaka podczas renderowania powoduje ponowne zamontowanie poddrzewa i utratę jego stanu.
Główne wnioski
- HOC otacza komponent i zwraca jego ulepszoną wersję.
- Komponenty klasowe polegały na HOC-ych, ponieważ dzielenie się logiką cyklu życia w inny sposób byłoby powtarzalne.
- W przypadku komponentów funkcjonalnych HOC to po prostu funkcja zwracająca komponent funkcjonalny.
- Współczesnym standardem do ponownego wykorzystania logiki są niestandardowe haki; zachowaj HOC-y do otaczania i warunkowego renderowania.
displayName, unikaj efektów ubocznych podczas renderowania i nigdy nie twórz HOC-ów w trakcie renderowania.Literatura pokrewna
- Typowanie React Hooks: useState, useEffect, useReducer i custom hooks — Dowiedz się, jak prawidłowo typować useState, useEffect, useReducer oraz custom hooks w TypeScriptie, a także kiedy wybór TypeScripta zamiast zwykłego JavaScripta naprawdę się opłaca.
- Zachowaj Hooks bez głowy: oddzielanie behawioru alertu od jego renderowania — Dowiedz się, dlaczego hook React, który zwraca JSX, ukrywa część drzewa komponentów i łączy behawior z prezentacją, oraz jak przepisać go tak, aby zamiast tego eksponował stan.
- React Props dla początkujących: Przekształcanie staticznych componentów w template'y — Dowiedz się, jak React props przekazują dane z komponentów rodzicielskich do potomnych, jak funkcjonuje obiekt props, jakie typy wartości można przekazać oraz dlaczego jednokierunkowy przepływ danych jest pomocny.
- Od JSX do zwykłych obiektów: Jak funkcjonują elementy, componenty i propсы w React — Zobacz, na co kompiluje się JSX, dlaczego componenty to po prostu funkcje oraz jak z tego jednego modelu mentalnego wynikają propсы, potomne elementy, wartości domyślne, rozszerzanie i sloty.