Escalando o gerenciamento de estados no React

Patrick Porto
Loft
Published in
7 min readNov 26, 2020

O grande desafio de muitas pessoas desenvolvedoras frontend é projetar bons gerenciamentos de estados em aplicações React. A complexidade de gerenciamento de estados em aplicações de larga escala é tamanha que se torna quase obrigatório utilizar bibliotecas terceiras para auxiliarem nesta tarefa. Hoje iremos analisar os aspectos de algumas opções de gerenciamento de estados disponíveis.

Porque um gerenciamento de estados é necessário?

Componentes React possuem states que são estados internos para armazenar propriedades.O problema ocorre quando existe necessidade de passar para outros componentes esses estados internos como o exemplo a seguir:

Nessa abordagem acabamos por nos deparar com inúmeras passagens de props difíceis de gerenciar e que irão impactar tanto na performance quanto na manutenibilidade da aplicação. Para resolver o problema das inúmeras passagens de props, se faz necessária uma arquitetura que forneça um estado comum que cada componente pode acessá-lo diretamente sem precisar ficar passando via props de um para o outro.

Redux

Redux é uma biblioteca de gerenciamento de estados que possui um estado compartilhado chamado de Store que é manipulado através de Actions e Reducers. Ele fornece um estado compartilhado onde cada componente pode acessar os dados que achar pertinente.

Problema #1 — Async

Redux não foi planejado para trabalhar com tarefas assíncronas, como requisitar dados de um servidor HTTP. Caso precise de async precisará de middlewares de terceiros, como redux-thunk ou redux-sagas.

O código a seguir só funciona porque o retorno da Action é uma função que o middleware do redux-thunk interpreta como uma chamada assíncrona.

Além disso, é pertinente saber o status de uma tarefa assíncrona. Por exemplo, saber se uma requisição ainda está em andamento para exibir uma mensagem de carregamento. Nesse caso toda a sua arquitetura de estados precisa ser projetada para trabalhar com metadados conforme o código a seguir.

O React tem introduzido uma solução para saber o status de tarefas assíncronas que chama-se React Suspense. Ele elimina a necessidade de ter metadados no estado para tarefas assíncronas. Mas React Suspense tem um funcionamento muito diferente do Redux e é complicado fazê-los trabalhar juntos.

Problema #2 — Relação entre Actions e estados

Não existe relação direta entre Actions e os estados, tornando qualquer forma de encapsulamento ineficaz. Observe o exemplo a seguir:

O código está semanticamente associado a entidade User. Mas o que acontece caso User seja um item dentro de uma listagem?

Agora temos tanto userReducer quanto userListReducer escutando os mesmos eventos dos Actions de User. Manter a sincronia entre os estados será ainda maior caso tenham eventos para alterações de propriedades específicas como username.

Problema #3 — Estados negligenciados

Por mais que ter um estado compartilhado tenha sido um grande benefício do Redux também trouxe grandes dores de cabeça. Estados iniciados com valores nulos podem quebrar componentes que esperam algum valor válido para serem renderizados. Observe o exemplo a seguir:

O userReducer foi inocentemente iniciado com um valor nulo e só iremos perceber as consequências quando for escrito um teste que não tem diretamente a ver com userReducer.

A seguir iremos escrever um teste que não tem nenhuma relação com currentUser:

Por mais que pareça tudo certo com esse caso de teste ele acaba quebrando porque algum componente dentro de Content tenta acessar alguma propriedade de currentUser.

Essa prática era ainda mais terrível quando a adoção do Enzyme era comum porque muitos testes eram escritos com render shallow para não renderizar a árvore completa de componentes e não precisar iniciar esses estados mal projetados.

Context API e Hooks

Quando o Context API foi introduzido no React muitos paralelos foram feitos entre ele e o Redux. Quando os hooks foram introduzidos se tornou mais fácil trabalhar com os Contexts pois os hooks poderiam abstrair a complexidade de acessá-los.

Outra vantagem de se trabalhar com Context API e hooks é serem compatíveis com React Suspense. Logo metadados para saber o status de tarefas assíncronas tornam-se desnecessários.

Problema #1 — Atualizações complexas

Segundo Sebastian Markbage, engenheiro no Facebook, Context API não é eficiente em atualizações de estado complexas ou recorrentes. Context API funciona bem para estados que mudam com baixa frequência, como idioma ou tema, mas não está pronto para substituir o Redux.

Todos os Consumers serão forçadamente re-renderizados sempre que o valor do Provider for atualizado. O problema de performance é tão relevante que o time que cuida do React-Redux teve que reverter parte da biblioteca reescrita utilizando Context API.

O uso de Context API para casos de uso mais intensos deve ser evitado até alguma proposta como Lazy Context ser aceito.

Problema #2 — ContextHell / HookHell

Para mitigar o problema das atualizações complexas, grandes projetos tendem a ter muitos contexts e hooks conforme o exemplo a seguir:

Depois do terceiro Provider o código aumenta significativamente a sua complexidade cognitiva.

Atom

O Facebook lançou uma biblioteca em estágio experimental chamada Recoil, ela resolve muitos dos problemas existentes nas abordagens anteriores. Diferente do Redux, o Recoil manipula os states diretamente, sem Actions como intermediários. Esse modelo de gerenciamento de estados é chamado de Atom, onde cada Atom possui apenas um getter e um setter, semelhante a hook useState do React mas trabalhando com um estado compartilhado como o Redux.

Atom foi projetado para trabalhar com React Suspense, tornando desnecessário projetar metadados de requisições assíncronas.

Recoil é considerado experimental, logo é desaconselhável para produção. Entretanto existem outras bibliotecas que trabalham com Atoms e são estáveis, entre elas o Jotai.

O código a seguir possui exemplos de Atoms em Jotai:

Quando um objeto literal é passado o mesmo é interpretado como o valor inicial do Atom. Quando uma função é recebida no primeiro parâmetro é considerado uma função getter enquanto que no segundo parâmetro é uma função setter. Repare que no segundo parâmetro o valor do Atom é atualizado através do método set ao invés do retorno da função.

O código a seguir demonstra como os Atoms são utilizados nos componentes React:

O modelo de gerenciamento de estados através de Atoms foi projetado para ser semelhante a hook useState mas possuem a vantagem do estado compartilhado.

Comparativos

Este é um comparativo entre as abordagens na data desta publicação. Verifique se não houve nenhuma atualização.

Comparativo entre as bibliotécas

Caso considere tipagem gradual um aspecto determinante considere Jotai sobre as demais soluções. Ele foi planejado para ser orientado a TypeScript.

Considerações Finais

Conforme foi possível constatar nesse artigo, ainda precisaremos de uma biblioteca para gerenciamento de estados para trabalhar performaticamente com React.

Redux não irá desaparecer da noite para o dia, até porque possui um dos ecossistemas mais maduros entre todas as propostas de solução para gerenciamento de estados. Os problemas listados nesse artigo podem ser contornados com muita disciplina e às vezes com boas regras no eslint.

Atom ainda não é a solução definitiva pois ainda precisa ser provado em larga escala, tanto é que o Recoil ainda é experimental. Mas foi planejado para ser aderente às tendências com React. Trabalhar com ele não terá uma curva de aprendizado maior do que aprender como funciona uma hook do React.

Referências

--

--