Sem árvores, apenas pedras

Passo a passo com Redux: Um fluxo de trabalho simples e robusto para aplicações da vida real

Redux se tornou uma das mais populares implementações de Flux para gerenciar estado em aplicações React. Muitas vezes, se você começar a ler sobre Redux, vai sentir uma sobrecarga de conteúdo, onde você não consegue ver a floresta como um todo, apenas cada uma de suas árvores separadamente. A seguir, está um fluxo de trabalho altamente opinativo e direto de como implementar Redux em aplicações da vida real. Irei mostrar com exemplos e passo a passo como implementar isso em uma aplicação real. Iremos aplicar os princípios por trás do Redux de um modo prático e detalhar todo o processo por trás de cada decisão tomada.

Uma abordagem opinativa para usar Redux idiomaticamente

Redux se tornou mais do que apenas uma biblioteca, é um ecossistema por inteiro. Um das razões por trás da sua popularidade é a habilidade de acomodar diferentes estilos em diferentes “sabores”. Quero implementar ações assíncronas, eu devo usar thunk? Ou talvez promises? ou sagas?
Não há reposta certa para qual “sabor” você deve usar. E não há um jeito certo de usar Redux. Depois de dizer isso, ter muitas escolhas é sufocante. Eu quero apresentar uma maneira opinativa que eu pessoalmente gosto. É robusto, pode lidar com situações complicadas da vida real — e acima de tudo — é simples.


Chega de papo, vamos montar nossa aplicação!

Nós precisamos de um exemplo real para realizar o passo a passo. Já que seremos opinativos, o lugar mais interessante da internet, é o Reddit. Vamos fazer uma aplicação que mostra os posts mais interessantes de lá.
Na primeira página, vamos perguntar ao usuário 3 tópicos que ele está interessado. Então, vamos buscar a lista na página padrão do subreddits.
Depois de o usuário fazer suas escolhas, vamos mostrar uma lista de posts de cada um desses 3 tópicos em uma lista que seja possível filtrá-la — todos os tópicos, ou apenas um dos três. Se o usuário clicar em um post da lista, nós vamos mostrar o conteúdo.

Configuração inicial

Como vamos usar React para web (podemos adicionar React Native em um post futuro), nosso ponto inicial será utilizar o Create React App, que é o kit inicial oficial do Facebook. Nós também vamos instalar redux, react-redux e redux-thunk. O resultado deve ser algo como esse.

Saindo do clichê básico, vamos iniciar nossa Redux store e ligar ao nosso middleware no nosso index.js:

Flux e seu ciclo de vida em uma aplicação Redux

Um dos pontos principais que muitas vezes não estão presentes em tutoriais de Redux é como visualizar toda a aplicação e onde o Redux se encaixa nela. Redux nada mais é que uma implementação da arquitetura Flux — um padrão para direcionar dados em aplicações React.

Utilizando o Flux clássico, o estado da aplicação é guardado em “stores”. São despachados "actions" para causar a mudança desse estado, depois disso, as "views" que estão escutando por essas mudanças irão se re-renderizar baseado nesse novo estado:

Flux simplifica a vida fazendo com que os dados fluam em apenas uma direção. Isso reduz o efeito spaghetti que acontece em quando base de dados crescem muito e acabam ficando complexas.


Uma das dificuldades de entender Redux é a sobrecarga de nomes não-intuitivos como “reducers”, “selectors” e “thunks”. É mais fácil de ver onde todos eles se encaixam colocando eles no diagrama do Flux.

Abaixo, estão alguns termos técnicos de vários construtores que implementam diferentes partes do ciclo de vida Redux:

Como você deve ter notado, outros termos do ecossistema Redux como "middlewares" e "sagas", estão ausentes. Isso é intencional, porque eles não desempenham um papel significativo no nosso fluxo de trabalho.


A partir daqui, todas as dicas, estrutura e organização são opinativas. Todos os termos que estiverem entre "aspas", são termos do ecossistema Redux.


Estrutura de diretórios do nosso projeto

Nós vamos organizar nosso código dentro da pasta src:

  • /src/components, componentes React que não terão conhecimento sobre o Redux (inglês, “Dumb Components”).
  • /src/containers, componentes React que serão conectados a nossa Redux store (inglês, “Smart Components”).
  • /src/services, abstração para chamada de serviços externos (como API, servidores, etc).
  • /src/store, todo nosso código Redux ficará aqui, incluindo nossa lógica de negócio da aplicação.

Nosso diretório store será organizado por domínio (inglês, "domain", outro termo do ecossistema Redux), que irá contar com:

  • /src/store/{domain}/reducer.js, Um "reducer" exporta por padrão um objeto, export default, e também todos os "selectors" como named exports.
  • /src/store/{domain}/actions.js, todos os manipuladores do domínio (“thunks” e “action creators”).

Uma abordagem que coloca o estado da aplicação em primeiro lugar

Nossa aplicação tem duas páginas, nós vamos começar com a primeira e deixar o usuário selecionar 3 tópicos. Podemos começar implementando qualquer ponto do ciclo do Flux, mas eu descobri, que para mim, é mais fácil começar estruturando o estado da aplicação.

Então, qual estado nossa página de tópicos precisa?

Nós precisamos manter uma lista de tópicos que será buscada no servidor. Nós também precisamos manter os ID’s dos tópicos selecionados pelo usuário (no máximo 3). Seria bacana mante-los na ordem que foram selecionados, e no caso de já termos 3 selecionados, nós simplesmente descartamos o mais antigo.

Sendo assim, como ficaria nosso estado? Há uma lista de dicas no meu post anterior — Evitando complexidade acidental ao estruturar sua aplicação Redux. Seguindo aquelas dicas, essa seria uma estrutura apropriada:

A URL do dos tópicos serão usados como ID único.

Aonde nós vamos manter essa estrutura? No Redux, o “reducer” é o construtor que mantêm e atualiza o estado. Nós vamos organizar nosso código por domínio, então, naturalmente, o lugar correto para esse “reducer” é: /src/store/topics/reducer.js.

Existe um padrão para criar um “reducer”, você pode ver aqui. Perceba que para fazer cumprir a imutabilidade do nosso estado (que é exigido no Redux), eu escolhi usar uma biblioteca de imutabilidade chamada seamless-immutable.


1º Cenário — Página de tópicos

Depois de modelar o estado, eu gosto de implementar um ciclo do usuário do começo ao fim. No nosso caso, vamos criar nossa página de tópicos e mostrar alguns deles. Esse componente será conectado ao nosso "reducer", isso quer dizer, que será um “smart component” que tem conhecimento sobre nossa implementação Redux. Nós vamos coloca-lo em /src/containers/TopicsScreen.js.

Para criar um “smart component”, também existe um padrão, você pode ver aqui. Vamos mostrar esse componente como sendo o conteúdo do nosso App Component. Agora que tudo está configurado, nós podemos buscar nossos tópicos.

Regra: "Smart Components" não devem conter nenhuma lógica de negócio além de despachar ações.

Nesse cenário, nossa implementação começa no componentDidMount. Já que não podemos ter lógica diretamente na página, nós precisamos despachar ações que irão buscar os tópicos. Essas ações são assíncronas, claro, então, devemos criar uma “thunk”:

Para abstrairmos a API do servidor do Reddit, nós vamos criar um novo serviço que irá buscar os tópicos. Esses métodos serão assíncronos e nós devemos await (esperar) pela resposta. No geral, eu amo a API async/await, isso é tão verdade, que meu código não vê uma chamada direta para promises a um bom tempo.

Esse serviço irá retornar um array, mas nossa estrutura de dados guarda os tópicos em um map (objeto). O melhor lugar para fazer essa conversão, é no corpo da função dessa ação. Para realmente armazenar os dados no nosso estado, devemos invocar nosso “reducer”, para isso, vamos despachar uma ação chamada — TOPICS_FETCHED.

O código completo dessa etapa está disponível aqui.


Um pouco mais sobre a camada de "services"

Nós usamos “services” para abstrair API’s externas — em vários casos, API’s como do Reddit. O benefício dessa camada de abstração é de que, se a API mudar, nosso código está desassociado o máximo possível dessas mudanças. Se no futuro, o Reddit decidir renomear os endpoints ou mudar o nome dos campos, nós, possivelmente, iremos mudar apenas a camada de “service” da nossa aplicação.

Regra: “services” não devem conter estado.

Essa é uma regra complicada na nossa metodologia. Imagine se a API do Reddit exigir login? Nós estaremos tentados há manter esse estado de login na camada de “service”, instanciando ela junto dos detalhes de login.

Isso não é permitido na nossa metodologia porque todo o estado da aplicação deve ser mantido na nossa “store”. Mantendo o estado na camada “service” seria um vazamento de estado. A abordagem aceitável nesse caso, seria fornecer todas as funções da camada “service”, recebendo as informações de login como argumentos e mantendo o estado de login em um dos nossos “reducers”.

Implementar a camada“service” do nosso exemplo, é bem simples e direto, você pode conferir aqui.


Finalizando o 1º Cenário — “reducer” e “view”

Nossa ação — TOPICS_FETCHED — irá chegar no nosso “reducer” e ela contém o recém buscado topicsByUrl como parâmetro. Nosso “reducer” não precisa fazer muito, exceto, salvar esses dados no nosso estado:

Perceba que usamos seamless-immutable aqui, fazendo com que essa mudança de estado imutável seja explícita e direta. Bibliotecas de imutabilidade com toda certeza são opcionais, mas eu prefiro essa facilidade do que usar truques com spread em objetos.

Depois de atualizarmos o estado, nossa tela precisa ser re-renderizada. Isso significa que a "view" precisa escutar parte do estado, apenas a parte que interessa a essa tela. Isso é feito com mapStateToProps:

Eu decidi que nossa tela irá renderizar uma lista de tópicos usando um componente chamado ListView que recebe um map de rowsById e rowsIdArray (inspirado no React Native). Eu estou usando mapStateToProps para preparar essas duas propriedades para nossa "view" TopicsScreen (eles serão passados para ListView depois). As duas propriedades podem ser derivadas do nosso estado. Perceba algo interessante, eu não estou acessando o estado diretamente.

Regra: “Smart Components” devem sempre acessar o estado através de seletores.

“selectors” são uma das partes mais importantes do Redux, onde pessoas tendem a ignorar. Um "selector" é uma função pura que recebe o estado global como argumento e retorna um novo objeto baseado em alguma transformação nesse estado. Seletores são diretamente ligados aos “reducers” e ficam localizados dentro do nosso reducer.js. Eles nos permitem realizar alguns cálculos/transformações antes de serem consumidos pela “view”. Na nossa metodologia, nos levamos essa idéia ainda mais longe. Toda vez que alguém precisa acessar parte do estado (como em mapStateToProps), eles precisam fazer isso através de um "selector".

Porque? A idéia é encapsular a estrutura internal dos dados do estado e esconder isso das “views”. Imagine que em um futuro próximo, nós decidimos mudar a estrutura do nosso estado. Nós não queremos ir em todas nossas “views” e refatora-las. Passando elas através de seletores, nós confinamos essa refatoração apenas nos nossos “reducers”.

Esse é como nosso topics/reducer.js deve parecer:

Todo o estado atual da aplicação, incluindo ListView, pode ser encontrado aqui.


Um pouco mais sobre "dumb components”

ListView é um bom exemplo de “dumb component”. Ele não está conectado com nenhuma “store” do nosso Redux. E diferente dos nossos “smart components” que estão localizados em /src/containers, esses componentes estão em /src/components.

“Dumb Components” recebem dados dos seus componentes pais através de props e podem conter estado local. Vamos supor que você está implementando um componente TextInput do princípio. A posição do cursor é um excelente exemplo para manter um estado local que não deve ser armazenado no estado global.

Levando isso em consideração, quando é que nós devemos criar “dumb components” ao invés de “smart components”?

Regra: Reduza as lógicas em “smart components” extraindo-as em “dumb components”.

Se olharmos a implementação do ListView, você verá que ele contém a lógica da “view”, iterando em cada row. Nós queremos eliminar essa lógica do nosso “smart component” TopicsScreen. Assim, deixando nosso “smart component” limpo, outro benefício, é que agora, a lógica do ListView, pode ser reutilizada.


2º Cenário — seleção múltipla de tópicos

Nós completamos nosso primeiro cenário. Vamos seguir adiante para o próximo — permitindo que o usuário selecione 3 tópicos da lista.

Nosso cenário se inicia com o usuário clicando em um dos tópicos. Esse evento é tratado pelo TopicsScreen, mas, como esse componente não deve conter nenhuma lógica, nós vamos despachar uma nova ação — selectTopic. Essa ação também irá ser uma “thunk”, colocada em topics/actions.js.

Como você pode ver, quase toda ação que exportamos (para serem despachadas pelas “views”) são uma “thunk”.

Nós, basicamente, despachamos ações como objetos de dentro da “thunk” para atualizar o estado dos nossos “reducers”.

Um aspecto interessante dessa “thunk” é que ela precisa ter acesso ao estado. Perceba como nós usamos a regra de todo acesso ao estado deve ser através de seletores, mesmo em “thunks” (embora alguns irão dizer que fomos longe demais).

Nós precisamos atualizar o “reducer” para tratar a ação TOPICS_SELECTED e armazenar os tópicos selecionados. Há uma questão interessante, se selectTopic precisa ser uma “thunk” mesmo.

Nós poderíamos usar selectTopic como uma ação e despachar um objeto e mover toda essa lógica de negócio para o “reducer”. Essa é uma estratégia válida. Pessoalmente, eu prefiro manter essa lógica em “thunks”.

Após atualizarmos nosso estado, nós precisamos propagar a seleção de tópicos de volta para nossa “view”. Isso significa adicionar os tópicos selecionados na função mapStateToProps.

Já que a “view” precisa verificar se cada rowId está selecionada ou não, é mais conveniente passar esses dados para a “view” como um map.

Aliás, esses dados devem passar por um "selector" de qualquer jeito, lá será um ótimo lugar para fazer essa transformação.

Após implementar as mudanças acima, e, extrair a lógica de seleção da linha para um novo “dumb component” — ListRow — nossa aplicação fica assim.


Um pouco mais sobre lógica de negócio

Um dos pontos principais dessa metodologia é atingir uma separação clara entre “views” e lógicas de negócio. Até agora, aonde nossa lógica de negócio está implementada?

Toda nossa lógica de negócio está implementada na parte do Redux, dentro de /src/store. A maior parte está dentro de “thunks” no actions.js e algumas delas estão dentro de seletores no reducer.js. Aliás, essa é uma regra oficial:

Regra: Mantenha toda sua regra de negócio dentro de ações (“thunks”), seletores (“selectors”) e “reducers”.

Navegando para a próxima tela — a lista de posts

Quando nós temos mais de uma tela, nós precisamos, de alguma maneira, navegar até ela. Isso é normalmente alcançado utilizando um componente de navegação como o react-router. Eu quero evitar de usar um router aqui para deixar nosso exemplo simples. Dependências externas e opinativas como routers, normalmente desviam a atenção do assunto principal.

Ao invés disso, vamos adicionar um variável no estado, chamada selectionFinalized, que irá nos dizer se o usuário completou a seleção ou não.

Assim que o usuário selecionar 3 tópicos, nós vamos mostrar um botão, que uma vez clicado, irá finalizar a seleção e nós enviar para a próxima tela.

Clicando no botão, irá despachar uma ação que irá atualizar essa variável diretamente.

É bem parecido com o que estamos fazendo até agora, a parte interessante, é saber quando mostrar o botão (logo após o usuário selecionar as 3 opções).

Ficamos tentados a adicionar mais uma variável para controlar isso, mas, na verdade, essa variável pode ser derivada dos dados que já temos no estado. Isso significa que devemos implementar essa lógica no “selector”:

A implementação completa do código acima pode ser encontrada aqui.

Para realmente mudar de tela, nós vamos precisar mudar nosso App, transformando-o em um componente conectado ao Redux e escutando selectionFinalized em sua função mapStateToProps.

A implementação completada dessa parte, pode ser encontrada aqui.


Mostrando o post — mais uma vez, estado em primeiro lugar

Agora que estamos experientes com essa metodologia, nós podemos implementar a segunda tela um pouco mais rápido.

Essa nova tela irá ter um novo arquivo “domain” — posts. Para manter a nossa aplicação o mais modular possível, vamos dar a esse arquivo um “reducer” e um pedaço separado no estado.

Lembre-se — essa tela tem como propósito, mostrar uma lista de posts que podem ser filtrados de acordo com o tópico. O usuário pode clicar no post e ver seu conteúdo.

Seguindo nossas dicas de estrutura do post anterior, a estrutura a seguir pode funcionar:

E assim nasce o nosso novo “reducer”.


Segunda página, primeiro cenário — lista de post sem filtro

Como estamos fazemos, depois de modelar nosso estado, nós podemos implementar o cenário do usuário do começo ao fim. Vamos começar mostrando a lista completa de post sem nenhum filtro.

Nós vamos precisar de um novo “smart component” para mostrar os posts, vamos chamá-lo de PostsScreen e ele irá despachar uma nova ação chamada fetchPosts quando ela for montada. A ação será uma “thunk” dentro do domínio posts/actions.js.

Isso é bem similar ao que fizemos anteriormente. A implementação completa pode ser encontrada aqui.

Essa “thunk” irá despachar um objeto com a ação POSTS_FETCHED que levará os posts para o “reducer”. Nós teremos que modificar o “reducer” para guardar os dados.

Para mostrar a lista em PostsScreen, nós precisamos ligar a função mapStateToProps com um seletor que irá fornecer essa parte do estado. Só assim, poderemos mostrar a lista re-usando nosso component ListView.

Como podemos ver, nada de novo! A implementação completa pode ser encontrada aqui.


Segunda página, próximo cenário — filtrando a lista de posts

Nesse cenário, nós mostraremos os filtros disponíveis para o usuário. Nós podemos buscar esses dados no topics "reducer”, utilizando um “selector” já existente. Quando um filtro for selecionado, nós iremos despachar uma ação que irá mudar o estado diretamente no posts “reducer”.

A parte interessante aqui, é aplicar o filtro na lista de posts. Na nossa estrutura do estado, nós temos postsById e currentFilter. Nós não queremos adicionar a lista filtrada no nosso estado, porque isso é um dado derivado.

Lógica de negócio para dado derivado deve ficar dentro dos “selectors” exatamente antes de irem para a “view”, na função mapStateToProps. Nosso “selector” então será:

A implementação completa pode ser encontrada aqui


Segunda página, último cenário — mostrando os detalhes do post

Esse cenário, na verdade, é o mais simples. Nós temos uma variável no nosso estado representado o currentPostId.

Tudo que nós precisamos fazer é atualizar essa variável quando o usuário clicar no post, e para fazer isso, nós despachamos uma ação.

PostsScreen precisa dessa variável para mostrar os detalhes do post atual, significa que, precisamos de um “selector” na função mapStateToProps.

A implementação completa pode ser encontrada aqui.


E chegamos ao fim!

E isso envolve toda a implementação e decisões da nossa aplicação!

O código fonte completo você pode encontrar no Github: https://github.com/wix/react-dataflow-example


Recapitulando as regras do nosso projeto

  • O estado da aplicação é um cidadão de primeira classe, estrutura seu estado como um banco de dados em memória.
  • “Smart Components” não devem conter nenhuma lógica de negócio, apenas despachar ações.
  • “Smart Components” devem acessar o estado, apenas, através dos “selectors”.
  • Reduza a lógica em seus “Smart Components”, extraindo elas em “Dumb Components”.
  • Mantenha toda sua lógica de negócio dentro de ações (“thunks”), “selectors” e “reducers”.
  • A camada de abstração de serviços, “services”, não deve conter estado algum.

Lembre-se, Redux oferece um grande espaço para um estilo pessoal de cada desenvolvedor.

Existem diferentes alternativas de fluxo de trabalho com diferentes regras.

Alguns bons amigos, me disseram que preferem usar redux-promise-middleware ao invés de “thunks” e também preferem manter toda a lógica de negócio apenas nos “reducers”.

Se você quiser compartilhar alguma metodologia diferente, que funcione para você, crie um PR com sua implementação para o projeto acima que vou adiciona-lo em uma nova branch para comparação.

Créditos


Coletividad é uma plataforma para as pessoas. Por mais oportunidades, experiências e escolhas melhores, na sua jornada individual, no Design, Tecnologia e Empreendedorismo.

Show your support

Clapping shows how much you appreciated Eduardo Rabelo’s story.