Wiederverwendung von Logik mit eigenen Hooks
React bietet mehrere eingebaute Hooks wieuseState,useContextunduseEffect. Manchmal wünscht man sich jedoch einen Hook für einen spezifischeren Zweck: zum Beispiel zum Abrufen von Daten, um zu verfolgen, ob der Benutzer online ist, oder um sich mit einem Chatraum zu verbinden. Diese Hooks findet man vielleicht nicht in React, aber man kann eigene Hooks für die Bedürfnisse der Anwendung erstellen.
Sie werden lernen
- Was eigene Hooks sind und wie man sie schreibt
- Wie man Logik zwischen Komponenten wiederverwendet
- Wie man eigene Hooks benennt und strukturiert
- Wann und warum man eigene Hooks extrahiert
Eigene Hooks: Logik zwischen Komponenten teilen
Stellen Sie sich vor, Sie entwickeln eine App, die stark auf das Netzwerk angewiesen ist (wie die meisten Apps). Sie möchten den Benutzer warnen, wenn seine Netzwerkverbindung versehentlich unterbrochen wurde, während er Ihre App verwendet. Wie würden Sie vorgehen? Es scheint, dass Sie zwei Dinge in Ihrer Komponente benötigen:
- Ein Stück State, das verfolgt, ob das Netzwerk online ist.
- Ein Effekt, der die globalenonline- undoffline-Ereignisse abonniert und diesen State aktualisiert.
Dadurch bleibt Ihre Komponente mit dem Netzwerkstatussynchronisiert. Sie könnten mit so etwas beginnen:
Versuchen Sie, Ihr Netzwerk ein- und auszuschalten, und beobachten Sie, wie dieseStatusBarauf Ihre Aktionen reagiert.
Stellen Sie sich nun vor, Sie möchten dieselbe Logikauchin einer anderen Komponente verwenden. Sie möchten eine Speichern-Schaltfläche implementieren, die deaktiviert wird und „Wird wieder verbunden…“ anstelle von „Speichern“ anzeigt, während das Netzwerk offline ist.
Zunächst können Sie denisOnline-State und den Effekt inSaveButtonkopieren und einfügen:
Überprüfen Sie, dass sich die Schaltfläche ändert, wenn Sie das Netzwerk ausschalten.
Diese beiden Komponenten funktionieren gut, aber die Duplizierung der Logik zwischen ihnen ist bedauerlich. Es scheint, dass Sie die Logik zwischen ihnen wiederverwenden möchten, obwohl sie ein unterschiedlichesErscheinungsbild haben.
Einen eigenen Hook aus einer Komponente extrahieren
Stellen Sie sich für einen Moment vor, dass es ähnlich wieuseStateunduseEffecteinen eingebautenuseOnlineStatus-Hook gäbe. Dann könnten beide Komponenten vereinfacht und die Duplizierung zwischen ihnen entfernt werden:
Obwohl es keinen solchen eingebauten Hook gibt, können Sie ihn selbst schreiben. Deklarieren Sie eine Funktion namensuseOnlineStatusund verschieben Sie den gesamten duplizierten Code aus den zuvor geschriebenen Komponenten hinein:
Am Ende der Funktion geben SieisOnlinezurück. Dadurch können Ihre Komponenten diesen Wert lesen:
Überprüfen Sie, ob das Ein- und Ausschalten des Netzwerks beide Komponenten aktualisiert.
Jetzt enthalten Ihre Komponenten nicht mehr so viel wiederholte Logik.Noch wichtiger ist, dass der Code in ihnen beschreibt,was sie tun sollen(den Online-Status verwenden!), und nichtwie sie es tun sollen(durch Abonnieren von Browser-Ereignissen).
Wenn Sie Logik in benutzerdefinierte Hooks auslagern, können Sie die komplizierten Details verbergen, wie Sie mit einem externen System oder einer Browser-API umgehen. Der Code Ihrer Komponenten drückt Ihre Absicht aus, nicht die Implementierung.
Hook-Namen beginnen immer mituse
React-Anwendungen werden aus Komponenten aufgebaut. Komponenten werden aus Hooks aufgebaut, ob eingebaut oder benutzerdefiniert. Sie werden wahrscheinlich oft benutzerdefinierte Hooks verwenden, die andere erstellt haben, aber gelegentlich schreiben Sie vielleicht selbst einen!
Sie müssen diese Namenskonventionen einhalten:
- React-Komponentennamen müssen mit einem Großbuchstaben beginnen,wie
StatusBarundSaveButton. React-Komponenten müssen auch etwas zurückgeben, das React anzeigen kann, wie ein Stück JSX. - Hook-Namen müssen mit
usegefolgt von einem Großbuchstaben beginnen,wieuseState(eingebaut) oderuseOnlineStatus(benutzerdefiniert, wie weiter oben auf der Seite). Hooks können beliebige Werte zurückgeben.
Diese Konvention stellt sicher, dass Sie immer auf eine Komponente schauen und wissen, wo sich ihr State, ihre Effects und andere React-Funktionen möglicherweise "verstecken". Wenn Sie beispielsweise einen Funktionsaufruf wiegetColor()in Ihrer Komponente sehen, können Sie sicher sein, dass er keinen React-State enthalten kann, da sein Name nicht mitusebeginnt. Ein Funktionsaufruf wieuseOnlineStatus()wird jedoch höchstwahrscheinlich Aufrufe anderer Hooks enthalten!
Hinweis
Wenn Ihr Linterfür React konfiguriert ist,erzwingt er diese Namenskonvention. Scrollen Sie nach oben zum Sandkasten und benennen SieuseOnlineStatusingetOnlineStatusum. Beachten Sie, dass der Linter Ihnen nicht mehr erlaubt,useStateoderuseEffectdarin aufzurufen. Nur Hooks und Komponenten können andere Hooks aufrufen!
Benutzerdefinierte Hooks ermöglichen das Teilen von zustandsbehafteter Logik, nicht des Zustands selbst
Im vorherigen Beispiel haben sich beide Komponenten gemeinsam aktualisiert, als du das Netzwerk ein- und ausschaltest. Es wäre jedoch falsch zu denken, dass eine einzelneisOnline-Zustandsvariable zwischen ihnen geteilt wird. Sieh dir diesen Code an:
Es funktioniert genauso wie vor der Extraktion der Duplizierung:
Dies sind zwei völlig unabhängige Zustandsvariablen und Effects! Sie hatten zufällig zur gleichen Zeit den gleichen Wert, weil du sie mit dem gleichen externen Wert synchronisiert hast (ob das Netzwerk eingeschaltet ist).
Um dies besser zu veranschaulichen, benötigen wir ein anderes Beispiel. Betrachte dieseForm-Komponente:
Es gibt einige sich wiederholende Logik für jedes Formularfeld:
- Es gibt einen Zustand (
firstNameundlastName). - Es gibt einen Change-Handler (
handleFirstNameChangeundhandleLastNameChange). - Es gibt ein Stück JSX, das die
value- undonChange-Attribute für dieses Eingabefeld angibt.
Du kannst die sich wiederholende Logik in diesen benutzerdefinierten HookuseFormInputextrahieren:
Beachte, dass es nureineZustandsvariable namensvaluedeklariert.
Die Form-Komponente ruftuseFormInputjedoch zweimal auf:
Deshalb funktioniert es wie die Deklaration zweier separater State-Variablen!
Custom Hooks ermöglichen es Ihnen,zustandsbehaftete Logikzu teilen, aber nicht denZustand selbst.Jeder Aufruf eines Hooks ist völlig unabhängig von jedem anderen Aufruf desselben Hooks.Deshalb sind die beiden obigen Sandboxen völlig äquivalent. Wenn Sie möchten, scrollen Sie zurück und vergleichen Sie sie. Das Verhalten vor und nach dem Extrahieren eines Custom Hooks ist identisch.
Wenn Sie den Zustand selbst zwischen mehreren Komponenten teilen müssen,heben Sie ihn an und geben Sie ihn weiter.
Reaktive Werte zwischen Hooks übergeben
Der Code innerhalb Ihrer Custom Hooks wird bei jedem erneuten Rendern Ihrer Komponente erneut ausgeführt. Deshalb müssen Custom Hooks, genau wie Komponenten,rein sein.Betrachten Sie den Code von Custom Hooks als Teil des Rumpfes Ihrer Komponente!
Da Custom Hooks zusammen mit Ihrer Komponente neu gerendert werden, erhalten sie immer die neuesten Props und den neuesten State. Um zu sehen, was das bedeutet, betrachten Sie dieses Chatraum-Beispiel. Ändern Sie die Server-URL oder den Chatraum:
Wenn SieserverUrloderroomIdändern,„reagiert“ der Effekt auf Ihre Änderungenund synchronisiert sich neu. Sie können anhand der Konsolenmeldungen erkennen, dass der Chat sich jedes Mal neu verbindet, wenn Sie die Abhängigkeiten Ihres Effekts ändern.
Verschieben Sie nun den Code des Effekts in einen benutzerdefinierten Hook:
Dadurch kann IhreChatRoom-Komponente Ihren benutzerdefinierten Hook aufrufen, ohne sich darum kümmern zu müssen, wie er intern funktioniert:
Das sieht viel einfacher aus! (Aber es macht dasselbe.)
Beachten Sie, dass die Logikweiterhin aufÄnderungen von Props und State reagiert. Versuchen Sie, die Server-URL oder den ausgewählten Raum zu bearbeiten:
Beachten Sie, wie Sie den Rückgabewert eines Hooks nehmen:
und ihn als Eingabe an einen anderen Hook übergeben:
Jedes Mal, wenn IhreChatRoom-Komponente neu gerendert wird, übergibt sie die neuesten Werte vonroomIdundserverUrlan Ihren Hook. Deshalb verbindet sich Ihr Effekt bei jedem erneuten Rendern neu mit dem Chat, wenn sich deren Werte geändert haben. (Wenn Sie schon einmal mit Audio- oder Videobearbeitungssoftware gearbeitet haben, könnte Sie das Verketten von Hooks an das Verketten visueller oder auditiver Effekte erinnern. Es ist, als ob die Ausgabe vonuseStatein die Eingabe vonuseChatRoom)
Event-Handler an benutzerdefinierte Hooks übergeben
Wenn Sie beginnen,useChatRoomin mehr Komponenten zu verwenden, möchten Sie vielleicht, dass Komponenten sein Verhalten anpassen können. Zum Beispiel ist derzeit die Logik für den Umgang mit eingehenden Nachrichten fest im Hook codiert:
Angenommen, Sie möchten diese Logik zurück in Ihre Komponente verschieben:
Um dies zu ermöglichen, ändern Sie Ihren benutzerdefinierten Hook so, dass eronReceiveMessageals eine seiner benannten Optionen akzeptiert:
Das funktioniert, aber es gibt noch eine Verbesserung, die Sie vornehmen können, wenn Ihr benutzerdefinierter Hook Event-Handler akzeptiert.
Eine Abhängigkeit vononReceiveMessagehinzuzufügen ist nicht ideal, da sie dazu führt, dass der Chat bei jedem erneuten Rendern der Komponente neu verbunden wird.Wickeln Sie diesen Event-Handler in einen Effekt-Ereignis-Handler, um ihn aus den Abhängigkeiten zu entfernen:
Jetzt wird der Chat nicht mehr bei jedem erneuten Rendern derChatRoom-Komponente neu verbunden. Hier ist eine voll funktionsfähige Demo zum Übergeben eines Event-Handlers an einen benutzerdefinierten Hook, mit der Sie experimentieren können:
Beachten Sie, dass Sie nicht mehr wissen müssen,wieuseChatRoomfunktioniert, um es zu verwenden. Sie könnten es zu jeder anderen Komponente hinzufügen, beliebige andere Optionen übergeben, und es würde genauso funktionieren. Das ist die Stärke von benutzerdefinierten Hooks.
Wann benutzerdefinierte Hooks verwendet werden sollten
Sie müssen nicht für jedes kleine Stück doppelten Code einen benutzerdefinierten Hook extrahieren. Etwas Duplizierung ist in Ordnung. Zum Beispiel ist das Extrahieren einesuseFormInputHooks, um einen einzelnenuseState-Aufruf wie zuvor zu kapseln, wahrscheinlich unnötig.
Wenn Sie jedoch einen Effect schreiben, sollten Sie überlegen, ob es klarer wäre, ihn auch in einen benutzerdefinierten Hook zu kapseln.Sie sollten Effects nicht sehr oft benötigen,wenn Sie also einen schreiben, bedeutet das, dass Sie „außerhalb von React treten“ müssen, um mit einem externen System zu synchronisieren oder etwas zu tun, für das React keine eingebaute API hat. Das Kapseln in einen benutzerdefinierten Hook ermöglicht es Ihnen, Ihre Absicht und den Datenfluss präzise zu kommunizieren.
Betrachten Sie zum Beispiel eineShippingForm-Komponente, die zwei Dropdowns anzeigt: eines zeigt die Liste der Städte und das andere die Liste der Gebiete in der ausgewählten Stadt. Sie könnten mit Code beginnen, der so aussieht:
Obwohl dieser Code ziemlich repetitiv ist,ist es korrekt, diese Effects voneinander getrennt zu halten.Sie synchronisieren zwei verschiedene Dinge, daher sollten Sie sie nicht in einen Effect zusammenführen. Stattdessen können Sie dieShippingForm-Komponente oben vereinfachen, indem Sie die gemeinsame Logik zwischen ihnen in Ihren eigenenuseDataHook extrahieren:
Jetzt können Sie beide Effects in denShippingForm-Komponenten durch Aufrufe vonuseDataersetzen:
Das Extrahieren eines benutzerdefinierten Hooks macht den Datenfluss explizit. Sie geben dieurlein und erhalten diedatazurück. Durch das „Verstecken“ Ihres Effects innerhalb vonuseDataverhindern Sie auch, dass jemand, der an derShippingForm-Komponente arbeitet,unnötige Abhängigkeitenhinzufügt. Mit der Zeit werden die meisten Effects Ihrer App in benutzerdefinierten Hooks sein.
Custom Hooks helfen Ihnen, zu besseren Mustern zu migrieren
Effects sind eine„Notlösung“: Sie verwenden sie, wenn Sie „außerhalb von React treten“ müssen und es keine bessere eingebaute Lösung für Ihren Anwendungsfall gibt. Mit der Zeit ist es das Ziel des React-Teams, die Anzahl der Effects in Ihrer App auf ein Minimum zu reduzieren, indem es spezifischere Lösungen für spezifischere Probleme bereitstellt. Das Einwickeln Ihrer Effects in Custom Hooks erleichtert das Aktualisieren Ihres Codes, wenn diese Lösungen verfügbar werden.
Kehren wir zu diesem Beispiel zurück:
Im obigen Beispiel wirduseOnlineStatusmit einem Paar aususeStateunduseEffectimplementiert. Dies ist jedoch nicht die bestmögliche Lösung. Es gibt eine Reihe von Randfällen, die nicht berücksichtigt werden. Zum Beispiel wird angenommen, dass beim Mounten der KomponenteisOnlinebereitstrueist, aber das könnte falsch sein, wenn das Netzwerk bereits offline gegangen ist. Sie können die Browser-APInavigator.onLineverwenden, um das zu überprüfen, aber ihre direkte Verwendung würde auf dem Server für die Erzeugung des initialen HTML nicht funktionieren. Kurz gesagt, dieser Code könnte verbessert werden.
React enthält eine spezielle API namensuseSyncExternalStore, die all diese Probleme für Sie übernimmt. Hier ist IhruseOnlineStatusHook, umgeschrieben, um diese neue API zu nutzen:
Beachten Sie, dassSie keine der Komponenten ändern mussten, um diese Migration durchzuführen:
Das ist ein weiterer Grund, warum es oft vorteilhaft ist, Effekte in benutzerdefinierten Hooks zu kapseln:
- Sie machen den Datenfluss zu und von Ihren Effekten sehr explizit.
- Sie lassen Ihre Komponenten sich auf die Absicht konzentrieren, anstatt auf die genaue Implementierung Ihrer Effekte.
- Wenn React neue Funktionen hinzufügt, können Sie diese Effekte entfernen, ohne Ihre Komponenten zu ändern.
Ähnlich wie bei einemDesignsystemkann es hilfreich sein, gängige Idiome aus den Komponenten Ihrer App in benutzerdefinierte Hooks zu extrahieren. Dadurch bleibt der Code Ihrer Komponenten auf die Absicht fokussiert, und Sie müssen selten rohe Effekte schreiben. Viele ausgezeichnete benutzerdefinierte Hooks werden von der React-Community gepflegt.
Es gibt mehr als einen Weg, es zu tun
Nehmen wir an, Sie möchten eine Einblend-Animationvon Grund aufmit der Browser-APIrequestAnimationFrameimplementieren. Sie könnten mit einem Effect beginnen, der eine Animationsschleife einrichtet. In jedem Frame der Animation könnten Sie die Deckkraft des DOM-Knotens ändern, den Siein einer Ref halten, bis sie1erreicht. Ihr Code könnte so beginnen:
Um die Komponente besser lesbar zu machen, könnten Sie die Logik in einen benutzerdefinierten HookuseFadeInauslagern:
Sie könnten den Code vonuseFadeInso belassen, aber Sie könnten ihn auch weiter umstrukturieren. Zum Beispiel könnten Sie die Logik für die Einrichtung der Animationsschleife aususeFadeInin einen benutzerdefinierten HookuseAnimationLoopauslagern:
Das mussten Sie jedochnichttun. Wie bei regulären Funktionen entscheiden Sie letztendlich, wo Sie die Grenzen zwischen verschiedenen Teilen Ihres Codes ziehen. Sie könnten auch einen ganz anderen Ansatz wählen. Anstatt die Logik im Effect zu belassen, könnten Sie den größten Teil der imperativen Logik in eine JavaScript-Klasse verschieben:
Effects ermöglichen es Ihnen, React mit externen Systemen zu verbinden. Je mehr Koordination zwischen Effects erforderlich ist (z. B. um mehrere Animationen zu verketten), desto sinnvoller ist es, diese Logik vollständig aus Effects und HooksherauszuziehenDann wird der extrahierte Code zum „externen System“
Die obigen Beispiele gehen davon aus, dass die Einblendlogik in JavaScript geschrieben werden muss. Diese spezielle Einblendanimation ist jedoch sowohl einfacher als auch viel effizienter mit einer einfachenCSS-Animation zu implementieren:
Manchmal brauchen Sie nicht einmal einen Hook!
Zusammenfassung
- Custom Hooks ermöglichen es Ihnen, Logik zwischen Komponenten zu teilen.
- Custom Hooks müssen mit
usebeginnen, gefolgt von einem Großbuchstaben. - Custom Hooks teilen nur zustandsbehaftete Logik, nicht den Zustand selbst.
- Sie können reaktive Werte von einem Hook an einen anderen übergeben, und sie bleiben aktuell.
- Alle Hooks werden bei jedem erneuten Rendern Ihrer Komponente erneut ausgeführt.
- Der Code Ihrer Custom Hooks sollte rein sein, wie der Code Ihrer Komponente.
- Wrap event handlers received by custom Hooks into Effect Events.
Try out some challenges
Challenge 1 of 5:Extract a useCounter Hook #
This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called useCounter. Your goal is to make the Counter component implementation look exactly like this:
export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}You’ll need to write your custom Hook in useCounter.js and import it into the App.js file.
