paint-brush
Como os átomos fixam o fluxopor@bowheart
2,015 leituras
2,015 leituras

Como os átomos fixam o fluxo

por Josh Claunch13m2023/06/05
Read on Terminal Reader

Muito longo; Para ler

A arquitetura do Flux ecoa em todos os gerenciadores de estado modernos do React. As bibliotecas atômicas cumprem a visão original do Flux melhor do que o Flux jamais poderia, oferecendo melhor escalabilidade, autonomia, divisão de código, gerenciamento de cache, organização de código e primitivas para compartilhamento de estado.
featured image - Como os átomos fixam o fluxo
Josh Claunch HackerNoon profile picture


Recoil introduziu o modelo atômico no mundo React . Seus novos poderes vieram à custa de uma curva de aprendizado íngreme e recursos de aprendizado escassos.


Jotai e Zedux posteriormente simplificaram vários aspectos desse novo modelo, oferecendo muitos novos recursos e ampliando os limites desse incrível novo paradigma.


Outros artigos se concentrarão nas diferenças entre essas ferramentas. Este artigo se concentrará em um grande recurso que todos os três têm em comum:


Eles consertaram o Flux.


Índice

  • Fluxo
  • Árvores de Dependência
  • O Modelo Singleton
  • Voltar à rotina
  • A Lei de Deméter
  • Os heróis
  • Zedux
  • O modelo atômico
  • Conclusão


Fluxo

Se você não conhece o Flux, aqui vai um resumo:



Ações -> Despachante -> Lojas -> Visualizações



Além do Redux , todas as bibliotecas baseadas no Flux seguiram basicamente este padrão: um aplicativo tem várias lojas. Existe apenas um Despachante cujo trabalho é alimentar ações para todas as lojas na ordem adequada. Essa "ordem adequada" significa classificar dinamicamente as dependências entre as lojas.


Por exemplo, considere a configuração de um aplicativo de comércio eletrônico:



UserStore <-> CartStore <-> PromosStore




Quando o usuário move, digamos, uma banana para o carrinho, o PromosStore precisa aguardar a atualização do estado do CartStore antes de enviar uma solicitação para ver se há um cupom de banana disponível.


Ou talvez as bananas não possam ser enviadas para a área do usuário. O CartStore precisa verificar o UserStore antes de atualizar. Ou talvez os cupons só possam ser usados uma vez por semana. A PromosStore precisa consultar a UserStore antes de enviar a solicitação do cupom.



Flux não gosta dessas dependências. Dos documentos legados do React :


Os objetos dentro de um aplicativo Flux são altamente desacoplados e seguem fortemente a Lei de Demeter , o princípio de que cada objeto dentro de um sistema deve saber o mínimo possível sobre os outros objetos do sistema.


A teoria por trás disso é sólida. 100%. Então ... por que esse sabor de várias lojas do Flux morreu?


Árvores de Dependência

Acontece que as dependências entre contêineres de estado isolados são inevitáveis. Na verdade, para manter o código modular e DRY, você deve usar outras lojas com frequência.


No Flux, essas dependências são criadas em tempo real:


 // This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })


Este exemplo mostra como as dependências não são declaradas diretamente entre as lojas - em vez disso, elas são agrupadas por ação. Essas dependências informais exigem que você pesquise o código de implementação para encontrá-las.


Este é um exemplo muito simples! Mas você já pode ver como o Flux se sente desordenadamente. Efeitos colaterais, operações de seleção e atualizações de estado são todos remendados. Este colocation pode realmente ser legal. Mas misture algumas dependências informais, triplique a receita e sirva em algum prato e você verá o Flux quebrar rapidamente.


Outras implementações do Flux, como Flummox e Reflux, melhoraram o clichê e a experiência de depuração. Embora muito utilizável, o gerenciamento de dependências era o único problema persistente que afetava todas as implementações do Flux. Usar outra loja parecia feio. Árvores de dependência profundamente aninhadas eram difíceis de seguir.


Fluxo na Teoria vs Fluxo na Prática



Este aplicativo de comércio eletrônico poderia um dia ter lojas para OrderHistory, ShippingCalculator, DeliveryEstimate, BananasHoarded, etc. Um aplicativo grande poderia facilmente ter centenas de lojas. Como manter as dependências atualizadas em todas as lojas? Como você acompanha os efeitos colaterais? E a pureza? E a depuração? As bananas são realmente uma baga?


Quanto aos princípios de programação introduzidos pelo Flux, o fluxo de dados unidirecional foi o vencedor, mas, por enquanto, a Lei de Deméter não.


O Modelo Singleton

Todos nós sabemos como o Redux apareceu para salvar o dia. Ele abandonou o conceito de várias lojas em favor de um modelo singleton. Agora tudo pode acessar todo o resto sem nenhuma "dependência".



Ações -> Middleware -> Loja -> Exibições




Os redutores são puros, portanto, toda a lógica que lida com várias fatias de estado deve ir para fora do armazenamento. A comunidade criou padrões para gerenciar os efeitos colaterais e o estado derivado. As lojas Redux são lindamente depuráveis. A única grande falha de fluxo que o Redux originalmente não conseguiu consertar foi seu clichê.


Mais tarde, o RTK simplificou o infame clichê do Redux. Então Zustand removeu algumas penugens ao custo de algum poder de depuração. Todas essas ferramentas se tornaram extremamente populares no mundo React.


Com o estado modular, as árvores de dependência crescem tão naturalmente complexas que a melhor solução que pudemos pensar foi: "Apenas não faça isso, eu acho."


Tem problemas? Apenas não



E funcionou! Essa nova abordagem singleton ainda funciona bem o suficiente para a maioria dos aplicativos. Os princípios do Flux eram tão sólidos que a simples remoção do pesadelo da dependência o corrigiu.


Ou fez?


Voltar à rotina

O sucesso da abordagem singleton levanta a questão: o que o Flux queria dizer em primeiro lugar? Por que queremos várias lojas?


Permita-me lançar alguma luz sobre isso.

Razão # 1: Autonomia

Com várias lojas, os pedaços de estado são divididos em seus próprios contêineres modulares e autônomos. Essas lojas podem ser testadas isoladamente. Eles também podem ser compartilhados facilmente entre aplicativos e pacotes.

Razão nº 2: divisão de código

Esses armazenamentos autônomos podem ser divididos em blocos de código separados. Em um navegador, eles podem ser carregados lentamente e conectados em tempo real.


Os redutores do Redux também são bastante fáceis de dividir em código. Graças a replaceReducer , a única etapa extra é criar o novo redutor combinado. No entanto, mais etapas podem ser necessárias quando efeitos colaterais e middleware estão envolvidos.

Razão #3: Primitivos Padronizados

Com o modelo singleton, é difícil saber como integrar o estado interno de um módulo externo com o seu. A comunidade Redux introduziu o padrão Ducks como uma tentativa de resolver isso. E funciona, ao custo de um pequeno clichê.


Com várias lojas, um módulo externo pode simplesmente expor uma loja. Por exemplo, uma biblioteca de formulários pode exportar um FormStore. A vantagem disso é que o padrão é "oficial", o que significa que é menos provável que as pessoas criem suas próprias metodologias. Isso leva a uma comunidade mais robusta e unificada e a um ecossistema de pacotes.

Razão #4: Escalabilidade

O modelo singleton tem um desempenho surpreendente. Redux provou isso. No entanto, seu modelo de seleção tem um limite superior rígido. Escrevi alguns pensamentos sobre isso nesta discussão Reselect . Uma árvore seletora grande e cara pode realmente começar a se arrastar, mesmo ao assumir o controle máximo sobre o armazenamento em cache.


Por outro lado, com armazenamentos múltiplos, a maioria das atualizações de estado são isoladas em uma pequena parte da árvore de estado. Eles não tocam em mais nada no sistema. Isso é escalável muito além da abordagem singleton - na verdade, com vários armazenamentos, é muito difícil atingir as limitações da CPU antes de atingir as limitações de memória na máquina do usuário.

Razão # 5: Destruição

Destruir o estado não é muito difícil no Redux. Assim como no exemplo de divisão de código, são necessárias apenas algumas etapas extras para remover uma parte da hierarquia do redutor. Mas ainda é mais simples com várias lojas - em teoria, você pode simplesmente separar a loja do despachante e permitir que ela seja coletada como lixo.

Razão #6: Colocação

Este é o grande problema que Redux, Zustand e o modelo singleton em geral não lidam bem. Os efeitos colaterais são separados do estado com o qual eles interagem. A lógica de seleção é separada de tudo. Embora o Flux de várias lojas talvez estivesse muito localizado, o Redux foi para o extremo oposto.


Com várias lojas autônomas, essas coisas naturalmente andam juntas. Realmente, o Flux só carecia de alguns padrões para impedir que tudo se tornasse uma mistura desordenada de gobbledygook (desculpe).

Resumo dos motivos

Agora, se você conhece a biblioteca OG Flux, sabe que na verdade ela não era boa em nada disso. O despachante ainda adota uma abordagem global - despachando cada ação para cada loja. A coisa toda com dependências informais/implícitas também tornou a divisão e destruição de código menos do que perfeita.


Ainda assim, o Flux tinha muitos recursos interessantes a seu favor. Além disso, a abordagem de armazenamento múltiplo tem potencial para ainda mais recursos, como inversão de controle e gerenciamento de estado fractal (também conhecido como local).


Flux poderia ter evoluído para um gerente de estado verdadeiramente poderoso se alguém não tivesse nomeado sua deusa Deméter. Estou falando sério! ... Ok, eu não sou. Mas agora que você mencionou, talvez a lei de Deméter mereça um olhar mais atento:

A Lei de Deméter

O que exatamente é essa chamada "lei"? Da Wikipédia :


  • Cada unidade deve ter apenas conhecimento limitado sobre outras unidades: apenas unidades "estreitamente" relacionadas à unidade atual.
  • Cada unidade deve falar apenas com seus amigos; não fale com estranhos.


Esta lei foi projetada tendo em mente a Programação Orientada a Objetos, mas pode ser aplicada em muitas áreas, incluindo o gerenciamento de estado do React.


O PromosStore não deve usar o estado interno ou as dependências do CartStore


A ideia básica é evitar que uma loja:


  • Acoplando-se firmemente aos detalhes de implementação de outra loja.
  • Usando lojas que não precisam saber .
  • Usar qualquer outra loja sem declarar explicitamente uma dependência dessa loja.


Em termos de banana, uma banana não deve descascar outra banana e não deve falar com uma banana em outra árvore. No entanto, ele pode falar com a outra árvore se as duas árvores conectarem primeiro uma linha telefônica banana.


Isso incentiva a separação de preocupações e ajuda seu código a permanecer modular, DRY e SOLID. Teoria sólida! Então, o que estava faltando no Flux?


Bem, as dependências entre lojas são uma parte natural de um bom sistema modular. Se uma loja precisar adicionar outra dependência, ela deve fazer isso e o mais explicitamente possível . Aqui está um pouco desse código Flux novamente:


 PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })


PromosStore tem múltiplas dependências declaradas de maneiras diferentes - ele espera e lê de CartStore e lê de UserStore . A única maneira de descobrir essas dependências é procurar lojas na implementação do PromosStore.


As ferramentas de desenvolvimento também não podem ajudar a tornar essas dependências mais detectáveis. Em outras palavras, as dependências são muito implícitas.


Embora este seja um exemplo muito simples e inventado, ele ilustra como Flux interpretou mal a Lei de Deméter. Embora eu tenha certeza de que nasceu principalmente de um desejo de manter as implementações do Flux pequenas (o gerenciamento de dependência real é uma tarefa complexa!), Foi aqui que o Flux falhou.


Ao contrário dos heróis desta história:

Os heróis

Em 2020, o Recoil entrou em cena tropeçando. Embora um pouco desajeitado no início, ele nos ensinou um novo padrão que reviveu a abordagem de várias lojas do Flux.


Fluxo de dados unidirecional movido da própria loja para o gráfico de dependência. As lojas agora eram chamadas de átomos. Os átomos eram adequadamente autônomos e divisíveis por código. Eles tinham novos poderes como suporte de suspense e hidratação. E o mais importante, os átomos declaram formalmente suas dependências.


O modelo atômico nasceu.


 // a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })


O Recoil lutou com uma base de código inchada, vazamentos de memória, desempenho ruim, desenvolvimento lento e recursos instáveis - principalmente efeitos colaterais. Isso resolveria lentamente alguns deles, mas, nesse meio tempo, outras bibliotecas pegaram as ideias de Recoil e trabalharam com elas.


Jotai entrou em cena e rapidamente ganhou seguidores.


 // a Jotai atom const greetingAtom = atom('Hello, World!')


Além de ser uma pequena fração do tamanho do Recoil, o Jotai oferecia melhor desempenho, APIs mais elegantes e nenhum vazamento de memória devido à sua abordagem baseada em WeakMap.


No entanto, isso custou algum poder - a abordagem WeakMap torna o controle do cache difícil e o compartilhamento de estado entre várias janelas ou outros domínios quase impossível. E a falta de chaves de string, embora elegante, torna a depuração um pesadelo. A maioria dos aplicativos deve adicioná-los de volta, manchando drasticamente a elegância de Jotai.


 // a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'


Algumas menções honrosas são Reatom e Nanostores . Essas bibliotecas exploraram mais a teoria por trás do modelo atômico e tentam levar seu tamanho e velocidade ao limite.


O modelo atômico é rápido e escala muito bem. Mas até muito recentemente, havia algumas preocupações que nenhuma biblioteca atômica havia abordado muito bem:


  • A curva de aprendizado. Os átomos são diferentes . Como tornamos esses conceitos acessíveis para os desenvolvedores do React?


  • Dev X e depuração. Como tornamos os átomos detectáveis? Como você rastreia atualizações ou aplica boas práticas?


  • Migração incremental para bases de código existentes. Como você acessa as lojas externas? Como você mantém a lógica existente intacta? Como você evita uma reescrita completa?


  • Plug-ins. Como tornamos o modelo atômico extensível? Ele pode lidar com todas as situações possíveis?


  • Injeção de dependência. Os átomos definem naturalmente as dependências, mas eles podem ser trocados durante o teste ou em ambientes diferentes?


  • A Lei de Deméter. Como ocultamos detalhes de implementação e evitamos atualizações dispersas?


É aqui que eu entro. Veja, eu sou o principal criador de outra biblioteca atômica:

Zedux

Zedux finalmente entrou em cena há algumas semanas. Desenvolvido por uma empresa Fintech em Nova York - a empresa para a qual trabalho - o Zedux não foi projetado apenas para ser rápido e escalável, mas também para fornecer uma experiência elegante de desenvolvimento e depuração.


 // a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')


Não vou me aprofundar nos recursos do Zedux aqui - como eu disse, este artigo não focará nas diferenças entre essas bibliotecas atômicas.


Basta dizer que o Zedux aborda todas as preocupações acima. Por exemplo, é a primeira biblioteca atômica a oferecer Inversão de Controle real e a primeira a nos trazer de volta à Lei de Deméter, oferecendo exportações atômicas para ocultar detalhes de implementação.


As últimas ideologias do Flux foram finalmente revividas - não apenas revividas, mas melhoradas! - graças ao modelo atômico.


Então, o que exatamente é o modelo atômico?

O modelo atômico

Essas bibliotecas atômicas têm muitas diferenças - elas até têm definições diferentes do que significa "atômico". O consenso geral é que os átomos são contêineres de estado autônomos, pequenos e isolados, atualizados de forma reativa por meio de um gráfico acíclico direcionado.


Eu sei, eu sei, parece complexo, mas espere até eu explicar com bananas.


Eu estou brincando! Na verdade é bem simples:


Atualizar -> UserAtom -> CartAtom -> PromosAtom



As atualizações ricocheteiam no gráfico. É isso!


A questão é que, independentemente da implementação ou da semântica, todas essas bibliotecas atômicas reviveram o conceito de várias lojas e as tornaram não apenas utilizáveis, mas uma verdadeira alegria de se trabalhar.


As 6 razões que dei para querer várias lojas são exatamente as razões pelas quais o modelo atômico é tão poderoso:


  1. Autonomia - Os átomos podem ser testados e usados completamente de forma isolada.
  2. Divisão de código - Importe um átomo e use-o! Nenhuma consideração extra necessária.
  3. Primitivos padronizados - Qualquer coisa pode expor um átomo para integração automática.
  4. Escalabilidade - As atualizações afetam apenas uma pequena parte da árvore de estado.
  5. Destruição - Basta parar de usar um átomo e todo o seu estado é coletado como lixo.
  6. Colocation - Os átomos definem naturalmente seu próprio estado, efeitos colaterais e lógica de atualização.


As APIs simples e a escalabilidade por si só tornam as bibliotecas atômicas uma excelente escolha para todos os aplicativos React. Mais potência e menos clichê do que o Redux? Isso é um sonho?

Conclusão

Que jornada! O mundo do gerenciamento de estado do React nunca para de surpreender, e estou muito feliz por ter pegado carona.


Estamos apenas começando. Há muito espaço para inovação com átomos. Depois de passar anos criando e usando o Zedux, vi como o modelo atômico pode ser poderoso. Na verdade, seu poder é seu calcanhar de Aquiles:


Quando os desenvolvedores exploram os átomos, eles geralmente se aprofundam nas possibilidades que voltam dizendo: "Olhe para este poder complexo louco", em vez de: "Veja como os átomos resolvem este problema de maneira simples e elegante". Estou aqui para mudar isso.


O modelo atômico e a teoria por trás dele não foram ensinados de uma forma acessível para a maioria dos desenvolvedores React. De certa forma, a experiência de átomos do mundo React até agora tem sido o oposto do Flux:


Átomos na teoria versus átomos na prática



Este artigo é o segundo de uma série de recursos de aprendizado que estou produzindo para ajudar os desenvolvedores do React a entender como as bibliotecas atômicas funcionam e por que você pode querer usar uma. Confira o primeiro artigo - Scalability: the Lost Level of React State Management .


Demorou 10 anos, mas a sólida teoria CS introduzida pelo Flux está finalmente impactando os aplicativos React em grande estilo, graças ao modelo atômico. E continuará a fazê-lo nos próximos anos.