Ersetzen von Redux durch Observables und React Hooks

State Management ist eine der wichtigsten Aufgaben, die bei der Entwicklung von React gelöst wurden. Viele Tools wurden entwickelt, um Entwicklern bei der Lösung dieses Problems zu helfen. Das beliebteste Tool ist Redux, eine kleine Bibliothek, die von Dan Abramov erstellt wurde, um Entwicklern dabei zu helfen, das Flux-Entwurfsmuster in ihren Anwendungen zu verwenden. In diesem Artikel schauen wir uns an, ob wir Redux wirklich brauchen und wie wir es durch einen einfacheren Ansatz ersetzen können, der auf Observable und React Hooks basiert.


Warum brauchen wir überhaupt Redux?


Redux wird so oft mit React in Verbindung gebracht, dass viele Entwickler es verwenden, ohne darüber nachzudenken, warum sie Redux benötigen. Mit React ist es einfach, eine Komponente und ihren Status mit setState() / useState() zu synchronisieren. Aber alles wird komplizierter, sobald der Zustand von mehreren Komponenten gleichzeitig genutzt wird. Die naheliegendste Lösung für die gemeinsame Nutzung eines Status zwischen mehreren Komponenten besteht darin, ihn (Status) auf den gemeinsamen übergeordneten Status zu verschieben. Eine solche direkte Lösung kann jedoch schnell zu Problemen führen: Wenn die Komponenten in der Komponentenhierarchie weit voneinander entfernt sind, erfordert die Aktualisierung des allgemeinen Status ein umfangreiches Scrollen durch die Eigenschaften der Komponenten. React Context kann dazu beitragen, die Anzahl der ausgelaufenen Inhalte zu verringern. Das Deklarieren eines neuen Kontexts bei jeder Verwendung eines Status in Verbindung mit einer anderen Komponente ist jedoch aufwändiger und kann letztendlich zu Fehlern führen.


Redux löst diese Probleme, indem es ein Store Objekt einführt, das den gesamten Status der Anwendung enthält. In Komponenten, die Zugriff auf den Status benötigen, wird dieser Store mithilfe der connect implementiert. Diese Funktion stellt auch sicher, dass beim Ändern eines Status alle davon abhängigen Komponenten neu gezeichnet werden. Um den Status zu ändern, müssen Komponenten schließlich Aktionen senden, die die Reduzierung auslösen, um den neuen geänderten Status zu berechnen.



Als ich zum ersten Mal die Konzepte von Redux verstand


Was ist los mit Redux?


Als ich das offizielle Redux-Tutorial zum ersten Mal las, war ich am meisten beeindruckt von der großen Menge an Code, die ich schreiben musste, um den Status zu ändern. Das Ändern des Status erfordert das Deklarieren einer neuen Aktion, das Implementieren des entsprechenden Reduzierers und das endgültige Senden der Aktion. Redux empfiehlt auch das Schreiben eines Aktionserstellers , um das Erstellen einer Aktion bei jeder Übermittlung zu vereinfachen.


Mit all diesen Schritten erschwert Redux das Codeverständnis, das Refactoring und das Debuggen. Beim Lesen von Code, der von einer anderen Person geschrieben wurde, ist es oft schwierig, herauszufinden, was passiert, wenn die Aktion gesendet wird. Zuerst müssen wir uns mit dem Code des Aktionserstellers befassen, um den entsprechenden Aktionstyp zu finden, und dann die Reduzierungen, die diesen Aktionstyp handhaben. Die Dinge können noch komplizierter werden, wenn einige Middlewares wie Redux-Saga verwendet werden, wodurch der Lösungskontext noch impliziter wird.


Und schließlich kann Redux bei der Verwendung von TypeScript enttäuschend sein. Aktionen sind konstruktionsbedingt einfach Zeichenfolgen, die mit zusätzlichen Parametern verknüpft sind. Es gibt Möglichkeiten, gut typisierten Redux-Code mit TypeScript zu schreiben, aber dies kann sehr mühsam sein und wiederum zu einer Erhöhung der zu schreibenden Codemenge führen.



Der Nervenkitzel beim Lernen von Code, der mit Redux geschrieben wurde


Observable and Hook: Ein einfacher Ansatz zur Verwaltung des Staates.


Ersetzen Sie Store durch Observable


Um das Problem der Statusfreigabe auf einfachere Weise zu lösen, müssen wir zunächst eine Möglichkeit finden, Komponenten zu benachrichtigen, wenn andere Komponenten ihren allgemeinen Status ändern. Dazu erstellen wir eine Observable Klasse, die einen einzelnen Wert enthält und die es Komponenten ermöglicht, Änderungen an diesem Wert zu abonnieren. Die Abonnementmethode gibt die Funktion zurück, die aufgerufen werden muss, um das Abonnement für Observable .


Unter TypeScript ist die Implementierung einer solchen Klasse recht einfach:


 type Listener<T> = (val: T) => void; type Unsubscriber = () => void; export class Observable<T> { private _listeners: Listener<T>[]; constructor(private _val: T) {} get(): T { return this._val; } set(val: T) { if (this._val !== val) { this._val = val; this._listeners.forEach(l => l(val)); } } subscribe(listener: Listener<T>): Unsubscriber { this._listeners.push(listener); return () => { this._listeners = this._listeners.filter(l => l !== listener); }; } } 

Wenn Sie diese Klasse mit dem Redux Store , werden Sie getState() , dass sie sich sehr ähnlich sind: get() entspricht getState() und subscribe() ist dasselbe. Der Hauptunterschied ist die Methode dispatch() , die durch die einfachere Methode set() , mit der Sie den darin enthaltenen Wert ändern können, ohne auf die Reduzierung angewiesen zu sein. Ein weiterer wesentlicher Unterschied ist, dass wir im Gegensatz zu Redux viel Observable anstelle eines einzelnen Store , der den gesamten Status enthält.


Reduzierungsservices ersetzen


Jetzt kann Observable verwendet werden, um den allgemeinen Zustand zu speichern, aber wir müssen noch die im Reduzierer enthaltene Logik verschieben. Dafür verwenden wir das Konzept der Dienstleistungen. Services sind Klassen, die die gesamte Geschäftslogik unserer Anwendungen implementieren. Versuchen wir, den Reducer Todo aus dem Redux-Tutorial mit Observable in den Todo Service umzuschreiben:


 import { Observable } from "./observable"; export interface Todo { readonly text: string; readonly completed: boolean; } export enum VisibilityFilter { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE, } export class TodoService { readonly todos = new Observable<Todo[]>([]); readonly visibilityFilter = new Observable(VisibilityFilter.SHOW_ALL); addTodo(text: string) { this.todos.set([...this.todos.get(), { text, completed: false }]); } toggleTodo(index: number) { this.todos.set(this.todos.get().map( (todo, i) => (i === index ? { text: todo.text, completed: !todo.completed } : todo) )); } setVisibilityFilter(filter: VisibilityFilter) { this.visibilityFilter.set(filter); } } 

Vergleicht man dies mit dem Reduzierer Todo , können wir die folgenden Unterschiede feststellen:


  • Aktionen wurden durch Methoden ersetzt, sodass kein Aktionstyp, keine Aktion selbst und kein Aktionsersteller deklariert werden müssen.
  • Sie müssen keinen großen Schalter mehr schreiben, um zwischen den Aktionstypen zu wechseln. Dafür sorgt der dynamische Javascript-Versand (d. H. Methodenaufrufe).
  • Und am wichtigsten ist, dass der Service den von ihm verwalteten Status enthält und ändert. Dies ist ein großer konzeptioneller Unterschied zu Reduzierern, die reine Funktionen sind.

Zugang zu Diensten und von Komponenten aus beobachtbar


Nachdem wir "Store and Reducer from Redux" durch "Observable and Services" ersetzt haben, müssen wir die Services für alle React-Komponenten verfügbar machen. Hierfür gibt es mehrere Möglichkeiten: Wir könnten das IoC-Framework verwenden, z. B. Inversify. Verwenden Sie den Kontext Reagieren oder verwenden Sie denselben Ansatz wie in Store Redux - eine globale Instanz für jeden Dienst. In diesem Artikel betrachten wir den letzten Ansatz:


 import { TodoService } from "./todoService"; export const todoService = new TodoService(); 

Jetzt können wir auf den freigegebenen Status zugreifen und ihn in allen React-Komponenten ändern, indem wir eine Instanz von todoService importieren. Wir müssen jedoch noch einen Weg finden, um unsere Komponenten neu zu zeichnen, wenn der allgemeine Status durch eine andere Komponente geändert wird. Dazu schreiben wir einen einfachen Hook, der der Komponente eine Statusvariable hinzufügt, die Observable abonniert und die Statusvariable aktualisiert, wenn sich der Observable Wert ändert:


 import { useEffect, useState } from "react"; import { Observable } from "./observable"; export function useObservable<T>(observable: Observable<T>): T { const [val, setVal] = useState(observable.get()); useEffect(() => { setVal(observable.get()); //   @mayorovp return observable.subscribe(setVal); }, [observable]); return val; } 

Alles zusammen


Unser Toolkit ist fertig. Wir können Observable , um den allgemeinen Status in Diensten zu speichern, und useObservable um sicherzustellen, dass Komponenten immer mit diesem Status synchron sind.


Lassen Sie uns die TodoList-Komponente aus dem Redux-Tutorial mit dem neuen Hook umschreiben:


 import React from "react"; import { useObservable } from "./observableHook"; import { todoService } from "./services"; import { Todo, VisibilityFilter } from "./todoService"; export const TodoList = () => { const todos = useObservable(todoService.todos); const filter = useObservable(todoService.visibilityFilter); const visibleTodos = getVisibleTodos(todos, filter); return ( <div> <ul> {visibleTodos.map((todo, index) => ( <TodoItem key={index} todo={todo} index={index} /> ))} </ul> <p> Show: <FilterLink filter={VisibilityFilter.SHOW_ALL}>All</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ACTIVE}>Active</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ALL}>Completed</FilterLink> </p> </div> ); }; const TodoItem = ({ todo: { text, completed }, index }: { todo: Todo; index: number }) => { return ( <li style={{ textDecoration: completed ? "line-through" : "none", }} onClick={() => todoService.toggleTodo(index)} > {text} </li> ); }; const FilterLink = ({ filter, children }: { filter: VisibilityFilter; children: React.ReactNode }) => { const activeFilter = useObservable(todoService.visibilityFilter); const active = filter === activeFilter; return active ? ( <span>{children}</span> ) : ( <a href="" onClick={() => todoService.setVisibilityFilter(filter)}> {children} </a> ); }; function getVisibleTodos(todos: Todo[], filter: VisibilityFilter): Todo[] { switch (filter) { case VisibilityFilter.SHOW_ALL: return todos; case VisibilityFilter.SHOW_COMPLETED: return todos.filter(t => t.completed); case VisibilityFilter.SHOW_ACTIVE: return todos.filter(t => !t.completed); } } 

Wie wir sehen können, haben wir mehrere Komponenten geschrieben, die sich auf allgemeine todos beziehen ( todos und visibilityFilter ). Diese Werte werden einfach durch Aufrufen von Methoden aus todoService . Dank des Hooks useObservable, der Wertänderungen abonniert, werden diese Komponenten automatisch neu gezeichnet, wenn sich der allgemeine Status ändert.


Fazit


Wenn wir diesen Code mit dem Redux-Ansatz vergleichen, werden wir mehrere Vorteile sehen:


  • Prägnanz: Das Einzige, was wir tun mussten, war, die useObservable in Observable useObservable und den Hook useObservable wenn auf diese Werte von den Komponenten aus useObservable . Es ist nicht erforderlich, eine Aktion zu deklarieren, eine Aktion zu mapStateToProps , eine Reduzierung zu schreiben oder zu kombinieren oder unsere Komponenten mit den mapDispatchToProps mapStateToProps und mapDispatchToProps mit dem mapStateToProps mapDispatchToProps .
  • Einfachheit: Jetzt ist es viel einfacher, die Codeausführung zu verfolgen. Um zu verstehen, was beim Drücken einer Taste tatsächlich passiert, müssen Sie lediglich zur Implementierung der aufgerufenen Methode wechseln. Die schrittweise Ausführung mit dem Debugger wird ebenfalls erheblich verbessert, da keine Zwischenstufe zwischen unseren Komponenten und unseren Services besteht.
  • Typensicherheit ab Werk : TypeScript-Entwickler benötigen keine zusätzliche Arbeit, um den Code korrekt eingeben zu können. Es ist nicht erforderlich, Typen für den Status und für jede Aktion zu deklarieren.
  • Unterstützung für async / await: Obwohl dies hier nicht demonstriert wurde, eignet sich diese Lösung hervorragend für asynchrone Funktionen und vereinfacht die asynchrone Programmierung erheblich. Sie müssen sich nicht auf Middleware wie Redux-Thunk verlassen, für deren Verständnis fundierte Kenntnisse der funktionalen Programmierung erforderlich sind.

Natürlich bietet Redux immer noch einige wichtige Vorteile, insbesondere Redux DevTools, mit denen Entwickler Statusänderungen während der Entwicklung überwachen und rechtzeitig zu früheren Anwendungsstatus wechseln können, was ein großartiges Debugging-Tool sein kann. Aber meiner Erfahrung nach habe ich das selten genutzt, und der zu zahlende Preis scheint zu hoch für einen kleinen Gewinn.


In all unseren React- und React Native-Anwendungen haben wir erfolgreich einen Ansatz verwendet, der dem in diesem Artikel beschriebenen ähnelt. Tatsächlich hatten wir nie das Bedürfnis nach einem komplexeren Staatsverwaltungssystem als diesem.


Hinweise


Die in diesem Beitrag vorgestellte Observable Klasse ist ziemlich einfach. Es kann durch fortgeschrittenere Implementierungen wie Micro Observables (unsere eigene Bibliothek) oder RxJS ersetzt werden.


Die hier vorgestellte Lösung ähnelt der, die mit MobX erreicht werden kann. Der Hauptunterschied besteht darin, dass MobX eine tiefgreifende Veränderbarkeit des Zustands von Objekten unterstützt. Es ist auch darauf angewiesen, dass ES6-Proxys Änderungen melden, implizit neu rendern und das Debuggen erschweren, wenn die Dinge nicht wie erwartet funktionieren. Darüber hinaus funktioniert MobX nicht gut mit asynchronen Funktionen.


Von einem Übersetzer: Diese Publikation, die als Einführung in die Zustandsverwaltung mit Observable betrachtet werden kann, ist eine Fortsetzung des Themas, das im Artikel Verwalten des Anwendungszustands mit RxJS / Immer als einfache Alternative zu Redux / MobX behandelt wird , in dem beschrieben wird, wie die Verwendung dieses Ansatzes vereinfacht wird.

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


All Articles