Sustituci贸n de Redux con observables y ganchos de reacci贸n

La gesti贸n del estado es una de las tareas m谩s importantes resueltas en el desarrollo de React. Se han creado muchas herramientas para ayudar a los desarrolladores a resolver este problema. La herramienta m谩s popular es Redux, una peque帽a biblioteca creada por Dan Abramov para ayudar a los desarrolladores a usar el patr贸n de dise帽o Flux en sus aplicaciones. En este art铆culo, analizaremos si realmente necesitamos Redux y veremos c贸mo podemos reemplazarlo con un enfoque m谩s simple basado en Ganchos Observables y React.


驴Por qu茅 necesitamos Redux?


Redux se asocia tan a menudo con React que muchos desarrolladores lo usan sin pensar por qu茅 necesitan Redux. React facilita la sincronizaci贸n de un componente y su estado con setState() / useState() . Pero todo se vuelve m谩s complicado tan pronto como el estado comienza a ser utilizado por varios componentes a la vez. La soluci贸n m谩s obvia para compartir un estado com煤n entre varios componentes es moverlo (estado) a su padre com煤n. Pero tal soluci贸n "frontal" puede conducir r谩pidamente a dificultades: si los componentes est谩n lejos unos de otros en la jerarqu铆a de componentes, la actualizaci贸n del estado general requerir谩 mucho desplazamiento a trav茅s de las propiedades de los componentes. Reaccionar contexto puede ayudar a reducir la cantidad de derrames, pero declarar un nuevo contexto cada vez que un estado comienza a usarse junto con otro componente requerir谩 m谩s esfuerzo y, en 煤ltima instancia, puede conducir a errores.


Redux resuelve estos problemas al introducir un objeto Store que contiene todo el estado de la aplicaci贸n. En los componentes que requieren acceso al estado, esta Store se implementa mediante la funci贸n de connect . Esta funci贸n tambi茅n asegura que cuando un estado cambia, todos los componentes que dependen de 茅l ser谩n redibujados. Finalmente, para cambiar el estado, los componentes deben enviar acciones que activen el reductor para calcular el nuevo estado modificado.



Cuando entend铆 por primera vez los conceptos de Redux


驴Qu茅 le pasa a Redux?


La primera vez que le铆 el tutorial oficial de Redux , me sorprendi贸 la gran cantidad de c贸digo que ten铆a que escribir para cambiar de estado. Cambiar el estado requiere declarar una nueva acci贸n, implementar el reductor correspondiente y finalmente enviar la acci贸n. Redux tambi茅n alienta la escritura de un creador de acciones para facilitar la creaci贸n de una acci贸n cada vez que desee enviarla.


Con todos estos pasos, Redux complica la comprensi贸n del c贸digo, la refactorizaci贸n y la depuraci贸n. Al leer el c贸digo escrito por otra persona, a menudo es dif铆cil darse cuenta de lo que sucede cuando se presenta la acci贸n. Primero, tendremos que sumergirnos en el c贸digo del creador de la acci贸n para encontrar el tipo de acci贸n apropiado, y luego encontrar los reductores que manejan este tipo de acci贸n. Las cosas pueden volverse a煤n m谩s complicadas si se usan algunos middlewares, como redux-saga, que hace que el contexto de la soluci贸n sea a煤n m谩s impl铆cito.


Y finalmente, cuando se usa TypeScript, Redux puede ser decepcionante. Por dise帽o, las acciones son simplemente cadenas asociadas con par谩metros adicionales. Hay formas de escribir c贸digo Redux bien tipeado usando TypeScript, pero puede ser muy tedioso y nuevamente puede conducir a un aumento en la cantidad de c贸digo que tenemos que escribir.



La emoci贸n de aprender c贸digo escrito con Redux


Observable y hook: un enfoque simple para administrar el estado.


Reemplazar tienda con observable


Para resolver el problema de compartir el estado de una manera m谩s simple, primero necesitamos encontrar una manera de notificar a los componentes cuando otros componentes cambian su estado general. Para hacer esto, Observable una clase Observable que contenga un solo valor y que permita a los componentes suscribirse a los cambios en este valor. El m茅todo de suscripci贸n devolver谩 la funci贸n que debe llamarse para darse de baja del Observable .


En TypeScript, implementar tal clase es bastante 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 compara esta clase con Redux Store , ver谩 que son bastante similares: get() corresponde a getState() , y subscribe() es lo mismo. La principal diferencia es el m茅todo dispatch() , que ha sido reemplazado por el m茅todo set() m谩s simple, que le permite cambiar el valor contenido en 茅l sin tener que depender del reductor. Otra diferencia significativa es que, en contraste con Redux, usaremos muchos Observable lugar de una sola Store contenga todo el estado.


Reemplazar servicios reductores


Ahora Observable se puede usar para almacenar el estado general, pero a煤n necesitamos mover la l贸gica contenida en el reductor. Para esto utilizamos el concepto de servicios. Los servicios son clases que implementan toda la l贸gica de negocios de nuestras aplicaciones. Intentemos reescribir el reductor Todo del tutorial de Redux al servicio Todo usando 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); } } 

Comparando esto con el reductor Todo , podemos notar las siguientes diferencias:


  • Las acciones fueron reemplazadas por m茅todos, eliminando la necesidad de declarar un tipo de acci贸n, la acci贸n misma y el creador de la acci贸n.
  • Ya no necesita escribir un interruptor grande para enrutar entre el tipo de acci贸n. El despacho din谩mico de Javascript (es decir, llamadas a m茅todos) se encarga de esto.
  • Y lo m谩s importante, el servicio contiene y cambia el estado que administra. Esta es una gran diferencia conceptual de los reductores, que son funciones puras.

Acceso a servicios y observable desde componentes.


Ahora que hemos reemplazado "tienda y reductor de Redux" por "Observable y servicios", necesitamos que los servicios est茅n disponibles en todos los componentes de React. Hay varias formas de hacer esto: podr铆amos usar el marco de IoC, por ejemplo, Inversify; use el contexto React o use el mismo enfoque que en Store Redux: una instancia global para cada servicio. En este art铆culo, consideraremos el 煤ltimo enfoque:


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

Ahora podemos acceder al estado compartido y cambiarlo desde todos nuestros componentes React importando una instancia de todoService . Pero a煤n necesitamos encontrar una manera de volver a dibujar nuestros componentes cuando otro estado cambia el estado general. Para hacer esto, escribiremos un enlace simple que agregue una variable de estado al componente, se suscriba al Observable y actualice la variable de estado cuando cambie el valor del Observable :


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

Poniendo todo junto


Nuestro kit de herramientas est谩 listo. Podemos usar Observable para almacenar el estado general en los servicios y usar useObservable para asegurar que los componentes siempre est茅n sincronizados con este estado.


Reescribamos el componente TodoList del tutorial de Redux usando el nuevo gancho:


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

Como podemos ver, hemos escrito varios componentes que se refieren a valores de estado generales ( todos y visibilityFilter ). Estos valores simplemente se cambian llamando a m茅todos desde todoService . Gracias al hook useObservable, que se suscribe a los cambios de valor, estos componentes se vuelven a dibujar autom谩ticamente cuando cambia el estado general.


Conclusi贸n


Si comparamos este c贸digo con el enfoque de Redux, veremos varias ventajas:


  • Concisi贸n: lo 煤nico que ten铆amos que hacer era ajustar los valores de estado en el Observable y usar hook useObservable al acceder a estos valores desde los componentes. No es necesario declarar acci贸n, creador de acci贸n, escribir o combinar reductor, o conectar nuestros componentes al repositorio con los mapDispatchToProps mapStateToProps y mapDispatchToProps .
  • Simplicidad: ahora es mucho m谩s f谩cil rastrear la ejecuci贸n del c贸digo. Comprender qu茅 sucede realmente cuando se presiona un bot贸n es solo cuesti贸n de cambiar a la implementaci贸n del m茅todo llamado. La ejecuci贸n paso a paso utilizando el depurador tambi茅n se mejora significativamente, ya que no hay un nivel intermedio entre nuestros componentes y nuestros servicios.
  • Tipo de seguridad fuera de la caja: los desarrolladores de TypeScript no necesitar谩n trabajo adicional para tener el c贸digo escrito correctamente. No es necesario declarar tipos para el estado y para cada acci贸n.
  • Soporte para async / await: aunque esto no se ha demostrado aqu铆, esta soluci贸n funciona muy bien con funciones asincr贸nicas, lo que simplifica enormemente la programaci贸n asincr贸nica. No es necesario confiar en el middleware, como redux-thunk, que requiere un profundo conocimiento de la programaci贸n funcional para comprender.

Redux, por supuesto, todav铆a tiene algunas ventajas serias, especialmente Redux DevTools, que permiten a los desarrolladores monitorear los cambios de estado durante el desarrollo y pasar a tiempo a estados de aplicaciones anteriores, lo que puede ser una gran herramienta para la depuraci贸n. Pero en mi experiencia, rara vez us茅 esto, y el precio a pagar parece demasiado alto para una peque帽a ganancia.


En todas nuestras aplicaciones React y React Native, hemos utilizado con 茅xito un enfoque similar al descrito en este art铆culo. De hecho, nunca sentimos la necesidad de un sistema de gesti贸n estatal m谩s complejo que este.


Notas


La clase Observable presentada en esta publicaci贸n es bastante simple. Se puede reemplazar con implementaciones m谩s avanzadas como micro-observables (nuestra propia biblioteca) o RxJS.


La soluci贸n presentada aqu铆 es muy similar a lo que se puede lograr con MobX. La principal diferencia es que MobX admite una profunda mutabilidad del estado de los objetos. Tambi茅n se basa en los proxies ES6 para notificar los cambios, volver a representarlos de forma impl铆cita y complicar la depuraci贸n cuando las cosas no funcionan como se esperaba. Adem谩s, MobX no funciona bien con funciones asincr贸nicas.


De un traductor: esta publicaci贸n, que puede verse como una introducci贸n a la gesti贸n del estado utilizando Observable, es una continuaci贸n del tema cubierto en el art铆culo Gesti贸n del estado de la aplicaci贸n con RxJS / Immer como una alternativa simple a Redux / MobX , que describe c贸mo simplificar el uso de este enfoque.

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


All Articles