Revolution oder Schmerz? Yandex React Hooks-Bericht

Mein Name ist Artyom Berezin, ich bin Entwickler mehrerer interner Yandex-Dienste. In den letzten sechs Monaten habe ich aktiv mit React Hooks gearbeitet. Dabei gab es einige Schwierigkeiten, die bekämpft werden mussten. Jetzt möchte ich diese Erfahrung mit Ihnen teilen. In dem Bericht habe ich die React Hook-API aus praktischer Sicht untersucht. Warum brauchen wir Hooks? Lohnt es sich zu wechseln, was beim Portieren besser zu berücksichtigen ist? Es ist leicht, während des Übergangs Fehler zu machen, aber es ist auch nicht so schwierig, sie zu vermeiden.



- Hooks sind nur eine weitere Möglichkeit, die Logik Ihrer Komponenten zu beschreiben. Sie können den Funktionskomponenten einige Funktionen hinzufügen, die bisher nur Komponenten in Klassen eigen waren.



Zuallererst ist es Unterstützung für den internen Zustand, dann - Unterstützung für Nebenwirkungen. Zum Beispiel - Netzwerkanforderungen oder Anforderungen an WebSocket: Abonnement, Abmeldung von einigen Kanälen. Oder wir sprechen über Anfragen an andere asynchrone oder synchrone Browser-APIs. Außerdem geben uns Haken Zugang zum Lebenszyklus der Komponente, zu ihrem Beginn des Lebens, dh zur Montage, zur Aktualisierung ihrer Requisiten und zu ihrem Tod.



Wahrscheinlich der einfachste Weg, um im Vergleich zu veranschaulichen. Hier ist der einfachste Code, der nur mit einer Komponente in Klassen vorhanden sein kann. Die Komponente verändert etwas. Dies ist ein regulärer Zähler, der erhöht oder verringert werden kann, nur ein Feld im Status. Im Allgemeinen denke ich, dass der Code für Sie völlig offensichtlich ist, wenn Sie mit React vertraut sind.



Eine ähnliche Komponente, die genau dieselbe Funktion ausführt, jedoch in Hooks geschrieben ist, sieht viel kompakter aus. Nach meinen Berechnungen nimmt der Code beim Portieren von Komponenten in Klassen zu Komponenten in Hooks im Durchschnitt etwa anderthalb Mal ab, und es gefällt ihm.

Ein paar Worte darüber, wie Haken funktionieren. Ein Hook ist eine globale Funktion, die in React deklariert und jedes Mal aufgerufen wird, wenn eine Komponente gerendert wird. React verfolgt die Aufrufe dieser Funktionen und kann deren Verhalten ändern oder entscheiden, was zurückgegeben werden soll.



Es gibt einige Einschränkungen bei der Verwendung von Hooks, die sie von normalen Funktionen unterscheiden. Erstens können sie nicht in Komponenten für Klassen verwendet werden. Eine solche Einschränkung gilt nur, weil sie nicht für sie erstellt wurden, sondern für funktionale Komponenten. Hooks können nicht innerhalb interner Funktionen, innerhalb von Schleifen, Bedingungen aufgerufen werden. Nur auf der ersten Ebene der Verschachtelung innerhalb der Komponentenfunktionen. Diese Einschränkung wird von React selbst auferlegt, um verfolgen zu können, welche Hooks aufgerufen wurden. Und er stapelt sie in einer bestimmten Reihenfolge in seinem Gehirn. Wenn sich diese Reihenfolge plötzlich ändert oder einige verschwinden, sind komplexe, schwer fassbare und schwer zu debuggende Fehler möglich.

Wenn Sie jedoch eine recht komplizierte Logik haben und beispielsweise Hooks in Hooks verwenden möchten, ist dies höchstwahrscheinlich ein Zeichen dafür, dass Sie einen Hook erstellen sollten. Angenommen, Sie stellen mehrere Haken her, die in einem separaten benutzerdefinierten Haken miteinander verbunden sind. Und darin können Sie andere benutzerdefinierte Hooks verwenden, wodurch eine Hierarchie von Hooks erstellt wird, die die allgemeine Logik dort hervorhebt.



Haken bieten einige Vorteile gegenüber Klassen. Zunächst können Sie, wie im vorherigen Abschnitt beschrieben, mit benutzerdefinierten Hooks die Logik viel einfacher fummeln. Zuvor haben wir unter Verwendung des Ansatzes mit Komponenten höherer Ordnung eine Art gemeinsame Logik entworfen, die die Komponente umhüllte. Jetzt setzen wir diese Logik in Haken. Dadurch wird der Komponentenbaum reduziert: Seine Verschachtelung wird reduziert, und es wird für React einfacher, Komponentenänderungen zu verfolgen, den Baum neu zu berechnen, das virtuelle DOM neu zu berechnen usw. Dies löst das Problem der sogenannten Wrapper-Hölle. Ich denke, diejenigen, die mit Redux arbeiten, sind damit vertraut.

Mit Hooks geschriebener Code lässt sich mit modernen Minimierern wie Terser oder altem UglifyJS viel einfacher minimieren. Tatsache ist, dass wir die Namen von Methoden nicht speichern müssen, wir müssen nicht über Prototypen nachdenken. Wenn das Ziel nach der Transpilation ES3 oder ES5 ist, erhalten wir normalerweise eine Reihe von Prototypen, die gepatcht werden. Hier muss dies alles nicht getan werden, daher ist es einfacher zu minimieren. Und da wir keine Klassen verwenden, müssen wir nicht darüber nachdenken. Für Anfänger ist dies oft ein großes Problem und wahrscheinlich einer der Hauptgründe für Fehler: Wir vergessen, dass dies ein Fenster sein kann, dass wir eine Methode binden müssen, zum Beispiel im Konstruktor oder auf andere Weise.

Durch die Verwendung von Hooks können Sie auch die Logik hervorheben, die einen Nebeneffekt steuert. Bisher musste diese Logik, insbesondere wenn wir mehrere Nebenwirkungen für eine Komponente haben, in verschiedene Methoden des Lebenszyklus der Komponente unterteilt werden. Und seit dem Erscheinen von Minimierungs-Hooks, React.memo, eignen sich nun funktionale Komponenten zum Auswendiglernen, dh diese Komponente wird bei uns nicht neu erstellt oder aktualisiert, wenn sich ihre Requisiten nicht geändert haben. Dies war vorher nicht möglich, jetzt ist es möglich. Alle Funktionskomponenten können in Memos verpackt werden. Außerdem wurde der useMemo-Hook angezeigt, mit dem wir einige schwere Werte berechnen oder einige Dienstprogrammklassen nur einmal instanziieren können.

Der Bericht wird unvollständig sein, wenn ich nicht über einige grundlegende Haken spreche. Zuallererst sind dies State-Management-Hooks.



Zunächst einmal - useState.



Ein Beispiel ähnelt dem am Anfang des Berichts. useState ist eine Funktion, die einen Anfangswert annimmt und ein Tupel aus dem aktuellen Wert und der Funktion zum Ändern dieses Werts zurückgibt. Alle Magie wird von React intern serviert. Wir können diesen Wert einfach entweder lesen oder ändern.

Im Gegensatz zu Klassen können wir so viele Statusobjekte verwenden, wie wir benötigen. Der Status wird in logische Teile zerlegt, um sie nicht wie in Klassen in einem Objekt zu mischen. Und diese Teile werden vollständig voneinander isoliert: Sie können unabhängig voneinander geändert werden. Das Ergebnis dieses Codes: Wir ändern zwei Variablen, berechnen das Ergebnis und zeigen Schaltflächen an, mit denen wir die erste Variable hier und da und die zweite Variable hier und da ändern können. Denken Sie an dieses Beispiel, denn später werden wir etwas Ähnliches tun, aber viel komplizierter.



Es gibt einen solchen useState für Steroide für Redux-Liebhaber. Sie können den Status mithilfe eines Reduzierers konsistenter ändern. Ich denke, dass diejenigen, die mit Redux vertraut sind, nicht einmal erklären können, für diejenigen, die nicht vertraut sind, werde ich sagen.

Ein Reduzierer ist eine Funktion, die einen Status akzeptiert, und ein Objekt, das normalerweise als Aktion bezeichnet wird und beschreibt, wie sich dieser Status ändern soll. Genauer gesagt, es werden einige Parameter übergeben, und innerhalb des Reduzierers entscheidet es bereits abhängig von seinen Parametern, wie sich der Status ändert. Infolgedessen sollte ein neuer Status zurückgegeben und aktualisiert werden.



Auf ungefähr diese Weise wird es im Komponentencode verwendet. Wir haben einen useReducer-Hook, der eine Reducer-Funktion benötigt, und der zweite Parameter ist der Anfangswert des Zustands. Gibt wie useState, den aktuellen Status und die Funktion zum Ändern zurück, ist dispatch. Wenn Sie ein Aktionsobjekt an den Versand übergeben, rufen wir eine Statusänderung auf.



Sehr wichtige VerwendungEffekthaken. Sie können der Komponente Nebenwirkungen hinzufügen, die eine Alternative zum Lebenszyklus darstellen. In diesem Beispiel verwenden wir eine einfache Methode mit useEffect: Sie fordert lediglich einige Daten vom Server an, beispielsweise mit der API, und zeigt diese Daten auf der Seite an.



UseEffect verfügt über einen erweiterten Modus. Wenn die an useEffect übergebene Funktion eine andere Funktion zurückgibt, wird diese Funktion im nächsten Zyklus aufgerufen, wenn dieser useEffect angewendet wird.

Ich habe vergessen zu erwähnen, dass useEffect direkt nach dem Anwenden der Änderung auf das DOM asynchron aufgerufen wird. Das heißt, es garantiert, dass es ausgeführt wird, nachdem die Komponente gerendert wurde, und kann zum nächsten Rendern führen, wenn sich einige Werte ändern.



Hier begegnen wir zum ersten Mal einem Konzept wie Abhängigkeiten. Einige Hooks - useEffect, useCallback, useMemo - verwenden ein Array von Werten als zweites Argument, mit dem wir sagen können, was verfolgt werden soll. Änderungen in diesem Array führen zu Effekten. Zum Beispiel haben wir hier hypothetisch eine Art Komponente für die Auswahl eines Autors aus einer Liste. Und ein Teller mit Büchern dieses Autors. Und wenn sich der Autor ändert, wird useEffect aufgerufen. Wenn diese authorId geändert wird, wird eine Anfrage aufgerufen und Bücher werden geladen.

Ich erwähne auch beim Übergeben von Hooks wie useRef, dass dies eine Alternative zu React.createRef ist, ähnlich wie useState, aber Änderungen an ref führen nicht zum Rendern. Manchmal praktisch für einige Hacks. Mit useImperativeHandle können wir bestimmte "öffentliche Methoden" für die Komponente deklarieren. Wenn Sie useRef in der übergeordneten Komponente verwenden, können diese Methoden abgerufen werden. Um ehrlich zu sein, habe ich es einmal zu Bildungszwecken versucht, in der Praxis war es nicht nützlich. useContext ist nur eine gute Sache. Sie können den aktuellen Wert aus dem Kontext übernehmen, wenn der Anbieter diesen Wert irgendwo höher in der Hierarchieebene definiert hat.

Es gibt eine Möglichkeit, React-Anwendungen an Hooks zu optimieren: Memoisierung. Das Auswendiglernen kann in interne und externe unterteilt werden. Zuerst über die Außenseite.



Dies ist React.memo, praktisch eine Alternative zur React.PureComponent-Klasse, die Änderungen an Requisiten und geänderten Komponenten nur dann verfolgte, wenn sich Requisiten oder Status änderten.

Hier ähnlich jedoch ohne Staat. Es überwacht auch Änderungen an Requisiten, und wenn sich die Requisiten geändert haben, tritt ein Renderer auf. Wenn sich die Requisiten nicht geändert haben, wird die Komponente nicht aktualisiert, und wir sparen dies.



Interne Optimierungsmethoden. Zuallererst ist dies eine eher einfache Sache - useMemo, selten verwendet. Sie können einen Wert berechnen und nur dann neu berechnen, wenn sich die in den Abhängigkeiten angegebenen Werte geändert haben.



Es gibt einen Sonderfall von useMemo für eine Funktion namens useCallback. Es wird hauptsächlich verwendet, um den Wert von Ereignishandlerfunktionen zu speichern, die an untergeordnete Komponenten übergeben werden, damit diese untergeordneten Komponenten nicht erneut gerendert werden können. Es wird einfach verwendet. Wir beschreiben eine bestimmte Funktion, verpacken sie in useCallback und geben an, von welchen Variablen sie abhängt.

Viele Menschen haben eine Frage, aber brauchen wir diese? Brauchen wir haken Bewegen wir uns oder bleiben wir wie zuvor? Es gibt keine einzige Antwort, alles hängt von den Vorlieben ab. Erstens, wenn Sie direkt fest an die objektorientierte Programmierung gebunden sind und Ihre Komponenten als Klasse daran gewöhnt sind, haben sie Methoden, die abgerufen werden können, dann scheint Ihnen diese Sache wahrscheinlich überflüssig zu sein. Im Prinzip schien es mir, als ich zum ersten Mal von Hooks hörte, dass es zu kompliziert war, dass eine Art Magie hinzugefügt wurde und es nicht klar war, warum.

Für Liebhaber von Funktionalitäten ist dies beispielsweise ein Muss, da Hooks Funktionen sind und funktionale Programmiertechniken auf sie anwendbar sind. Sie können sie beispielsweise kombinieren oder überhaupt etwas tun, indem Sie beispielsweise Bibliotheken wie Ramda und dergleichen verwenden.



Da wir Klassen losgeworden sind, müssen wir diesen Kontext nicht mehr an Methoden binden. Wenn Sie diese Methoden als Rückruf verwenden. Normalerweise war dies ein Problem, da Sie daran denken mussten, sie im Konstruktor zu binden oder eine inoffizielle Erweiterung der Sprachsyntax zu verwenden, z. B. Pfeilfunktionen als Eigenschaft. Ziemlich übliche Praxis. Ich habe meinen Dekorateur benutzt, der im Prinzip auch experimentell auf Methoden basiert.



Es gibt einen Unterschied darin, wie der Lebenszyklus funktioniert und wie er verwaltet wird. Hooks verknüpfen fast alle Lebenszyklusaktionen mit dem useEffect-Hook, mit dem Sie sowohl die Geburt als auch die Aktualisierung einer Komponente und deren Tod abonnieren können. In den Klassen mussten wir dafür verschiedene Methoden neu definieren, z. B. componentDidMount, componentDidUpdate und componentWillUnmount. Außerdem sollte die shouldComponentUpdate-Methode jetzt durch React.memo ersetzt werden.



Es gibt einen relativ kleinen Unterschied im Umgang mit dem Staat. Erstens haben Klassen ein Zustandsobjekt. Wir mussten dort alles stopfen. In Hooks können wir den logischen Zustand in einige Teile zerlegen, was für uns praktisch wäre, wenn wir separat arbeiten würden.

setState () von Komponenten in Klassen, die einen Status-Patch angeben dürfen, wodurch ein oder mehrere Felder des Status geändert werden. In Hooks müssen wir den gesamten Zustand als Ganzes ändern, und das ist sogar gut so, denn es ist in Mode, alle möglichen unveränderlichen Dinge zu verwenden und niemals zu erwarten, dass unsere Objekte mutieren. Sie sind immer neu bei uns.

Das Hauptmerkmal von Klassen, die Hooks nicht haben: Wir könnten Statusänderungen abonnieren. Das heißt, wir ändern den Status und abonnieren sofort seine Änderungen, wobei wir unmittelbar nach dem Anwenden der Änderungen unbedingt etwas verarbeiten müssen. Bei Hooks funktioniert dies einfach nicht. Dies muss auf sehr interessante Weise geschehen, ich werde es Ihnen weiter erzählen.

Und ein wenig über die funktionale Art der Aktualisierung. Es funktioniert sowohl dort als auch dort, wenn die Statusänderungsfunktionen eine andere Funktion akzeptieren, die dieser Status nicht ändern, sondern erstellen soll. Und wenn im Fall der Klassenkomponente eine Art Patch an uns zurückgegeben werden kann, müssen wir in den Hooks den gesamten neuen Wert zurückgeben.

Im Allgemeinen ist es unwahrscheinlich, dass Sie eine Antwort erhalten, ob Sie sich bewegen oder nicht. Aber ich rate zumindest, zumindest für den neuen Code zu versuchen, ihn zu fühlen. Als ich gerade anfing, mit Hooks zu arbeiten, identifizierte ich sofort mehrere benutzerdefinierte Hooks, die für mich für mein Projekt geeignet sind. Grundsätzlich habe ich versucht, einige der Funktionen zu ersetzen, die ich durch Komponenten höherer Ordnung implementiert hatte.



useDismounted - Für diejenigen, die mit RxJS vertraut sind, besteht die Möglichkeit, alle Observable innerhalb einer Komponente oder innerhalb einer Funktion in großen Mengen abzubestellen, indem jedes Observable ein spezielles Objekt, Subject, abonniert wird. Wenn es geschlossen wird, werden alle Abonnements gekündigt. Dies ist sehr praktisch, wenn die Komponente komplex ist und wenn im Observable viele asynchrone Vorgänge vorhanden sind. Es ist praktisch, sich von allen gleichzeitig und nicht von jedem einzeln abzumelden.

useObservable gibt einen Wert von Observable zurück, wenn dort ein neuer angezeigt wird. Ein ähnlicher useBehaviourSubject-Hook wird von BehaviourSubject zurückgegeben. Der Unterschied zu Observable besteht darin, dass es zunächst eine Bedeutung hat.

Mit dem praktischen benutzerdefinierten Hook useDebounceValue können wir beispielsweise einen Sujest für die Suchzeichenfolge organisieren, sodass nicht jedes Mal, wenn Sie eine Taste drücken, etwas an den Server gesendet wird, sondern gewartet wird, bis der Benutzer die Eingabe beendet hat.

Zwei ähnliche Haken. useWindowResize gibt aktuelle Istwerte für Fenstergrößen zurück. Der nächste Haken für die Bildlaufposition ist useWindowScroll. Ich benutze sie, um einige Popups oder modale Fenster wiederzugeben, wenn es komplizierte Dinge gibt, die mit CSS einfach nicht möglich sind.

Und so ein kleiner Haken zum Implementieren von Hotkeys, den die Komponente, wenn sie auf der Seite vorhanden ist, mit einem Hotkey abonniert. Wenn er stirbt, erfolgt eine automatische Abmeldung.

Wofür sind diese benutzerdefinierten Haken geeignet? Dass wir ein Abbestellen innerhalb des Hooks stopfen können und nicht daran denken müssen, das Abmelden irgendwo in der Komponente, in der dieser Hook verwendet wird, manuell abzubestellen.

Vor nicht allzu langer Zeit warfen sie mir einen Link zur React-Use-Bibliothek, und es stellte sich heraus, dass die meisten dieser benutzerdefinierten Hooks dort bereits implementiert waren. Und ich habe ein Fahrrad geschrieben. Dies ist manchmal nützlich, aber in Zukunft werde ich sie höchstwahrscheinlich wegwerfen und React-Use verwenden. Und ich rate Ihnen, auch zu prüfen, ob Sie Haken verwenden möchten.



Das Hauptziel des Berichts ist es, zu zeigen, wie man falsch schreibt, welche Probleme auftreten können und wie man sie vermeidet. Das allererste, wahrscheinlich, was jeder, der diese Hooks studiert und versucht, etwas zu schreiben, ist, useEffect falsch zu verwenden. Hier ist der Code ähnlich dem, den 100% jeder geschrieben hat, wenn er Hooks ausprobiert hat. Dies liegt an der Tatsache, dass useEffect zunächst mental als Alternative zu componentDidMount wahrgenommen wird. Im Gegensatz zu componentDidMount, das nur einmal aufgerufen wird, wird useEffect bei jedem Rendering aufgerufen. Und der Fehler hier ist, dass es beispielsweise die Datenvariable ändert und gleichzeitig zu einem Komponenten-Renderer führt, wodurch der Effekt erneut angefordert wird. Auf diese Weise erhalten wir eine endlose Reihe von AJAX-Anforderungen an den Server, und die Komponente selbst wird ständig aktualisiert, aktualisiert, aktualisiert.



Das Problem zu beheben ist sehr einfach. Sie müssen hier ein leeres Array der Abhängigkeiten hinzufügen, von denen es abhängt, und Änderungen, bei denen der Effekt neu gestartet wird. Wenn hier eine leere Liste von Abhängigkeiten angegeben ist, wird der Effekt dementsprechend nicht neu gestartet. Dies ist keine Art von Hack, sondern eine grundlegende Funktion der Verwendung von useEffect.



Nehmen wir an, wir haben es behoben. Jetzt etwas kompliziert. Wir haben eine Komponente, die etwas rendert, das für eine Art ID vom Server übernommen werden muss. In diesem Fall funktioniert im Prinzip alles einwandfrei, bis wir die entityId im übergeordneten Element ändern. Dies ist möglicherweise für Ihre Komponente nicht relevant.



Wenn es sich jedoch ändert oder geändert werden muss und Sie eine alte Komponente auf Ihrer Seite haben und sich herausstellt, dass sie nicht aktualisiert wird, ist es am besten, hier eine EntityId als Abhängigkeit hinzuzufügen, die die Aktualisierung verursacht und die Daten aktualisiert.



Ein komplexeres Beispiel mit useCallback. Hier ist auf den ersten Blick alles in Ordnung. Wir haben eine bestimmte Seite, die eine Art Countdown-Timer hat, oder umgekehrt einen Timer, der nur tickt. Eine Liste von Hosts und darüber hinaus Filter, mit denen Sie diese Liste von Hosts filtern können. Nun, hier wurde Wartung hinzugefügt, um einen sich häufig ändernden Wert zu veranschaulichen, der in einen Renderer übersetzt wird.

, , maintenance , , , onChange. onChange, . , HostFilters - , , dropdown, . , . , .



onChange useCallback. , .

, . , , . Facebook, React. , , , , '. , , confusing .



? — , - , , , , , . .

, , , , , , . , Garbage Collector , . , , , , . , , , reducer, , . , .

, , . - , , setValue - , , setState . - useEffect.

useEffect - , - , , , useEffect. useEffect , . , , Backbone, : , , , - . , , - , . - . , , , , - . , , , , , , . .

, , . , , . , . , . , , , dropdown . , . dropdown pop-up, useWindowScroll, useWindowResize , . , , — , .

, , . , , , , , . , , , , , .



, «», . , , TypeScript . . , reducer Redux , action. , action , action. , , , .

. , action. , , IncrementA 0, 1, 2, . . , , , , . action action, - . UnionType “Action”, , , action. .

— . , initialState, . , - . TypeScript. . , typeState , initialState.



reducer. State, Action, : switch action.type. TypeScript UnionType: case, - , type. action .

, : , , . .



? , . . , reducer. , action creator , , dispatch.



extension Dev Tools. . .

, , . , , . useDebugValue , - Dev Tool. useConstants, - , loaded, , , .



— . , . , . , , , . , , — - , — .

. Facebook ESLint, . , , . , dependencies . , , , .

, , , - , . , , , . . , - - .

— , , - . , , . , , - . , . . Nützliche Links:

Source: https://habr.com/ru/post/de464071/


All Articles