react-brasil / forum

:beer: Portando discussões feitas em grupos (Facebook, Google Groups, Slack, Disqus) pra issues
MIT License
30 stars 0 forks source link

Redux #2

Open diegohaz opened 5 years ago

diegohaz commented 5 years ago

O @hnordt criou uma thread no Slack que acabou gerando uma discussão bacana sobre Redux. Sugeriram trazer a discussão pro fórum. Como eu pretendo escrever uma newsletter sobre isso em breve, resolvi abrir isso aqui pra compartilhar algumas ideias. 😊

Na minha concepção, Redux é desnecessário em aplicações pequenas e não escala bem em aplicações grandes.

É importante frisar que esta é a minha humilde opinião baseada na minha experiência: cerca de 2 anos arquitetando e mantendo Redux em diversos projetos pequenos (Trama), alguns médios (Storia) e pelo menos um bem grandinho com vários apps compartilhando módulos do Redux (HelloMD). Além disso, eu mantenho um boilerplate que vem sendo usado como base por centenas de outros projetos.

Vou citar aqui os problemas que eu vejo (e que eu mesmo já tive) que normalmente são resultado de tentativas ineficázes de escalar com Redux.

Acoplar a estrutura do Redux à estrutura de componentes

A estratégia mais comum que eu vejo por aí é tentar encapsular os módulos do Redux usando a estrutura de componentes. Algo como nesta árvore:

src
└── components
    └── Post
        ├── components
        ├── containers
        ├── actions.js
        ├── index.js
        ├── reducer.js
        ├── saga.js
        └── selectors.js

A própria documentação do Redux desencoraja isso:

Because the store represents the core of your application, you should define your state shape in terms of your domain data and app state, not your UI component tree

redux.js.org/recipes/structuringreducers...

Acontece que, muitas vezes, — especialmente antes da nova context API ser lançada — fazia total sentido usar o Redux para controlar alguns estados da UI, como um LoginModal, por exemplo, que deve poder ser aberto de qualquer outra parte do app.

Isso gera um problema de arquitetura do qual você não consegue se livrar de uma forma simples usando Redux.

O engenheiro vai sempre se perguntar: eu coloco esses arquivos dentro da pasta do componente? Coloco junto com os outros estados globais? Coloco as actions fora e o reducer dentro?

Essas dúvidas normalmente indicam uma arquitetura fraca. O que não é exatamente um problema. Afinal, nós estamos sempre trabalhando com tecnologias novas e requisitos que mudam com o tempo. Acertar a arquitetura no dia zero não é nem algo que deve ser perseguido.

Por isso, escolher tecnologias que facilitem refatoração é fundamental. Nesse sentido, Redux é uma péssima escolha. Dependendo do tamanho e da atual fragmentação do seu app, pode ser extremamente difícil identificar quais reducers e/ou sagas estão ouvindo determinada action, e qualquer mudança aí pode quebrar algum fluxo de forma silenciosa.

Organizar o Redux em módulos auto-contidos

Os módulos do Redux de uma aplicação devem ser considerados um processo separado, com entradas (actions) e saídas (selectors). Para o resto da aplicação, o que acontece no meio disso (thunks, sagas, middlewares etc.) não deveria importar.

Um simples teste de integração do Redux poderia ser descrito assim:

1. dispatch an action
2. wait
3. check state

Dito isso, a melhor forma de estruturar o Redux dentro de uma aplicação é mantê-lo isolado do resto da aplicação (numa pasta store, por exemplo).

Se tiver isso em mente, você pode eventualmente migrar de React pra Vue, pra Angular, ou mesmo pra Vanilla JS, e continuar usando a mesma store. Basta que os bindings (react-redux, por exemplo) sejam alterados.

No entanto, mesmo que isso resolva o problema de acoplamento, organizar reducers, sagas, actions, selectors e middlewares ainda é um problema.

Qualquer tentativa de separar — e portanto escalar — reducers, sagas e middlewares é uma ilusão. No final, só vai existir um de cada. Por outro lado, não é viável lidar com todo o estado de uma aplicação grande em um único reducer.

Você pode usar ducks, redux-modules ou outra abordagem. A realidade é que não dá pra garantir que os módulos sejam realmente auto-contidos. Eles eventualmente vão precisar se comunicar entre si.

E mais uma vez isso gera um problema de arquitetura do qual você não consegue se livrar de uma forma simples usando Redux.

Separar por tipo: actions, reducers, sagas etc.

Essa é a forma como o Spectrum é estruturado, por exemplo, e provavelmente uma das mais comuns entre os iniciantes. Hoje, se eu fosse trabalhar com Redux, usaria ela.

No final, dentro de cada pasta, ainda vão existir módulos. Essa estrutura, no entanto, nos dá uma maior liberdade nessa modularização, já que você pode agrupar actions de uma forma, e reducers de outra. O que faz mais sentido, já que um reducer pode responder a actions que não necessariamente estariam agrupadas.

Ainda assim, isso continua não resolvendo os problemas relacionados ao acoplamento com a UI. Mesmo o pessoal do Spectrum teve que criar actions e um reducer pra controlar o estado do modal, onde eles inclusive armazenam props. Na HelloMD, nós fizemos algo muito parecido.

O que eu uso hoje?

Eu criei o Constate com o objetivo principal de resolver meus problemas com escalabilidade no gerenciamento de estado.

A partir dessas experiências, eu pude classificar o estado de uma aplicação em três tipos, considerando a forma como eles são definidos: local, local-global e global.

Era de fundamental importância que eu pudesse migrar de local para local-global com facilidade, e que o global fosse usado só quando realmente necessário.

Além disso, eu precisava de uma única biblioteca que me permitisse fazer isso tudo com todo o tooling disponível no Redux (devtools, time travel etc.).

Local

É aquele onde você usa setState e this.state no próprio componente, normalmente relacionados à UI. Por exemplo:

No Constate, isso é feito assim:

<ModalContainer>
  {({ open, close, isOpen }) => (
    <>
      <Button onClick={open} />
      <Modal isOpen={isOpen} onRequestClose={close} />
    </>
  )}
</ModalContainer>

Local-global (ou contextual)

É a maior parte do estado global. É aquele que você quer programar como se fosse local, mas quer que várias partes do app tenham acesso (o que gera a necessidade de separar redux em módulos). Por exemplo:

No Constate, isso é feito assim (perceba que é só uma prop a mais —context):

<Provider devtools={process.env.NODE_ENV === "development"}>

  <ModalContainer context="my-modal">
    {({ open }) => <Button onClick={open} />}
  </ModalContainer>

  <ModalContainer context="my-modal">
    {({ close, isOpen }) => (
      <Modal isOpen={isOpen} onRequestClose={close} />
    )}
  </ModalContainer>

</Provider>

Global

É o que você escreve com o objetivo de fazer estados local-global se comunicarem.

Por exemplo, se você quiser manter em um contexto a última action disparada em qualquer outro contexto, isso poderia ser feito assim no Constate:

function onUpdate({ context, type, state, setContextState }) {
  if (context !== "lastAction") {
    setContextState("lastAction", {
      context,
      type,
      state: state[context]
    })
  }
}

<Provider onUpdate={onUpdate}>
  <Container context="lastAction">
    {({ context, type, state }) => (
      ...
    )}
  </Container>
</Provider>

Enfim, seria legal que vocês deixassem suas ideias, opiniões, experiências etc.

raisiqueira commented 5 years ago

Sendo bem sincero, hoje na aplicação que estamos desenvolvendo na empresa (um pouco grande, diga-se de passagem), estamos usando as actions, reducers, sagas e selectors dentro do próprio componente, começamos assim pq está (por enquanto) sendo confortável para a equipe.