Substituindo Redux por Observables e React Hooks

O gerenciamento de estado é uma das tarefas mais importantes resolvidas no desenvolvimento do React. Muitas ferramentas foram criadas para ajudar os desenvolvedores a resolver esse problema. A ferramenta mais popular é o Redux, uma pequena biblioteca criada por Dan Abramov para ajudar os desenvolvedores a usar o padrão de design do Flux em seus aplicativos. Neste artigo, examinaremos se realmente precisamos do Redux e veremos como podemos substituí-lo por uma abordagem mais simples, baseada nos ganchos Observable e React.


Por que precisamos do Redux?


O Redux é tão frequentemente associado ao React que muitos desenvolvedores o usam sem pensar no motivo pelo qual precisam do Redux. O React facilita a sincronização de um componente e seu estado com setState() / useState() . Mas tudo se torna mais complicado assim que o estado começa a ser usado por vários componentes ao mesmo tempo. A solução mais óbvia para compartilhar um estado comum entre vários componentes é movê-lo (estado) para seu pai comum. Porém, uma solução "frontal" pode levar rapidamente a dificuldades: se os componentes estiverem longe um do outro na hierarquia de componentes, a atualização do estado geral exigirá muita rolagem pelas propriedades dos componentes. O contexto de reação pode ajudar a reduzir o número de derramamentos, mas declarar um novo contexto toda vez que um estado começa a ser usado em conjunto com outro componente exigirá mais esforço e poderá levar a erros.


O Redux resolve esses problemas introduzindo um objeto Store que contém todo o estado do aplicativo. Nos componentes que requerem acesso ao estado, este Store é implementado usando a função de connect . Essa função também garante que, quando um estado for alterado, todos os componentes que dependem dele sejam redesenhados. Por fim, para alterar o estado, os componentes devem enviar ações que acionam o redutor para calcular o novo estado alterado.



Quando entendi pela primeira vez os conceitos de Redux


O que há de errado com o Redux?


A primeira vez que li o tutorial oficial do Redux , fiquei mais impressionado com a grande quantidade de código que precisei escrever para mudar de estado. Alterar o estado requer declarar uma nova ação, implementar o redutor correspondente e finalmente enviar a ação. O Redux também incentiva a criação de um criador de ações para facilitar a criação de uma ação sempre que você a enviar.


Com todas essas etapas, o Redux complica a compreensão, refatoração e depuração de código. Ao ler o código escrito por outra pessoa, geralmente é difícil descobrir o que acontece quando a ação é enviada. Primeiro, teremos que mergulhar no código do criador da ação para encontrar o tipo de ação apropriado e, em seguida, encontrar os redutores que lidam com esse tipo de ação. As coisas podem se tornar ainda mais complicadas se alguns middlewares forem usados, como o redux-saga, que torna o contexto da solução ainda mais implícito.


E, finalmente, ao usar o TypeScript, o Redux pode ser decepcionante. Por design, as ações são simplesmente cadeias associadas a parâmetros adicionais. Existem maneiras de escrever código Redux bem digitado usando o TypeScript, mas pode ser muito entediante e, novamente, pode levar a um aumento na quantidade de código que precisamos escrever.



A emoção de aprender código escrito com Redux


Observável e gancho: uma abordagem simples para gerenciar o estado.


Substituir loja por observável


Para resolver o problema do compartilhamento de estado de uma maneira mais simples, primeiro precisamos encontrar uma maneira de notificar os componentes quando outros componentes alteram seu estado geral. Para fazer isso, vamos criar uma classe Observable que contenha um único valor e que permita que os componentes se inscrevam nas alterações desse valor. O método de inscrição retornará a função que deve ser chamada para cancelar a assinatura do Observable .


No TypeScript, implementar essa classe é bastante simples:


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

Se você comparar essa classe com a Redux Store , verá que elas são bastante semelhantes: get() corresponde a getState() e subscribe() é o mesmo. A principal diferença é o método dispatch() , que foi substituído pelo método set() mais simples, que permite alterar o valor contido nele sem ter que confiar no redutor. Outra diferença significativa é que, ao contrário do Redux, usaremos muito Observable vez de um único Store contendo todo o estado.


Substituir serviços redutores


Agora Observable pode ser usado para armazenar estado geral, mas ainda precisamos mover a lógica contida no redutor. Para isso, usamos o conceito de serviços. Serviços são classes que implementam toda a lógica comercial de nossos aplicativos. Vamos tentar reescrever o redutor Todo do tutorial Redux para o serviço Todo usando o 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 isso com o redutor Todo , podemos observar as seguintes diferenças:


  • As ações foram substituídas por métodos, eliminando a necessidade de declarar um tipo de ação, a própria ação e o criador da ação.
  • Você não precisa mais escrever uma opção grande para rotear entre o tipo de ação. O envio dinâmico de Javascript (ou seja, chamadas de método) cuida disso.
  • E o mais importante, o serviço contém e altera o estado que gerencia. Essa é uma grande diferença conceitual dos redutores, que são funções puras.

Acesso a serviços e observável a partir de componentes


Agora que substituímos “armazenamento e redutor do Redux” por “Observable and services”, precisamos disponibilizar os serviços de todos os componentes do React. Existem várias maneiras de fazer isso: poderíamos usar a estrutura de IoC, por exemplo, Inversify; use o contexto React ou use a mesma abordagem que no Store Redux - uma instância global para cada serviço. Neste artigo, consideraremos a última abordagem:


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

Agora podemos acessar o estado compartilhado e alterá-lo de todos os nossos componentes React importando uma instância de todoService . Mas ainda precisamos encontrar uma maneira de redesenhar nossos componentes quando o estado geral for alterado por outro componente. Para fazer isso, escreveremos um gancho simples que adiciona uma variável de estado ao componente, assina o Observable e atualiza a variável de estado quando o valor do Observable é alterado:


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

Juntando tudo


Nosso kit de ferramentas está pronto. Podemos usar Observable para armazenar o estado geral em serviços e use useObservable para garantir que os componentes estejam sempre sincronizados com esse estado.


Vamos reescrever o componente TodoList no tutorial do Redux usando o novo 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, escrevemos vários componentes que se referem a valores de estado geral ( todos e visibilityFilter ). Esses valores são simplesmente alterados chamando métodos de todoService . Graças ao gancho useObservable, que assina as alterações de valor, esses componentes são redesenhados automaticamente quando o estado geral é alterado.


Conclusão


Se compararmos esse código com a abordagem Redux, veremos várias vantagens:


  • Concisão: a única coisa que precisávamos fazer era useObservable os valores de estado no Observable e usar hook useObservable ao acessar esses valores a partir dos componentes. Não há necessidade de declarar ação, criador de ação, escrever ou combinar redutor ou conectar nossos componentes ao repositório com os mapDispatchToProps e mapDispatchToProps .
  • Simplicidade: agora é muito mais fácil acompanhar a execução do código. Entender o que realmente acontece quando um botão é pressionado é apenas uma questão de mudar para a implementação do método chamado. A execução passo a passo usando o depurador também é significativamente aprimorada, pois não há nível intermediário entre nossos componentes e nossos serviços.
  • Segurança de digitação pronta para uso : os desenvolvedores do TypeScript não precisarão de trabalho adicional para digitar o código corretamente. Não há necessidade de declarar tipos para o estado e para cada ação.
  • Suporte para assíncrono / espera: embora isso não tenha sido demonstrado aqui, esta solução funciona muito bem com funções assíncronas, simplificando bastante a programação assíncrona. Não há necessidade de confiar no middleware, como o redux-thunk, que requer um profundo conhecimento de programação funcional para entender.

O Redux, é claro, ainda tem algumas vantagens importantes, especialmente o Redux DevTools, que permite que os desenvolvedores monitorem as alterações de estado durante o desenvolvimento e passem no tempo para os estados anteriores do aplicativo, o que pode ser uma ótima ferramenta de depuração. Mas, na minha experiência, raramente usei isso, e o preço a pagar parece alto demais para um pequeno ganho.


Em todos os nossos aplicativos React e React Native, usamos com sucesso uma abordagem semelhante à descrita neste artigo. De fato, nunca sentimos a necessidade de um sistema de gerenciamento de estado mais complexo do que isso.


Anotações


A classe Observable introduzida neste post é bastante simples. Ele pode ser substituído por implementações mais avançadas, como micro-observáveis ​​(nossa própria biblioteca) ou RxJS.


A solução apresentada aqui é muito semelhante ao que pode ser alcançado com o MobX. A principal diferença é que o MobX suporta uma profunda mutabilidade do estado dos objetos. Ele também conta com os proxies do ES6 para notificar alterações, re-renderizar implicitamente e complicar a depuração quando as coisas não funcionam como o esperado. Além disso, o MobX não funciona bem com funções assíncronas.


De um tradutor: esta publicação, que pode ser vista como uma introdução ao gerenciamento de estado usando o Observable, é uma continuação do tópico abordado no artigo Gerenciando o estado do aplicativo com o RxJS / Immer como uma alternativa simples ao Redux / MobX , que descreve como simplificar o uso dessa abordagem.

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


All Articles