Wenn man Hunderte von „Senior- React-Entwicklerninterviewt, ist man überrascht, wie viele diese einfache Frage nicht beantworten können:
„Was ist eine Higher-Order Component?"
Und noch weniger können die folgende Frage beantworten: „Warum gibt es Higher-Order Components in React?"
Mit anderen Worten: „Hat ein React-Teammitglied Higher-Order Components bewusst als Konzept erstellt und in React integriert?"
In diesem Artikel erfahren Sie die richtigen Antworten auf diese Fragen und lernen alles, was Sie über HOCs wissen müssen.
Hinweis: Stellen Sie sicher, dass Sie Pfeilfunktionen und die Grundlagen von Reactverstehen.
Zuerst sehen Sie die formale Definition von HOCs und im weiteren Verlauf dieses Artikels werden Sie die dahinterstehende Theorie verstehen. Eine Higher-Order Component ist eine Funktion, die eine Komponente entgegennimmt und eine neue Komponente zurückgibt.
Die React-Doku führen weiter aus:
„Eine Higher-Order Component (HOC) ist eine fortgeschrittene Technik in React zur Wiederverwendung von Komponentenlogik. HOCs sind nicht direkt Teil der React-API. Sie sind ein Muster, das aus der kompositorischen Natur von React entsteht.“
Die Theorie hinter HOCs stammt aus ...
In der Mathematik, Funktionskomposition bezeichnet das Kombinieren von Funktionen, um eine neue Funktion oder ein Ergebnis zu bilden, indem eine Funktion auf das Ergebnis einer anderen angewendet wird.
In JavaScript sieht das so aus:
const inc = n => n + 1; // f
const double = n => n * 2; // g
// h(x) = (f ∘ g)(x) = f(g(x))
const doubleThenInc = x => inc(double(x)); // hBeachten Sie, wie Sie die kombinierten Funktionen einer neuen Variablen namens doubleThenInczuweisen, was möglich ist, da JavaScript First-Class- Funktionen hat.
Mehr über First-Class-Funktionen erfahren Sie in unserem Artikel über den Unterschied zwischen useCallback und useMemo.
Eine Programmiersprache verfügt über Funktionen erster Klasse, wenn sie es erlaubt, Funktionen Variablen zuzuweisen.
Sie können die Komposition abstrahieren, um beliebige zwei Funktionen zu kombinieren:
const compose2 = (f, g) => x => f(g(x));
const doubleThenInc2 = compose2(inc, double);Sie lassen das Argument x in der Definition von doubleThenInc2 weg. Das bedeutet, doubleThenInc2 ist definiert punktfrei, was bedeutet, dass Sie eine Funktion definieren, ohne ihre Argumente zu erwähnen.
const doubleThenInc = x => inc(double(x)); // mentions X 👉 pointed
const doubleThenInc2 = compose2(inc, double); // point-freeWenn Sie eine beliebige Anzahl von Funktionen zusammensetzen möchten, müssen Sie die Kompositionsfunktion verallgemeinern.
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const square = n => n * n;
const doubleThenInc3 = compose(inc, double);
const doulbeThenIncThenSquare = compose(square, inc, double);Ausgefeiltere Versionen der compose -Funktion werden häufig von Bibliotheken bereitgestellt, die HOCs nutzen, wie z.B. Redux und Apollo.
Die Argumente und Rückgabewerte von Funktionen müssen übereinstimmen, um sie zu komponieren. Zum Beispiel können Sie eine Funktion, die ein Objekt akzeptiert und einen String zurückgibt, nicht mit einer Funktion komponieren, die ein Array empfängt und eine Zahl zurückgibt.
// (number, number) => number[]
const echo = (value, times) => Array(times).fill(value);
// number[] => number[]
const doubleMap = array => array.map(x => x * 2);
// Correct composition. ✅
const echoAndDoubleMap = compose(doubleMap, echo);
console.log(echoAndDoubleMap(3, 4)); // [6, 6, 6, 6]
// Incorrect composition that will throw an error. ❌
const wrongOrder = compose(echo, doubleMap);
try {
// This will fail because doubleMap expects an array,
// instead of two numbers.
console.log(wrongOrder(3, 4));
} catch (error) {
console.error("Error:", error.message); // Error: array.map is not a function
}Da inc und double beide Zahlen entgegennehmen und zurückgeben, können Sie sie in beliebiger Reihenfolge zusammensetzen.
// Composition that doubles then increments. ✅
const doubleThenInc = compose(inc, double);
console.log(doubleThenInc(3)); // 7
// Composition that increments then doubles. ✅
const incThenDouble = compose(double, inc);
console.log(incThenDouble(3)); 8Außerdem sind compose2 und compose Funktionen höherer Ordnung.
Eine Funktion höherer Ordnung ist eine Funktion, die entweder eine Funktion empfängt oder zurückgibt oder beides tut.
const multiply = multiplier => multiplicant => multiplier * multiplicant;
const double = multiply(2);
const map = f => arr => arr.map(f);
const doubleMap = map(double);
const numbers = [1, 2, 3];
doubleMap(numbers); // [2, 4, 6]multiply IST eine Funktion höherer Ordnung, weil sie eine Zahl entgegennimmt und eine Funktion zurückgibt.double IST KEINE Funktion höherer Ordnung, weil sie weder eine Funktion empfängt noch zurückgibt. Sie ist punktfrei definiert.map IST eine Funktion höherer Ordnung, weil sie sowohl eine Funktion akzeptiert als auch zurückgibt.doubleMap IST KEINE Funktion höherer Ordnung, weil sie weder eine Funktion empfängt noch zurückgibt. Sie ist punktfrei definiert.React-Komponenten können entweder Funktionen oder Klassen sein.
import { Component } from 'react'
function MyFunctionComponent() {
return <div>Function</div>
}
class MyClassComponent extends Component {
render() {
return <div>Class</div>
}
}In JavaScript,das Schlüsselwort <code>class</code> ist im Wesentlichen ein Wrapper für das <code>function</code> Schlüsselwort und handhabt die prototypische Vererbung. Mit anderen Worten, Klassen kompilieren zu Konstruktorfunktionen.
Daher, da alle Komponenten in React Funktionen sind und JavaScript Funktionen höherer Ordnung besitzt, erhält man HOCs quasi umsonst. Das ist es, was die Dokumentation meint, wenn sie sagt, dass HOCs „ein Muster sind, das aus der kompositorischen Natur von React entsteht.“
Nun sollten Sie die grundlegende Definition von HOCs verstehen:
Eine Higher-Order-Komponente ist eine Funktion, die eine Komponente entgegennimmt und eine neue Komponente zurückgibt.
Jede Funktion, deren Eingabe und Ausgabe eine React-Komponente ist, ist ein HOC.
Sie möchten wahrscheinlich sehen, wie eine Higher-Order-Komponente aussieht. Folgen Sie dem weiteren Verlauf dieses Tutorials, um Ihre eigene mit TDD zu schreiben. Sie werden Vitest mit React Testing Library verwenden, um die Tests zu schreiben.
Aus der Definition einer Higher-Order-Komponente können Sie zwei Anforderungen ableiten:
Sie können diese Anforderungen in einem Unit-Test erfassen.
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import myHOC from './my-hoc';
function MyComponent({ title = 'Hello' }) {
return <p>{title}</p>;
}
describe('myHOC', () => {
test('given a component: returns the component with a default title', () => {
const WrappedComponent = myHOC(MyComponent);
render(<WrappedComponent />);
expect(screen.getByText('Hello')).toHaveTextContent('Hello');
});
});
Der Test prüft beide Anforderungen, denn wenn dieser Test erfolgreich ist, können Sie logisch ableiten, dass Ihr HOC eine Funktion ist und eine Komponente zurückgibt, ohne diese Anforderungen explizit aufzuführen. Wenn das HOC nicht eine Funktion wäre und Sie versuchen würden, es aufzurufen, würde es einen Fehler werfen, und Ihr Unit-Test würde mit einem klaren Stack-Trace fehlschlagen. Ebenso rendert der Test den Rückgabewert des HOC, was sicherstellt, dass es eine React-Komponente ist.
Beachten Sie, dass Sie NICHT auf typeof function hier getestet haben. Unit-Tests, die nur Typen prüfen, sind ein Anti-Pattern. Dies ist redundant, wenn man einfach die Funktion aufruft und ihren Ausgabewert prüft. Im Allgemeinen sind Typüberprüfungen bei gut geschriebenen Unit-Tests redundant. Deshalb können Unit-Tests die meisten Typfehler abfangen, ohne die Notwendigkeit zusätzlicher Maßnahmen wie Typannotationen (obwohl Annotationen und Typinferenz immer noch nützlich sein können, um IDE-Tools zu ermöglichen).
Sie können den Test bestehen lassen, indem Sie Ihr HOC zur Identitätsfunktionmachen.
export default Component => Component;Ihr Testergebnis sollte nun so aussehen.
✓ app/my-hoc.test.jsx (1)
✓ myHOC (1)
✓ given a component: returns the component with a default title
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 16:07:11
Duration 128ms
PASS Waiting for file changes...
press h to show help, press q to quit
Ihr aktuelles HOC tut nichts. Das werden Sie aber bald ändern.
Generell, HOCs eignen sich hervorragend zur Abstraktion von Logik oder Styling. Sie ermöglichen es Ihnen, unnötige Code-Duplizierung zu vermeiden. Wenn Sie feststellen, dass sich bestimmte JSX- oder Logikmuster in Ihrer Komponente wiederholen, können Sie diese möglicherweise mithilfe von HOCs abstrahieren.
Wenn Sie beispielsweise eine Seite für Ihre Website oder einen Bildschirm für Ihre React Native-App haben, weisen die meisten Seiten oder Bildschirme dasselbe Layout auf. Sie alle teilen sich Elemente wie Kopfzeilen, Fußzeilen oder Formatierungscontainer.
Sie können unserem HOC Styling-Fähigkeiten hinzufügen und es withLayout anstatt MyHOC.
Beginnen Sie damit, einen Test hinzuzufügen, der überprüft, ob Ihr HOC Ihrer Komponente ein Layout hinzufügt.
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import withLayout from './with-layout';
function MyComponent({ title = 'Hello' }) {
return <p>{title}</p>;
}
describe('withLayout', () => {
test('given a component: returns the component with a default title', () => {
const WrappedComponent = withLayout(MyComponent);
render(<WrappedComponent />);
expect(screen.getByText('Hello')).toHaveTextContent('Hello');
});
test('given a component: renders the layout around the component', () => {
const WrappedComponent = withLayout(MyComponent);
render(<WrappedComponent />);
expect(screen.getByRole('heading')).toHaveTextContent(/some title/i);
expect(screen.getByRole('main')).toContainElement(screen.getByText('Hello'));
expect(screen.getByRole('contentinfo')).toHaveTextContent(/some footer/i);
});
});Sehen Sie zu, wie Ihr Test fehlschlägt, und erstellen Sie dann eine Layout-Komponente.
export function Layout({ children }) {
return (
<div>
<header>
<h1>Some Title</h1>
</header>
<main>
{children}
</main>
<footer>
<p>Some footer</p>
</footer>
</div>
);
}Layouts können je nach verwendeter App und Framework variieren. In einer React Native-App werden Sie feststellen, dass Sie ähnliche Layout-HOCs schreiben, die React Navigation's <SafeAreaView />verwenden. In einer Remix-App benötigen Sie kein Layout-HOC, da Sie eine Layout-Komponente aus Ihrer root.tsx Datei.
Lassen Sie Ihren Test nun bestehen, indem Sie das Layout Komponente in Ihrem HOC verwenden.
import { Layout } from './layout';
export default Component => () => (
<Layout>
<Component />
</Layout>
);Ihre Tests sollten nun beide bestehen.
✓ app/with-layout.test.jsx (2)
✓ withLayout (2)
✓ given a component: returns the component with a default title
✓ given a component: renders the layout around the component
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 16:05:39
Duration 128ms
PASS Waiting for file changes...
press h to show help, press q to quitBeachten Sie, wie die withLayout HOC jetzt eine Komponente entgegennimmt und dann eine Funktion zurückgibt, weil es vor dieser Änderung tatsächlich KEINE Higher-Order-Komponente war.
Dies zeigt auch das häufigste Missverständnis über HOCs. Viele Entwickler beantworten die Frage „Was ist eine Higher-Order-Komponente?“ mit „Es ist eine Komponente die eine React-Komponente entgegennimmt und zurückgibt“."
Sie denken wahrscheinlich an so etwas.
// Wrong! ❌
function NotAHigherOrderComponent({ Component }) {
return (
<div>
<h1>Header added by NotAHigherOrderComponent</h1>
<Component />
</div>
);
}
function MyComponent() {
return <p>Hello, I am a regular component.</p>;
}
function App() {
return (
<div>
<NotAHigherOrderComponent Component={MyComponent} />
</div>
);
}Was Sie oben sehen, ist eine React-Komponente, die eine andere React-Komponente als Prop entgegennimmt.
Aber das ist KEINE Higher-Order-Komponente, denn HOCs sind Funktionen und KEINE Komponenten. Sie können KEINE HOC rendern.
Wenn wir uns Ihr withLayout HOC ansehen, enthält es einen Fehler. Können Sie ihn entdecken?
Falls nicht, ist das in Ordnung. Sie können den folgenden Test schreiben, um den Fehler aufzudecken.
describe('withLayout', () => {
// ... your other tests
test('given props for the wrapped component: passes on the props to the wrapped component', () => {
const WrappedComponent = withLayout(MyComponent);
const customTitle = 'Custom Title';
render(<WrappedComponent title={customTitle} />);
expect(screen.getByText(customTitle)).toHaveTextContent(customTitle);
});
});Der neue Test schlägt fehl.
❯ app/with-layout.test.jsx (3)
❯ withLayout (3)
✓ given a component: returns the component with a default title
✓ given a component: renders the layout around the component
× given props for the wrapped component: passes on the props to the wrapped componentDer Test deckt das Problem auf: Sie übergeben keine Props an die umhüllte Komponente. Sie können den Test bestehen lassen, indem Sie die Props, die das HOC empfängt, weitergeben.
import { Layout } from './layout';
export default Component => props => (
<Layout>
<Component {...props} />
</Layout>
);Ihre Tests bestehen jetzt, weil Ihr HOC die Props korrekt an die umhüllte Komponente weiterleitet.
✓ app/with-layout.test.jsx (3)
✓ withLayout (3)
✓ given a component: returns the component with a default title
✓ given a component: renders the layout around the component
✓ given props for the wrapped component: passes on the props to the wrapped component
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 16:55:45
Duration 139ms
PASS Waiting for file changes...
press h to show help, press q to quitAllerdings wären die Abstraktionsfähigkeiten von HOCs nicht so nützlich, wenn sie nicht noch eine weitere Schlüsselfunktion hätten. Eric Elliott beschreibt es so:
„Der Hauptvorteil von HOCs ist nicht, was sie ermöglichen (es gibt andere Wege, dies zu tun); es ist vielmehr, wie sie sich auf der obersten Seitenebene zusammensetzen lassen.“
Mit anderen Worten, der Schlüssel zum guten Einsatz von HOCs ist zu wissen, wie und wann man sie zusammensetzen möchte. Sie schreiben einen Test, um das „Wie“ zu demonstrieren. Spoiler: es ist im Grunde Funktionskomposition.
Hier ist ein Test, der zeigt, wie man HOCs zusammensetzt.
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import withLayout from './with-layout';
function MyComponent({ title }) {
return <p>{title}</p>;
}
describe('withLayout', () => {
// ... your other tests
test('given used in composition with other HOCs: passes on the props of the other HOCs', () => {
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const withTitle = Component => props => (
<Component title="foo" {...props} />
);
const ComposedComponent = compose(
withLayout,
withTitle
)(MyComponent);
render(<ComposedComponent />);
expect(screen.getByText('foo')).toHaveTextContent('foo');
});
});Dieser Test ist bereits erfolgreich.
Sie setzen withLayout mit withTitlezusammen. withTitle ist ein HOC, das einen Titel Prop an eine Komponente.
HOCs akzeptieren häufig Konfigurationsobjekte. Das kennen Sie wahrscheinlich von der Verwendung von React Redux' connect mit mapStateToProps. (Tatsächlich akzeptiert es zwei weitere Argumente: mapDispatchToProps und mergeProps.)
Nehmen wir an, einige Seiten sollen ohne Header gerendert werden. Sie ändern also Ihre Layout-Komponente so, dass sie eine Prop entgegennimmt, mit der Sie den Header ein- und ausblenden können.
export function Layout({ children, showHeader = true }) {
return (
<div>
{showHeader && (
<header>
<h1>Some Title</h1>
</header>
)}
<main>{children}</main>
<footer>
<p>Some footer</p>
</footer>
</div>
);
}Schreiben Sie nun einen Test, der es Ihnen ermöglicht, Ihr HOC zu modifizieren. Sie müssen auch Ihre bestehenden Tests anpassen, um der Tatsache Rechnung zu tragen, dass Ihr HOC nun ein Konfigurationsobjekt entgegennimmt.
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import withLayout from './with-layout';
function MyComponent({ title = 'Hello' }) {
return <p>{title}</p>;
}
describe('withLayout', () => {
test('given a component: returns the component with a default title', () => {
const WrappedComponent = withLayout()(MyComponent);
render(<WrappedComponent />);
expect(screen.getByText('Hello')).toHaveTextContent('Hello');
});
test('given a component: renders the layout around the component', () => {
const WrappedComponent = withLayout()(MyComponent);
render(<WrappedComponent />);
expect(screen.getByRole('heading')).toHaveTextContent(/some title/i);
expect(screen.getByRole('main')).toContainElement(
screen.getByText('Hello'),
);
expect(screen.getByRole('contentinfo')).toHaveTextContent(/some footer/i);
});
test('given props for the wrapped component: passes on the props to the wrapped component', () => {
const WrappedComponent = withLayout()(MyComponent);
const customTitle = 'Custom Title';
render(<WrappedComponent title={customTitle} />);
expect(screen.getByText(customTitle)).toHaveTextContent(customTitle);
});
test('given used in composition with other HOCs: passes on the props of the other HOCs', () => {
const compose =
(...fns) =>
x =>
fns.reduceRight((y, f) => f(y), x);
const withTitle = Component => props => (
<Component title="foo" {...props} />
);
const ComposedComponent = compose(withLayout(), withTitle)(MyComponent);
render(<ComposedComponent />);
expect(screen.getByText('foo')).toHaveTextContent('foo');
});
test('given a component and NOT rendering the header: does NOT render the header', () => {
const WrappedComponent = withLayout({ showHeader: false })(MyComponent);
render(<WrappedComponent />);
expect(screen.queryByRole('heading')).toBeNull();
});
});
Beobachten Sie, wie alle Ihre Tests fehlschlagen, weil Ihrer Komponente noch das Konfigurationsobjekt fehlt. Fügen Sie es hinzu, damit sie bestehen.
import { Layout } from './layout';
export default ({ showHeader = true } = {}) =>
Component =>
props => (
<Layout showHeader={showHeader}>
<Component {...props} />
</Layout>
);Um die Frage zu beantworten, wann Komposition für HOCs verwendet werden sollte, erinnern Sie sich an das, was Sie zuvor gelernt haben. HOCs eignen sich hervorragend, wenn Sie gemeinsame Logik zwischen vielen Komponenten abstrahieren möchten. Sie haben sich entschieden, Ihrer Funktion eine Layout-Funktionalität zu geben, weil dies ein Bereich ist, den die meisten Bildschirme Ihrer Anwendung gemeinsam haben werden. Mithilfe von compose können Sie ein HOC definieren, mit dem Sie alle Ihre Seiten umhüllen können.
Hier ist ein Praxisbeispiel für ein SignInForm Container-Komponente. Prüfen Sie, ob Sie es verstehen, und lesen Sie dann die Erklärung, um zu überprüfen, ob Sie richtig lagen.
import { withFormik } from 'formik';
import compose from 'ramda/src/compose.js';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import SignInComponent from './sign-in-form-component.js';
import { isAuthenticating, signIn } from './user-authentication-reducer.js';
import { signInValidationSchema } from './validation-schema.js';
const initialFormValues = { email: '', password: '' };
const mapStateToProps = state => ({ loading: isAuthenticating(state) });
const formikConfig = {
handleSubmit: ({ email, password }, { props: { signIn } }) => {
signIn({ email, password });
},
mapPropsToValues: () => initialFormValues,
validationSchema: signInValidationSchema,
};
export default compose(
withRouter,
connect(
mapStateToProps,
{ signIn }
),
withFormik(formikConfig),
)(SignInComponent);
Im obigen Beispiel haben Sie 3 verschiedene HOCs kombiniert.
withRouter ist ein HOC von React Router DOM. Es injiziert das history Objekt, das Sie verwenden können, um zum Bildschirm zum Zurücksetzen des Passworts zu navigieren, wenn der Benutzer auf den „Passwort vergessen“-Button klickt.connect ist ein HOC von React Redux. Sie verwenden es, um Ihre Komponente mit Ihrem Redux-Store zu verbinden. Sie injizieren die loading Prop und den signIn Action Creator.withFormik ist ein HOC von Formik. Formik ermöglicht es Ihnen, den lokalen Formularstatus zu steuern und übernimmt die Formularvalidierung für Sie.Manchmal müssen Sie übernehmen statische Eigenschaften wie propTypes, defaultProps und getStaticProps (wenn Sie Next.js verwenden) von der inneren Komponente zur resultierenden Komponente. Hier ist ein Higher-Order HOC (eine Funktion, die ein HOC zurückgibt), das dies für Sie erledigt.
import hoistNonReactStatics from 'hoist-non-react-statics';
const hoistStatics = higherOrderComponent => Component => {
const WrappedComponent = higherOrderComponent(Component);
hoistNonReactStatics(WrappedComponent, Component);
return WrappedComponent;
};Übrigens: Bei der Verwendung von HOCs müssen Sie auch Refs besonders behandeln. Wenn Sie refs durch eine Komponenten-Hierarchie weitergeben müssen, sollten Sie wahrscheinlich einen Hook für den ref anstelle eines HOCs verwenden.
Sie wissen aus der Funktionskomposition, dass Sie nur Funktionen zusammensetzen können, deren Typen übereinstimmen. Ähnlich müssen Sie auf die Reihenfolge achten, in der Sie Ihre HOCs zusammensetzen. Ein HOC kann Props injizieren, von denen ein anderes abhängen könnte. Wenn das HOC, das von den Props abhängt, vor dem Props injizierenden HOC injiziert wird, könnte Ihre Anwendung abstürzen.
const formatTitleProp = ({ title, ...otherProps }) => ({
title: title.toUpperCase(),
...otherProps,
});
const withTitle = Component => props => <Component title="Hello" {...props} />;
const withFormattedTitle = Component => props => (
<Component {...formatTitleProp(props)} />
);
const breakingApp = compose(withFormattedTitle, withTitle)(App); // 🔴 Breaks!
const workingApp = compose(withTitle, withFormattedTitle)(App); // ✅ Correct order!
Wenn Sie die Reihenfolge der HOCs im obigen Praxisbeispiel ändern, wird es ebenfalls abstürzen. withFormik(formikConfig) hängt von signIn ab, dass es definiert ist, und transformProps hängt sowohl von history als auch von der formikBag props.
HOCs mit impliziten Abhängigkeiten voneinander können ein Code-Smell sein. In einigen Fällen ist es möglicherweise besser, diese Abhängigkeiten explizit zu machen, indem die gemeinsame Funktionalität in die Komponenten importiert wird, die sie benötigen, oder indem die Abhängigkeit als Konfigurationsparameter des HOCs übergeben wird. Es ist wahrscheinlich in Ordnung, implizit von etwas abzuhängen, das für alle Ihre Seiten ziemlich universell ist, wie z.B. Ihr Store-Provider.
Jetzt können Sie selbstbewusst:
Für einen tieferen Einblick in React, besuchen Sie meinen YouTube-Kanal!