Verwalten des Status mit React Hooks - Ohne Redux- und Kontext-API

Hallo allerseits! Mein Name ist Arthur, ich arbeite als VKontakte im mobilen Webteam , ich bin am VKUI- Projekt beteiligt - einer Bibliothek von React-Komponenten, mit deren Hilfe einige unserer Schnittstellen in mobilen Anwendungen geschrieben werden. Die Frage der Arbeit mit einem globalen Staat steht uns noch offen. Es gibt mehrere bekannte Ansätze: Redux, MobX, Context API. Kürzlich bin ich auf einen Artikel von André Gardi State Management mit React Hooks - No Redux oder Context API gestoßen , in dem der Autor vorschlägt, React Hooks zu verwenden, um den Status der Anwendung zu steuern.

Hooks brechen schnell in das Leben von Entwicklern ein und bieten neue Möglichkeiten, um verschiedene Aufgaben und Ansätze zu lösen oder zu überdenken. Sie verändern unser Verständnis nicht nur der Beschreibung von Komponenten, sondern auch des Umgangs mit Daten. Lesen Sie die Übersetzung des Artikels und den Kommentar des Übersetzers unter der Katze.

Bild

Reaktionshaken sind leistungsfähiger als Sie denken


Heute werden wir React Hooks untersuchen und einen benutzerdefinierten Hook zum Verwalten des globalen Status der Anwendung entwickeln, der einfacher als die Redux-Implementierung und produktiver als die Context-API ist.

Grundlagen von Hooks reagieren


Sie können diesen Teil überspringen, wenn Sie bereits mit Hooks vertraut sind.

useState ()


Vor dem Auftreten von Hooks konnten Funktionskomponenten keinen lokalen Status festlegen. Die Situation hat sich mit dem Aufkommen von useState() geändert.



Dieser Aufruf gibt ein Array zurück. Das erste Element ist eine Variable, die den Zugriff auf den Statuswert ermöglicht. Das zweite Element ist eine Funktion, die den Status aktualisiert und die Komponente neu zeichnet, um die Änderungen widerzuspiegeln.

 import React, { useState } from 'react'; function Example() { const [state, setState] = useState({counter:0}); const add1ToCounter = () => { const newCounterValue = state.counter + 1; setState({ counter: newCounterValue}); } return ( <div> <p>You clicked {state.counter} times</p> <button onClick={add1ToCounter}> Click me </button> </div> ); } 

useEffect ()


Klassenkomponenten reagieren auf Nebenwirkungen mithilfe von Lebenszyklusmethoden wie componentDidMount() . Mit dem useEffect() Hook können Sie dasselbe in Funktionskomponenten tun.

Standardmäßig werden Effekte nach jedem Neuzeichnen ausgelöst. Sie können jedoch sicherstellen, dass sie erst ausgeführt werden, nachdem Sie die Werte bestimmter Variablen geändert haben, und ihnen den zweiten optionalen Parameter in Form eines Arrays übergeben.

 //     useEffect(() => { console.log('     '); }); //    useEffect(() => { console.log('     valueA'); }, [valueA]); 

Um ein ähnliches Ergebnis wie componentDidMount() zu erzielen, übergeben wir ein leeres Array an den zweiten Parameter. Da der Inhalt eines leeren Arrays immer unverändert bleibt, wird der Effekt nur einmal ausgeführt.

 //     useEffect(() => { console.log('    '); }, []); 

Staatsteilung


Wir haben gesehen, dass ein Hook-Status genauso funktioniert wie ein Klassenkomponentenstatus. Jede Komponenteninstanz hat ihren eigenen internen Status.

Um den Status zwischen den Komponenten zu teilen, erstellen wir unseren eigenen Hook.



Die Idee ist, ein Array von Listenern und nur einen Status zu erstellen. Jedes Mal, wenn eine Komponente den Status ändert, rufen alle abonnierten Komponenten ihren getState() und werden aus diesem Grund aktualisiert.

Wir können dies erreichen, indem useState() in unserem benutzerdefinierten Hook aufrufen. Anstatt die Funktion setState() , fügen wir sie dem Array von Listenern hinzu und geben eine Funktion zurück, die das setState() intern aktualisiert und alle Listener aufruft.

Warte einen Moment. Wie erleichtert es mir das Leben?


Ja, Sie haben Recht. Ich habe ein NPM-Paket erstellt , das die gesamte beschriebene Logik enthält.

Sie müssen es nicht in jedem Projekt implementieren. Wenn Sie keine Zeit mehr mit Lesen verbringen und das Endergebnis sehen möchten, fügen Sie dieses Paket einfach Ihrer Anwendung hinzu.

 npm install -s use-global-hook 

Um zu verstehen, wie mit einem Paket gearbeitet wird, lesen Sie die Beispiele in der Dokumentation. Und jetzt möchte ich mich darauf konzentrieren, wie das Paket im Inneren angeordnet ist.

Erste Version


 import { useState, useEffect } from 'react'; let listeners = []; let state = { counter: 0 }; const setState = (newState) => { state = { ...state, ...newState }; listeners.forEach((listener) => { listener(state); }); }; const useCustom = () => { const newListener = useState()[1]; useEffect(() => { listeners.push(newListener); }, []); return [state, setState]; }; export default useCustom; 

Verwendung in Komponenten


 import React from 'react'; import useCustom from './customHook'; const Counter = () => { const [globalState, setGlobalState] = useCustom(); const add1Global = () => { const newCounterValue = globalState.counter + 1; setGlobalState({ counter: newCounterValue }); }; return ( <div> <p> counter: {globalState.counter} </p> <button type="button" onClick={add1Global}> +1 to global </button> </div> ); }; export default Counter; 

Diese Version bietet bereits den Freigabestatus. Sie können Ihrer Anwendung eine beliebige Anzahl von Zählern hinzufügen, die alle einen gemeinsamen globalen Status haben.

Aber wir können es besser machen


Was willst du:

  • Entfernen Sie den Listener aus dem Array, wenn Sie die Bereitstellung der Komponente aufheben.
  • Machen Sie den Hook abstrakter, um ihn in anderen Projekten zu verwenden.
  • Verwalten von initialState mithilfe von Parametern.
  • Schreiben Sie den Haken in einem funktionaleren Stil um.

Aufrufen einer Funktion kurz vor dem Aufheben der Bereitstellung einer Komponente


Wir haben bereits herausgefunden, dass das Aufrufen von useEffect(function, []) mit einem leeren Array genauso funktioniert wie componentDidMount() . Wenn die im ersten Parameter übergebene Funktion jedoch eine andere Funktion zurückgibt, wird die zweite Funktion unmittelbar vor dem Aufheben der Bereitstellung der Komponente aufgerufen. Genau wie componentWillUnmount() .

Im Code der zweiten Funktion können Sie also die Logik zum Entfernen einer Komponente aus einem Array von Listenern schreiben.

 const useCustom = () => { const newListener = useState()[1]; useEffect(() => { //     listeners.push(newListener); return () => { //     listeners = listeners.filter(listener => listener !== newListener); }; }, []); return [state, setState]; }; 

Zweite Version


Zusätzlich zu diesem Update planen wir auch:

  • Übergeben Sie den React-Parameter und entfernen Sie den Import.
  • exportiere nicht customHook, sondern eine Funktion, die customHook mit dem angegebenen initalState ;
  • Erstellen Sie ein store , das den setState() und die Funktion setState() .
  • Ersetzen Sie die setState() durch die üblichen in setState() und useCustom() damit Sie den store verknüpfen können.

 function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { //     this.listeners.push(newListener); return () => { //     this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.setState]; } const useGlobalHook = (React, initialState) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); return useCustom.bind(store, React); }; export default useGlobalHook; 

Trennen Sie Aktionen von Komponenten


Wenn Sie jemals mit komplexen Statusverwaltungsbibliotheken gearbeitet haben, wissen Sie, dass die Manipulation eines globalen Status aus Komponenten keine gute Idee ist.

Es wäre richtiger, die Geschäftslogik zu trennen, indem Aktionen zum Ändern des Status erstellt werden. Daher möchte ich, dass die neueste Version des Pakets den Komponenten Zugriff nicht auf setState() , sondern auf eine Reihe von Aktionen setState() .

Dazu liefern wir unseren useGlobalHook(React, initialState, actions) dritten Argument. Ich möchte nur ein paar Kommentare hinzufügen.

  • Aktionen haben Zugriff auf das store . Auf diese Weise können Aktionen den Inhalt von store.state lesen, den store.setState() Aufrufen von store.setState() aktualisieren und sogar andere store.actions durch store.actions von store.actions .
  • Um Unordnung zu vermeiden, kann das Aktionsobjekt Unterobjekte enthalten. Auf diese Weise können Sie actions.addToCounter(amount ) mit allen Gegenaktionen an ein Unterobjekt übertragen: actions.counter.add(amount) .

Endgültige Version


Das folgende Snippet ist die aktuelle Version des NPM-Pakets use-global-hook .

 function setState(newState) { this.state = { ...this.state, ...newState }; this.listeners.forEach((listener) => { listener(this.state); }); } function useCustom(React) { const newListener = React.useState()[1]; React.useEffect(() => { this.listeners.push(newListener); return () => { this.listeners = this.listeners.filter(listener => listener !== newListener); }; }, []); return [this.state, this.actions]; } function associateActions(store, actions) { const associatedActions = {}; Object.keys(actions).forEach((key) => { if (typeof actions[key] === 'function') { associatedActions[key] = actions[key].bind(null, store); } if (typeof actions[key] === 'object') { associatedActions[key] = associateActions(store, actions[key]); } }); return associatedActions; } const useGlobalHook = (React, initialState, actions) => { const store = { state: initialState, listeners: [] }; store.setState = setState.bind(store); store.actions = associateActions(store, actions); return useCustom.bind(store, React); }; export default useGlobalHook; 

Anwendungsbeispiele


Sie müssen sich nicht mehr mit useGlobalHook.js befassen. Jetzt können Sie sich auf Ihre Anwendung konzentrieren. Im Folgenden finden Sie zwei Beispiele für die Verwendung des Pakets.

Mehrere Zähler, ein Wert


Fügen Sie so viele Zähler hinzu, wie Sie möchten: Sie haben alle einen globalen Wert. Jedes Mal, wenn einer der Zähler den globalen Status erhöht, werden alle anderen neu gezeichnet. In diesem Fall muss die übergeordnete Komponente nicht neu gezeichnet werden.
Lebendes Beispiel .

Asynchrone Ajax-Anforderungen


Durchsuchen Sie GitHub-Repositorys nach Benutzernamen. Wir verarbeiten Ajax-Anfragen asynchron mit async / await. Wir aktualisieren den Abfragezähler bei jeder neuen Suche.
Lebendes Beispiel .

Nun, das ist alles


Wir haben jetzt unsere eigene State-Management-Bibliothek für React Hooks.

Übersetzerkommentar


Die meisten vorhandenen Lösungen sind im Wesentlichen separate Bibliotheken. In diesem Sinne ist der vom Autor beschriebene Ansatz insofern interessant, als er nur die integrierten React-Funktionen verwendet. Darüber hinaus reduziert dieser Ansatz im Vergleich zu derselben Context-API, die ebenfalls sofort verfügbar ist, die Anzahl unnötiger Neuzeichnungen und gewinnt somit an Leistung.

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


All Articles