Startseite / Artikel

Höherer Ordnung: Komponenten von Klassenumhüllungen bis hin zu Funktionen und Hooks

Erfahren Sie, wie höherordentliche React-Komponenten funktionieren, wie derselbe Logger-HOC in Form einer Klasse und als Funktion aussieht, und warum benutzerdefinierte Hooks die meisten Anwendungsfälle für HOC ersetzt haben.

Verlag: REACTAPP.TOP5 Min. Lesezeit1092 Wörter

Quellen: React 27: Higher Order Components (HOC) — Deep Dive (Class vs Functional Components)

Lange bevor es benutzerdefinierte Hooks gab, benötigten React-Entwickler eine Möglichkeit, Verhaltensweisen wie Logging, Authentifizierungsprüfungen oder Datenladung über mehrere Komponenten hinweg zu teilen. Die Lösung waren höherordentliche Komponenten (HOCs): einfache Funktionen, die eine Komponente erhalten und einen erweiterten Wrapper um sie herum erstellen. Im Folgenden wird derselbe Logging-HOC sowohl als Klasse als auch als Funktion implementiert und anschließend mit einem benutzerdefinierten Hook verglichen.

Warum HOCs in der Klasse-Ära entstanden

Als der größte Teil des React-Code als Klassen geschrieben wurde, war die Wiederverwendung von Logik umständlich. Stellen Sie sich vor, Profile, Dashboard und Settings müssten alle protokollieren, wann sie geladen und aktualisiert werden, überprüfen, ob der Benutzer angemeldet ist, Daten laden und Fehler verarbeiten. Ohne eine gemeinsame Abstraktion wiederholte jede Klasse denselben Konstruktor, die Zustandsinitialisierung, die Lebenszyklusmethoden sowie die Hilfsmethoden. HOCs lösten dieses Problem, indem sie eine Klasse in eine weitere Klasse einhüllten, die die gemeinsame Lebenszykluslogik bereitstellt.

Ein HOC ist keine spezielle React-API, sondern lediglich die Anwendung von Funktionskomposition auf Komponenten.

Ein ProtokollierungshOC, der mit Klassen erstellt wird

Die Erstellung des Wrapper

Der folgende HOC gibt eine neue Klasse zurück, die protokolliert, wann die umhüllte Komponente geladen und aktualisiert wird, und anschließend die ursprüngliche Komponente mit allen übergebenen Eigenschaften darstellt:

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

Der Wrapper besitzt die Lebenszyklusmethoden und leitet this.props unverändert weiter, sodass die ursprüngliche Komponente keinen Logging-Code enthält.

Anwendung

Man exportiert die umgewickelte Version anstelle der rohen Klasse:

import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";

class Profile extends Component {
  render() {
    return <h2>Profile Component</h2>;
  }
}
export default withRenderLogger(Profile);

Durch das Rendern von Profile wird nun der Wrapper gerendert, der wiederum den eigentlichen Profile darstellt. Die zurückgegebene Klasse ist anonym, weshalb das Setzen eines displayName, wie zum Beispiel withRenderLogger(Profile), die Lesbarkeit in React DevTools verbessert.

Die Kosten von HOCs auf Klassenbasis

Es funktionierte, aber mit Einschränkungen:

  • verschachtelte Klassendefinitionen und this-Binding
  • Logik, die über verschiedene Lebenszyklusmethoden verteilt ist
  • tiefere Komponententriebe, wodurch das Debuggen bedeutete, sich durch Wrapper durchzukämpfen
  • Kollisionsfälle bei Prop-Names, wenn mehrere HOCs Props mit demselben Namen injizieren
  • implizite Verknüpfung zwischen dem HOC und den Props der umschlossenen Komponente
  • Dieselbe Idee mit Funktionskomponenten

    Ohne Klassen, this oder Lebenszyklusmethoden reduziert sich ein HOC auf eine Funktion, die eine Funktionskomponente zurückgibt:

    function HOC(WrappedComponent) {
      return function Enhanced(props) {
        return <WrappedComponent {...props} />;
      };
    }
    

    Hier ist der Logger in diesem Stil umgeschrieben:

    export function withLogger(WrappedComponent) {
      return function Enhanced(props) {
        console.log(`${WrappedComponent.name} rendered`);
       return <WrappedComponent {...props} />;
      };
    }
    

    Die Verwendung ist identisch mit der Klassenversion:

    import React from "react";
    import { withLogger } from "./withLogger";
    
    function Dashboard() {
      return <h2>Dashboard Component</h2>;
    }
    export default withLogger(Dashboard);
    

    Er protokolliert bei jeder Renderung, aber als Nebeneffekt im Render-Body. React kann häufiger rendern, als es Daten speichert, und im Strict Mode wird zweimal gerendert – daher sind doppelte Protokolle zu erwarten; verwenden Sie useEffect für alles, was über schnelles Debuggen hinausgeht.

    Eine kleine Demo-App

    Eine Schaltfläche wechselt zwischen dem klassenbasierten Profile und dem funktionsbasierten Dashboard, sodass beide HOCs protokollieren. Durch das Darstellen statt des Auslösen von Fehlern bleibt die Demo unter Fast Refresh vorhersagbar:

    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 verwendet den klassenbasierten HOC:

    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 verwendet den funktionsbasierten HOC:

    import React from "react";
    import { withLogger } from "./withLogger";
    
    function Dashboard() {
      return <h2>Dashboard Component</h2>;
    }
    export default withLogger(Dashboard);
    

    Das Modul für den klassenbasierten HOC:

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

    Und das Modul für den funktionsbasierten HOC:

    export function withLogger(WrappedComponent) {
      return function Enhanced(props) {
        console.log(`${WrappedComponent.name} rendered`);
        return <WrappedComponent {...props} />;
      };
    }
    

    Diese Dateien laufen in jeder Standard-React-Umgebung; mit Vite sollten JSX-Dateien .jsx heißen.

    Warum eigene Hooks die meisten HOCs ersetzt haben

    Eigene Hooks decken heute die meisten früheren Anwendungsfälle von HOCs ab: Laden von Remote-Daten, Authentifizierungsprüfungen, Tracking, DOM-Listener und Abonnements. Der Logger wird zu einem Hook:

    function useLogger(name) {
      useEffect(() => {
        console.log(`${name} rendered`);
      });
    }
    
    function Dashboard() {
      useLogger("Dashboard");
      return <h2>Dashboard Component</h2>;
    }
    

    Hooks fügen keine Wrapper-Schichten hinzu, geben Rückgabewerte statt Eigenschaften ein und ermöglichen die Kombination ohne Kollisionen. Da es keinen Abhängigkeitsarray gibt, wird der Effekt nach jedem Commit ausgeführt.

    HOCs eignen sich weiterhin für bedingte Verpackung (Anzeige eines Anmeldebildschirms anstelle der Komponente), Bibliotheken mit HOC-APIs sowie klassenbasierten Code; React.memo hat die gleiche Struktur. Wenden Sie HOCs auf Modulebene an: Das Erstellen eines Wrappers während des Renderings führt zu einem Neuaufbau des Unterbaums und zum Verlust seines Zustands.

    Wichtige Erkenntnisse

    • Ein HOC umhüllt eine Komponente und gibt eine verbesserte Version davon zurück.
    • Klassenkomponenten setzten auf HOCs, weil die Wiederverwendung von Lebenszykluslogik sonst wiederholt gewesen wäre.
    • Bei Funktionskomponenten ist ein HOC einfach eine Funktion, die eine Funktionskomponente zurückgibt.
    • Custom Hooks sind heute die Standardwahl zur Wiederverwendung von Logik; halten Sie HOCs für das Umhüllen und die bedingte Anzeige bei.
  • Legen Sie einen displayName fest, halten Sie Nebeneffekte außerhalb der Renderung und erstellen Sie niemals HOCs während der Renderung.