Remplacement de Redux par des observables et des crochets React

La gestion des états est l'une des tâches les plus importantes résolues dans le développement sur React. De nombreux outils ont été créés pour aider les développeurs à résoudre ce problème. L'outil le plus populaire est Redux, une petite bibliothèque créée par Dan Abramov pour aider les développeurs à utiliser le modèle de conception Flux dans leurs applications. Dans cet article, nous verrons si nous avons vraiment besoin de Redux et verrons comment nous pouvons le remplacer par une approche plus simple basée sur Observable et React Hooks.


Pourquoi avons-nous besoin de Redux?


Redux est si souvent associé à React que de nombreux développeurs l'utilisent sans se demander pourquoi ils ont besoin de Redux. React facilite la synchronisation d'un composant et de son état avec setState() / useState() . Mais tout devient plus compliqué dès que l'état commence à être utilisé par plusieurs composants à la fois. La solution la plus évidente pour partager un état commun entre plusieurs composants est de le déplacer (état) vers leur parent commun. Mais une telle solution «frontale» peut rapidement entraîner des difficultés: si les composants sont éloignés les uns des autres dans la hiérarchie des composants, la mise à jour de l'état général nécessitera beaucoup de défilement des propriétés des composants. React Context peut aider à réduire le nombre de déversements, mais déclarer un nouveau contexte chaque fois qu'un état commence à être utilisé en conjonction avec un autre composant nécessitera plus d'efforts et peut finalement conduire à des erreurs.


Redux résout ces problèmes en introduisant un objet Store qui contient tout l'état de l'application. Dans les composants qui nécessitent un accès à l'état, ce Store est implémenté à l'aide de la fonction de connect . Cette fonction garantit également que lorsqu'un état change, tous les composants qui en dépendent seront redessinés. Enfin, pour changer d'état, les composants doivent envoyer des actions qui déclenchent le réducteur pour calculer le nouvel état modifié.



Quand j'ai compris les concepts de Redux pour la première fois


Quel est le problème avec Redux?


La première fois que j'ai lu le tutoriel officiel de Redux , j'ai été le plus frappé par la grande quantité de code que j'ai dû écrire pour changer d'état. Changer l'état nécessite de déclarer une nouvelle action, d'implémenter le réducteur correspondant et enfin d'envoyer l'action. Redux encourage également l' écriture d'un créateur d'action pour faciliter la création d'une action chaque fois que vous souhaitez la soumettre.


Avec toutes ces étapes, Redux complique la compréhension, la refactorisation et le débogage du code. Lors de la lecture de code écrit par quelqu'un d'autre, il est souvent difficile de comprendre ce qui se passe lorsque l'action est soumise. Tout d'abord, nous devrons plonger dans le code du créateur d'action pour trouver le type d'action approprié, puis trouver les réducteurs qui gèrent ce type d'action. Les choses peuvent devenir encore plus compliquées si certains middlewares sont utilisés, comme redux-saga, ce qui rend le contexte de la solution encore plus implicite.


Et enfin, lors de l'utilisation de TypeScript, Redux peut être décevant. De par leur conception, les actions sont simplement des chaînes associées à des paramètres supplémentaires. Il existe des moyens d'écrire du code Redux bien typé à l'aide de TypeScript, mais cela peut être très fastidieux et peut encore entraîner une augmentation de la quantité de code que nous devons écrire.



Le plaisir d'apprendre du code écrit avec Redux


Observable et hook: une approche simple pour gérer l'état.


Remplacez Store par Observable


Pour résoudre le problème du partage d'état d'une manière plus simple, nous devons d'abord trouver un moyen de notifier les composants lorsque d'autres composants changent leur état général. Pour ce faire, créons une classe Observable qui contient une seule valeur et qui permet aux composants de s'abonner aux modifications de cette valeur. La méthode d'abonnement renverra la fonction qui doit être appelée pour se désinscrire de l' Observable .


Sur TypeScript, l'implémentation d'une telle classe est assez simple:


 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); }; } } 

Si vous comparez cette classe avec le Redux Store , vous verrez qu'elles sont assez similaires: get() correspond à getState() , et subscribe() est le même. La principale différence est la méthode dispatch() , qui a été remplacée par la méthode set() simple, qui vous permet de modifier la valeur qu'elle contient sans avoir à compter sur le réducteur. Une autre différence importante est que, contrairement à Redux, nous utiliserons beaucoup d' Observable au lieu d'un seul Store contenant tout l'état.


Remplacer les services de réduction


Maintenant, Observable peut être utilisé pour stocker l'état général, mais nous devons encore déplacer la logique contenue dans le réducteur. Pour cela, nous utilisons le concept de services. Les services sont des classes qui implémentent toute la logique métier de nos applications. Essayons de réécrire le réducteur Todo du tutoriel Redux au service Todo en utilisant Observable :


 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); } } 

En comparant cela avec le réducteur Todo , nous pouvons noter les différences suivantes:


  • Les actions ont été remplacées par des méthodes, éliminant ainsi la nécessité de déclarer un type d'action, l'action elle-même et le créateur de l'action.
  • Vous n'avez plus besoin d'écrire un grand commutateur pour router entre le type d'action. La répartition Javascript dynamique (c'est-à-dire les appels de méthode) s'en charge.
  • Et surtout, le service contient et modifie l'état qu'il gère. Il s'agit d'une grande différence conceptuelle par rapport aux réducteurs, qui sont de simples fonctions.

Accès aux services et observable depuis les composants


Maintenant que nous avons remplacé «magasin et réducteur de Redux» par «Observable et services», nous devons rendre les services disponibles à partir de tous les composants React. Il existe plusieurs façons de procéder: nous pourrions utiliser le cadre IoC, par exemple, Inversify; utilisez le contexte React ou utilisez la même approche que dans Store Redux - une instance globale pour chaque service. Dans cet article, nous considérerons la dernière approche:


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

Nous pouvons maintenant accéder à l'état partagé et le modifier à partir de tous nos composants React en important une instance de todoService . Mais nous devons encore trouver un moyen de redessiner nos composants lorsque l'état général est modifié par un autre composant. Pour ce faire, nous allons écrire un simple hook qui ajoute une variable d'état au composant, s'abonne à l' Observable et met à jour la variable d'état lorsque la valeur Observable change:


 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; } 

Tout mettre ensemble


Notre boîte à outils est prête. Nous pouvons utiliser Observable pour stocker l'état général dans les services et utiliser useObservable pour nous assurer que les composants seront toujours synchronisés avec cet état.


Réécrivons le composant TodoList du didacticiel Redux en utilisant le nouveau hook:


 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); } } 

Comme nous pouvons le voir, nous avons écrit plusieurs composants qui font référence à des valeurs d'état général ( todos et visibilityFilter ). Ces valeurs sont simplement modifiées en appelant des méthodes de todoService . Grâce au hook useObservable, qui souscrit aux modifications de valeur, ces composants sont automatiquement redessinés lorsque l'état général change.


Conclusion


Si nous comparons ce code avec l'approche Redux, nous verrons plusieurs avantages:


  • Concision: la seule chose que nous devions faire était d'encapsuler les valeurs d'état dans Observable et d'utiliser hook useObservable lors de l'accès à ces valeurs à partir des composants. Il n'est pas nécessaire de déclarer une action, un créateur d'action, d'écrire ou de combiner un réducteur, ou de connecter nos composants au référentiel avec les mapDispatchToProps mapStateToProps et mapDispatchToProps .
  • Simplicité: il est désormais beaucoup plus facile de suivre l'exécution du code. Comprendre ce qui se passe réellement quand un bouton est enfoncé n'est qu'une question de basculer vers l'implémentation de la méthode appelée. L'exécution étape par étape à l'aide du débogueur est également considérablement améliorée, car il n'y a pas de niveau intermédiaire entre nos composants et nos services.
  • Sécurité de type prête à l'emploi : les développeurs TypeScript n'auront pas besoin de travail supplémentaire pour avoir du code correctement tapé. Il n'est pas nécessaire de déclarer des types pour l'état et pour chaque action.
  • Prise en charge asynchrone / wait: bien que cela n'ait pas été démontré ici, cette solution fonctionne très bien avec les fonctions asynchrones, simplifiant considérablement la programmation asynchrone. Pas besoin de s'appuyer sur un middleware, tel que redux-thunk, qui nécessite une connaissance approfondie de la programmation fonctionnelle pour comprendre.

Redux, bien sûr, a encore de sérieux avantages, en particulier Redux DevTools, qui permet aux développeurs de surveiller les changements d'état pendant le développement et de passer dans le temps aux états d'application précédents, ce qui peut être un excellent outil de débogage. Mais d'après mon expérience, je l'ai rarement utilisé, et le prix à payer semble trop élevé pour un petit gain.


Dans toutes nos applications React et React Native, nous avons utilisé avec succès une approche similaire à celle décrite dans cet article. En fait, nous n'avons jamais ressenti le besoin d'un système de gestion d'État plus complexe que celui-ci.


Remarques


La classe Observable présentée dans ce post est assez simple. Il peut être remplacé par des implémentations plus avancées telles que les micro-observables (notre propre bibliothèque) ou RxJS.


La solution présentée ici est très similaire à ce qui peut être réalisé avec MobX. La principale différence est que MobX prend en charge une mutabilité profonde de l'état des objets. Il s'appuie également sur les proxys ES6 pour notifier les modifications, implicitement re-rendu et compliquer le débogage lorsque les choses ne fonctionnent pas comme prévu. De plus, MobX ne fonctionne pas bien avec les fonctions asynchrones.


D'après un traducteur: cette publication, qui peut être considérée comme une introduction à la gestion des états à l'aide d'Observable, est une continuation du sujet abordé dans l'article Gestion de l'état d'une application avec RxJS / Immer comme alternative simple à Redux / MobX , qui décrit comment simplifier l'utilisation de cette approche.

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


All Articles