Como Redux funciona?
Artigo original escrito por: Dave Ceddia neste link https://daveceddia.com/how-does-redux-work/
Depois de aprender um pouco sobre React e entrar no Redux, é realmente confuso como tudo funciona.
Actions, reducers, action creators, middlewares, funções puras, imutabilidade…
A maioria destes termos parecem totalmente desconhecidos.
Neste post nós vamos desmistificar como Redux funciona, vamos dar um passo para trás que eu acho que nos ajudará a entendê-lo melhor.
Se você ainda não tem certeza sobre o que é o Redux ou porque você deve usá-lo, leia esta publicação primeiro e volte e continue lendo este post.
Primeiro vamos falar sobre o Estado
O conceito de Estado vem da tal “Máquina de Estado Finita” ou (FSM-Finish State Machine) ou autômato finito é um modelo matemático usado para representar programas de computadores ou circuitos lógicos. O conceito é concebido como uma máquina abstrata que deve estar em um de um número finito de estados.
Iremos inicial com um exemplo de alteração de estado do React, e depois adicionar o Redux peça por peça.
Aqui temos um exemplo de contador, (desconsiderei o CSS para manter isso simples)
Como uma revisão rápida, veja como isso funciona:
- O estado inicial de
count
é armazenado no componente no nível superior - Quando o usuário clica no botão “+” o
onClick
é chamado, que é vinculado ao métodoincrement
- O método
increment
atualiza o estado com um nova contagem - Porque o estado foi alterado o React re-renderiza o componente (e seus filhos), e um novo valor de contagem é exibida.
Se você precisa de mais detalhes sobre como as mudanças de estado funcionam, leia A Visual Guide to State in React e então volte e continue daqui. Sério se o código acima não esta claro pra você, você precisa saber como as mudanças de estado do React funcionam antes de aprender Redux.
Então sem mais delongas vamos a configuração básica da nossa app. Se você quiser seguir com o código, crie um projeto agora:
- Instale o
create-react-app
se você não tem elenpm install -g create-react-app
- Crie um projeto:
create-react-app redux-intro
- Abra
src/App.js
e substitua por isso:
- Crie o
src/Counter.js
com o exemplo de código acima - Abra o
src/index.js
e substitua por isso:
Redux
Conforme discutido na Parte 1, o Redux mantém o estado do seu aplicativo em uma única store
. Então, você pode extrair partes desse estado e conectá-los em seus componentes como propriedades. Isso permite que você mantenha os dados em um lugar global (a store
) e alimenta-los diretamente para qualquer componente no aplicativo, sem a ginástica de passar props em vários níveis.
Muitas vezes você verá as palavras
state
estore
usadas interativamente. Tecnicamente, ostate
é o dado, e astore
é onde ele é mantido.
Ao seguir as etapas abaixo, siga o seu editor, isso irá ajudá-lo a entender como isso funciona (e nós vamos trabalhar juntos com alguns erros juntos)
Bora adicionar o Redux ao projeto:
npm install --save redux react-redux
redux vs react-redux
Espera, duas bibliotecas? “O que é react-redux” você sabe? Bem, eu tenho mentido pra você.
Veja redux
dá-lhe uma store
, e permite que você:
- Mantenha o estado.
- Obtenha o estado.
- Responda quando o estado muda.
Mas o redux
faz isso tudo o que faz o react-redux
?. Na verdade é o react-redux
que permite conectar partes do estado aos componentes React. Isso mesmo o redux
não sabe nada sobre React.
Essas bibliotecas são como duas ervilhas em um pote. 99,999% do tempo, quando alguém menciona “Redux” no contexto de React, eles estão se referindo a ambas as bibliotecas em conjunto. Portanto, tenha isso em mente quando você vê o Redux mencionado em StackOverflow, ou Reddit ou em outro lugar.
Ultimas coisa
A maioria dos tutoriais começa criando uma store
, configurando o Redux, redigindo um reducer
e assim por diante.
Eu vou tomar uma abordagem voltando um passo atrás, e será preciso código para fazer as coisas aparecerem na tela, mas espero que a motivação por trás de cada passo seja mais clara.
Voltando ao Redux
De volta ao aplicativo Counter, vamos imaginar por um segundo que mudamos o estado do componente para Redux
Vamos remover o estado do componente, já que obteremos isso do Redux em breve:
Conectando o Counter
Note que {this.state.count}
foi modificado para {this.props.count}
isto ainda não funcionará, é claro, porque o Counter não esta recebendo a propriedade count
. Vamos usar o Redux para injetar isso.
Para obter a contagem do Redux, primeiro precisamos importar a função de conexão na parte superior:
import { connect } from 'react-redux'
Então precisamos conectar o componente Counter ao Redux na parte inferior, mas antes, para isso utilizaremos Containers Components.
Containers Components tem a responsabilidade de resolver toda a lógica externa ao componente e em seguida processar o subcomponente.
Isso irá lançar um erro (falaremos ele mais pra frente)
Agora utilizamos o connect
para conectar o Redux com o componente Counter.
Porque connect
?
Você pode notar que a ligação parece paquena… estranha. Porque connect(mapStateToProps)(Counter)
e não connect(mapStateToProps, Counter)
ou connect(Counter, mapStateToProps)
? O que esta fazendo?
É escrito assim porque connect
é uma higher-order function, que é uma maneira elegante de dizer que retorna uma função quando você a chama. Ao chamar essa função com um componente, retorna um novo componente (envolvido).
Nos casos onde utilizamos os containers um outro nome para ele é um higher-order component (aka “HOC”). HOCs São componentes que retornam outros componentes (envolvidos).
O que faz o connect
é ligar para o Redux, retirar o estado inteiro e passá-lo através da função mapStateToProps
. Isso precisa ser uma função personalizada porque somente você conhecerá a “forma” do estado no Redux.
connect
passa todo o estado como se dissesse: “Ei, me conte o que você precisa com essa confusão confusa”.
O objeto que você retorna do mapStateToProps
é alimentado em seu componente como propriedades (props).
O exemplo acima passará state.count
como o valor da prop count
: As chaves do objeto se tornam nomes de propriedades e seus valores correspondentes se tornam os valores dos propriedades. Então, você vê, esta função define literalmente um mapeamento do estado em propriedades.
Sua sensação agora deve algo como a imagem abaixo (rsrs)
Erros
Could not find “store” in either the context or props of “Connect(Counter)”
Uma vez que o connect
tira os dados da store
e não configuramos uma Redux store
ou informamos ao aplicativo como encontrá-lo, esse erro é bastante lógico. Redux não tem idéia do que esta acontecendo agora.
Fornecendo uma Redux store
O Redux mantém o estado global para todo o aplicativo e envolvendo todo o aplicativo com o componente Provider
do react-redux
, todos os componentes da árvore do aplicativo poderão usar a conexão para acessar a store
do Redux se assim o desejar.
Isso significa que a App e filhos de App (como Counter) e filhos de seus filhos, e assim por diante, todos agora podem acessar a store
do Redux, mas, apenas se eles são explicitamente envolvidos pelo connect
para se conectar. Não estou dizendo para conectar cada componente, seria uma má idéia (design desarrumado e lento também).
O Provider
pode parecer uma magia total agora. É um pouco; ele realmente usa o recurso “contexto” do React sob o capô.
É como uma passagem secreta conectada a cada componente, e o uso da conexão abre a porta para a passagem.
Imagine merter xarope em uma pilha de panquecas, e como ele consegue entrar em TODAS as panquecas, mesmo que você simplesmente derramou na parte superior. O Provider
faz isso com o Redux.
Em src/index.js
, importe o Provider e envolva o conteúdo do App
com ele.
Nós ainda continuamos teaderendo erro, é porque o Provider
precisa de uma store
para trabalhar.
Criar uma store
O Redux vem com uma função útil que cria store's
, e isso é chamado createStore
. Sim. Vamos fazer uma store
e passá-lo para o Provider
:
Outro erro, mas diferente desta vez:
Expected the reducer to be a function.
Então, aqui está um assunto sobre Redux: não é muito inteligente. Você pode esperar que ao criar uma loja, isso lhe daria um valor padrão inicial para o estado dentro dessa store
. Talvez um objeto vazio?
Mas não: Redux faz zero pressuposições sobre a forma do seu estado. Você decide! Pode ser um objeto, um número ou uma string, ou o que você precisa. Portanto, temos que fornecer uma função que retornará o estado. Essa função é chamada de reducer
(veremos por que em um minuto). Então, vamos fazer o mais simples possível, passá-lo para createStore
e veja o que acontece:
O reducer
deve sempre retornar algo.
O erro é diferente agora:
Cannot read property ‘count’ of undefined
Está quebrando porque estamos tentando acessar state.count, mas o estado é indefinido. O Redux esperava que nossa função redutora retornasse um valor para o estado, exceto que (implicitamente) retornou indefinido. As coisas estão corretamente quebradas.
É esperado que o reducer
deve receba o estado atual e retornar um novo estado, mas nunca importa; voltaremos a isso.
Vamos fazer o reducer
retornar algo que corresponde à forma que precisamos: um objeto com uma propriedade de count
.
Ei! Funciona! A contagem agora aparece como “42”. Impressionante.
Apenas uma coisa: o count
será sempre 42.
A história até agora
Antes de entrar em como atualizar o contador, vejamos o que fizemos até agora:
- Nós escrevemos uma função mapStateToProps que faz o que o nome diz: transforma o estado Redux em um objeto contendo propriedades.
- Nós conectamos a store do Redux ao nosso componente Counter com a função
connect
do react-redux, usando a função mapStateToProps para configurar como a conexão funciona. - Criamos uma função redutora para dizer ao Redux como nosso estado deveria ser.
- Utilizamos a função createStore engenhosamente chamada para criar uma store e passamos o reducer.
- Nós embalamos todo o nosso aplicativo no componente
Provider
que vem com react-redux, e passou a nossastore
como suporte. - O aplicativo funciona perfeitamente, exceto o fato de que o contador está preso às 42.
Comigo até agora?
Interatividade (Fazendo-o funcionar)
Até agora isso é muito coxo, eu sei. Você poderia ter escrito uma página HTML estática com o número “42” e 2 botões quebrados em 60 segundos, mas aqui está, lendo como complicar demais essa mesma coisa com React e Redux e quem sabe o que mais.
Eu prometo que esta próxima seção fará tudo valer a pena.
Na verdade não. Retiro o que eu disse. Um simples app contador é uma excelente ferramenta de ensino, mas o Redux é absolutamente exagerado para algo assim. O estado do React está perfeitamente bom para algo tão simples. Heck, mesmo o JS simples funcionaria muito bem. Escolha a ferramenta certa para o trabalho. Redux nem sempre é essa ferramenta. Mas eu divago.
Estado inicial
Então precisamos de uma maneira de dizer ao Redux que altere o Counter.
Lembre-se da função redutora que escrevemos?
Lembre-se de como mencionei que leva o estado atual e retorna o novo estado? Bem, eu menti. Ele realmente leva o estado atual e uma ação, e então ele retorna o novo estado. Nós devemos escrever assim:
A primeira vez que Redux chama essa função, ela passará indefinida como o estado. Essa é a sua sugestão para retornar o estado inicial. Para nós, provavelmente é um objeto com uma contagem de 0.
É comum escrever o estado inicial acima do redutor e usar o recurso de argumento padrão do ES6 para fornecer um valor para o argumento do estado quando ele é indefinido.
Experimente isso. Ainda deve funcionar, exceto que agora o contador está preso em 0 em vez de 42.
Action
Estamos finalmente prontos para falar sobre o parâmetro de action
. O que é isso? De onde isso vem? Como podemos usá-lo para mudar o maldito contador?
Uma “action” é um objeto JS que descreve uma mudança que queremos fazer. O único requisito é que o objeto precisa ter uma propriedade de type
, e seu valor deve ser uma string
. Aqui está um exemplo de action
:
{ type: "INCREMENT" }
Aqui está mais um:
{ type: "DECREMENT" }
As engrenagens estão virando na sua cabeça? Você sabe o que vamos fazer depois?
Responda às actions
Lembre-se do trabalho do reducer
é levar o estado atual e uma action
e descobrir o novo estado. Então, se o reducer
recebeu uma action
como { type: "INCREMENT"}
, o que você pode querer retornar como o novo estado?
Se você respondeu algo assim, você está no caminho certo:
É comum usar uma instrução switch com casos para cada action
que você deseja manipular. Mude o seu reducer
para se parecer com isto:
Sempre devolva um estado!
Você notará que sempre há o caso de retorno onde tudo o que faz é retornar o estado. Isso é importante, porque o Redux pode (vai) chamar seu reducer
com actions
que não sabe o que fazer. Na verdade, a primeira action
que você receberá será {type: "@@redux/INIT"}
. Tente colocar um console.log (ação) acima do switch e veja.
Lembre-se de que o trabalho do reducer
é retornar um novo estado, mesmo que esse estado seja inalterado. Você nunca quer passar de “ter um estado” para “estado = undefined”, certo? Isso é o que aconteceria se você deixasse o caso padrão. Não faça isso.
Nunca mude o estado
Mais uma coisa para nunca fazer: não mude o estado. O Estado é imutável. Você nunca deve mudá-lo. Isso significa que você não pode fazer isso:
Você também não pode fazer coisas como state.foo = 7
, ou state.items.push(newItem)
.
Pense nisso como um jogo onde a única coisa que você pode fazer é retornar {…}. É um jogo divertido. Enlouquece um pouco no início, mas você vai melhorar com a prática.
Todas estas regras …
Sempre devolva um estado, nunca mude de estado, não conecte todos os componentes, coma seu brócolis, não fique fora do passado 11 … é cansativo. É como uma fábrica de regras, e nem sei o que é isso.
Sim, o Redux pode ser como um pai dominador. Mas vem de um lugar de amor. Amor de Programação funcional.
Redux é construído sobre a idéia de imutabilidade, porque o estado global mutante é a estrada para a ruína.
Você já manteve um objeto global e usou isso para passar o estado em torno de um aplicativo? Isso funciona bem, legal e fácil. E então, o estado começa a mudar de formas imprevisíveis e torna-se impossível encontrar o código que está mudando.
O Redux evita esses problemas com algumas regras simples. O estado é somente leitura, e as ações são a única maneira de modificá-lo. As mudanças acontecem de um jeito e de uma maneira: action -> reducer -> novo estado. A função reducer deve ser “pura” — não pode modificar seus argumentos.
Existem ainda pacotes de complemento que permitem que você registre todas as ações que ocorrem, rebobinam e reproduzam, e qualquer outra coisa que você possa imaginar. O debugging de viagem no tempo foi uma das motivações originais para a criação do Redux.
De onde são as actions?
Uma parte deste enigma permanece: precisamos de uma maneira de alimentar uma action em nossa função redutora para que possamos incrementar e decrementar o contador.
As actions não nascem, mas são despachadas, com uma função útil chamada dispatch
.
A função dispatch
é fornecida pela instância da store
do Redux. Ou seja, você não pode simplesmente import {dispatch}
e estar a caminho. Você pode chamar store.dispatch(action)
, mas isso não é muito conveniente, já que a instância da store
está disponível apenas em um arquivo.
Além de injetar o resultado do mapStateToProps como propriedades, conecte também a função de dispatch
como suporte. E com esse pouco de conhecimento, podemos finalmente recuperar o contador.
Aqui está o componente final em toda a sua glória. Se você acompanhou, as únicas coisas que mudaram são as implementações de incremento e decréscimo: eles agora chamam o suporte de dispatch
passando uma action
.
O código para todo o projeto (todos os dois arquivos dele) pode ser encontrado no Github.
Stateless component
Como o componente já não manipula mais o estado diretamente podemos isolar a lógica para dispachar as actions no container, lembram da responsabilidade dos containers?
Containers Components tem a responsabilidade de resolver toda a lógica externa ao componente e em seguida processar o subcomponente.
Vamos implementar o componente Counter da seguinte forma:
Vamos entender o que esta acontecendo aqui:
- Criamos uma constante
mapDispatchToProps
que é uma função anonima que recebe um parâmetro chamadodispatch
, que por sua vez ela retorna um objeto onde cada chave do objeto recebe uma outra função anonima que tem uma única responsabilidade “dispachar uma action” - Cada chave do objeto
mapDispatchToProps
será mapeada no connect e passará para o componente como propriedade, assim como omapStateToProps
. - Ao executar o
connect(mapStateToProps, mapDispatchToProps)
ele passará o métododispatch
dastore
como parâmetro da funçãomapDispatchToProps
.
Esta não é a melhor forma de se dispachar uma action, o mais correto seria utilizando os “actionCreators”
Agora o componente Counter
E agora?
Com o aplicativo Counter em mão, você está bem equipado para saber mais sobre o Redux.
“O que?! Tem mais?!”
Há muitas coisas que não abordamos aqui, com a esperança de tornar este guia facilmente digerível —action constants, actionCreators, middleware, thunks e chamadas assíncronas, seletores e assim por diante. Os documentos do Redux estão bem escritos e cobrem tudo isso e muito mais.
Mas você tem a idéia básica agora. Espero que você entenda como os dados fluem no Redux (dispatch (action) -> reducer -> new state -> re-render), e o que um reducer faz, e que action é, e como isso tudo se encaixa.
Mais uma vez não posso deixar de citar o artigo original escrito por: Dave Ceddia neste link https://daveceddia.com/how-does-redux-work/
Até mais.