Behalten Sie die Hooks ohne Kopf bei: Trennung des Benachrichtigungsverhaltens von der Darstellung der Benachrichtigung
Erfahren Sie, warum ein React-Hook, der JSX zurückgibt, einen Teil Ihres Komponentenbaums versteckt und Verhalten mit der Präsentation verknüpft, sowie wie Sie ihn umstrukturieren können, um stattdessen den Zustand freizugeben.
Custom Hooks eignen sich hervorragend dazu, Implementierungsdetails zu verbergen – manchmal jedoch verbergen sie zu viel. Ein Hook, der fertiges JSX zurückgibt, erscheint am Aufrufort praktisch, doch er verbirgt einen Teil Ihres UI-Baums hinter einer API, die wie reine Daten aussieht, und bindet das Verhalten an eine bestimmte Darstellung. Mit Hilfe eines typischen Alerts-Hooks aus einem Dashboard-Editor zeigt dieser Artikel, wie man solche Probleme erkennt, wie man zu einem headless Hook refaktorieren kann und welche Vorteile sich durch die zusätzlichen Zeilen ergeben.
Ein Alerts-Hook, der seine eigene UI rendernt
Alerts eignen sich ideal für Hooks: Sie benötigen einen Zustand, möglicherweise Timer, Schließfunktionen sowie eine Warteschlange. So wird ein solcher Hook genutzt:
const {
showSuccess,
showError,
alerts,
} = useAlerts();
return (
<>
{alerts}
<Dashboard />
</>
);
Es gibt bereits einen Hinweis darauf, dass etwas nicht stimmt. alerts wird direkt in den JSX eingefügt, was stark darauf hindeutet, dass der Hook rendernde Elemente zurückgibt. Der Hook ist für das Verhalten verantwortlich, da er showSuccess und showError bereitstellt, und offensichtlich auch dafür, wie die Alerts aussehen.
Die Implementierung bestätigt dies:
const useAlerts = () => {
const [alerts, setAlerts] = useState<Alert[]>([]);
const showSuccess = (message: string) => {
// update state
};
const showError = (message: string) => {
// update state
};
...
return {
showSuccess,
showError,
alerts: (
<AlertContainer>
{alerts.map(({ id, type, message }) => (
<Alert
key={id}
type={type}
message={message}
/>
))}
</AlertContainer>
),
};
};
useAlerts kontrolliert den Alert-Zustand, die Funktionen zur Änderung dieses Zustands sowie die Markup-Struktur: den AlertContainer-Wrapper und einen Alert pro Eintrag. Die Trennung zwischen Logik und Präsentation ist verschwunden.
Warum der zurückgegebene JSX irreführend ist
Er sieht aus wie Daten
Von der Seite des Benutzers aus gibt es kein Anzeichen dafür, dass alerts ein React-Unterbaum ist. Es befindet sich neben zwei Funktionen, hat einen Pluralnamen, der auf eine Liste von Einträgen hindeutet, und könnte leicht mit dem Zustand verwechselt werden:
const {
showSuccess,
showError,
alerts,
} = useAlerts();
Der einzige Weg, die Wahrheit herauszufinden, ist, den Hook zu öffnen.
Er versteckt die Herkunft der Benutzeroberfläche
Ein Komponentenbaum ist teilweise wertvoll, weil er dokumentiert, wo jeder Bestandteil der Benutzeroberfläche seinen Ursprung hat. Wenn man das Dashboard betrachtet und Folgendes sieht:
{alerts}
<Dashboard />
man kann ohne Einblick in den Hook nicht erkennen, was alerts darstellt, welche Komponenten es einbindet, von welchem Kontext oder Props diese Komponenten abhängen oder wie sie gestylt sind. Ein Teil des Baums wurde hinter etwas verlagert, das wie ein Verhaltensmechanismus aussieht.
Er verknüpft zwei unzusammenhängende Entscheidungen
Der Hook kann das Alert-Verhalten nicht mehr ohne gleichzeitige Festlegung des Alert-Komponenten-Elements freigeben. Jeder Bildschirm, der die Alert-Logik verwenden möchte, erbt das Aussehen des Alerts – unabhängig davon, ob es passt oder nicht. Es gibt keinen technischen Grund dafür, dass diese beiden Aspekte zusammenhängen müssen.
Refactoring zu einem headless Hook
Der Hook sollte ausschließlich den Zustand sowie die darauf ausgeführten Operationen verwalten. Es sollten die rohe Alert-Liste sowie die dazugehörigen Aktionen bereitgestellt werden, einschließlich einer Möglichkeit, ein Alert zu schließen:
const {
alerts,
showSuccess,
showError,
dismissAlert,
} = useAlerts();
Anschließend sollten die Alerts in der Komponente dargestellt werden, wohin die Darstellung ohnehin gehört:
<AlertContainer>
{alerts.map(({ id, type, message }) => (
<Alert
key={id}
type={type}
message={message}
onDismiss={() => dismissAlert(id)}
/>
))}
</AlertContainer>
<Dashboard />
Nun ist der Komponentenbaum wieder klar strukturiert. Jeder, der ihn liest, erkennt einen AlertContainer, jeweils einen Alert pro Eintrag, der über id identifiziert wird, sowie einen Handler zum Schließen des Alerts, der wieder mit dem Hook verbunden ist. Die Rolle des Hooks ist eindeutig: Er stellt den Zustand und die Aktionen bereit.
Ist der zusätzliche Code ein Problem?
Der verwendende Komponententeil wird tatsächlich länger – und das ist in Ordnung. Es ist verlockend, eine API so weit zu polieren, dass der Aufrufer fast nichts mehr schreiben muss, doch diese Polierung kann nützliche architektonische Informationen zerstören. Fünf Zeilen mit offensichtlicher Struktur sind leichter zu warten als ein einziger {alerts}, der einen ganzen UI-Bereich verbirgt.
Falls mehrere Bildschirme Benachrichtigungen identisch darstellen, lässt sich Wiederholung trotzdem vermeiden, ohne die UI wieder in den Hook einzubinden: Erstellen Sie eine AlertList-Komponente, die alerts und onDismiss als Props entgegennimmt, oder eine Komponente, die den Hook selbst aufruft. Auf jeden Fall befindet sich die Darstellung in einer Komponente, die im Baum sichtbar ist und wie jede andere Komponente analysiert, gememorisert und getestet werden kann.
Wiederverwendung wird möglich
Durch die integrierte JSX führt die Wiederverwendung des Verhaltens zwangsläufig zur Wiederverwendung der Darstellung – eine Annahme, die nicht immer zutrifft. Bei der headless-Version können ein Toast-Bereich, ein inline-Banner sowie eine mobile Anordnung denselben Alert-Lebenszyklus teilen, während sie ihn unterschiedlich darstellen. Das Alert-Komponente kann sich außerdem eigenständig weiterentwickeln, ohne den Hook zu berühren, der die Zeitsteuerung und das Löschen steuert.
Eine verwandte Falle: Hooks, die Komponenten zurückgeben
Ein naher Verwandter dieses Problems sind Hooks, die eine Komponentenfunktion definieren und zurückgeben, wie zum Beispiel const AlertView = () => ... innerhalb des Hook-Körpers. Da bei jeder Renderung eine neue Funktion erstellt wird, erkennt React jedes Mal einen neuen Komponententyp und entmontiert sowie remontiert diesen Unterbaum, wodurch sein interner Zustand und das DOM gelöscht werden. Das Zurückgeben von Elementen, wie es der ursprüngliche Hook tut, vermeidet dieses spezielle Problem, doch das oben beschriebene Designproblem bleibt bestehen.
Kernpunkte
- JSX, das aus einem Hook stammt, bedeutet in der Regel, dass zwei Verantwortlichkeiten in einer Abstraktion zusammengeführt wurden.
- Ein zurückgegebener Wert, der wie Daten aussieht, aber tatsächlich zur UI gehört, verdeckt Teile des Komponentenbaums vor den Lesern.
- Lassen Sie Hooks kopflos sein: Geben Sie Zustand und Aktionen zurück und rendern Sie in Komponenten.
- Für gemeinsame Darstellungen sollten Sie eine Komponente extrahieren, anstatt vom Hook Markup zurückzugeben.
- Beim Überprüfen eines solchen Hooks sollten Sie sich eine Frage stellen: Welchen Grund hat dieser Hook, darüber zu entscheiden, wie die UI aussieht?