Esses eventos são enviados ao Controller, que atualiza o estado.
Como você percebeu, há várias setas indo e voltando em direções diferentes. Esse é o problema a ser resolvido.
Para complicar um pouquinho mais, alguns frameworks e libs introduziram o conceito de two-way data binding, o que significa que, você tem um estado armazenado no model e algum elemento de interface de usuário que corresponde diretamente a esse estado.
Ou seja, os dados fluem em ambas as direções.
O problema com o two-way data binding é que você se encontrava em situações onde a renderização do View era imprevisível.
Em alguns casos, com o view fazendo requests, o que piorava a situação.
Você pode ter race conditions e efeitos colaterais acontecendo na aplicação, e nem se dar conta disso.
O time de desenvolvimento do Facebook percebeu esse problema e decidiu criar uma nova arquitetura para gerenciar o estado de seus projetos.
Eles queriam abandonar o MVC, que era o padrão da indústria por décadas, e fazer algo completamente diferente.
Dado um mesmo input, o output sempre será o mesmo.
A view pode disparar ações. Ações são objetos. E esse objeto irá descrever o tipo da ação.
A ação vai para um "dispatcher", e esse dispatcher envia a ação para a store.
E tudo isso acontece num fluxo unidirecional. Não há listeners indo e voltando aqui. É fácil entender exatamente o que está acontecendo quando uma ação chega à store.
Agora, network requests, I/O assíncronos e cliques do usuário estão isolados da aplicação.
Ter efeitos colaterais isolados do restante da sua lógica de negócio é tudo o que você precisa para manter a previsibilidade em sua aplicação.
E aqui entra o Redux.
O Redux pega o padrão Flux e o leva ainda mais além.
Redux = flux + programação funcional
Programação funcional é você construir sua aplicação baseada em funções puras e componíveis.
E isso é o que o Redux faz com a arquitetura flux.
A forma com que o redux faz isso é através de reducers.
A maioria das coisas que você fará com o redux acontecerá em reducers functions.
Por exemplo, vamos imaginar um reducer de soma.
Streams nada mais são do que uma sequência de objetos.
Perceba que há dois casos diferentes neste reducer. Além do ADD case, temos um default case.
Mesmo que nada seja passado como argumento para um reducer, isso deve ser um shortcut para que o estado seja retornado.
Se o método reduce() é familiar para você, você será capaz de aplicar esse conhecimento do redux à muitas outras coisas. Mas, por que você iria querer fazer isso?
É como registros de transações. Imagine que sua conta bancária mantém um registro de cada transação que já aconteceu.
Você pode dar "rewind" e "replay" em seus actions objects, o que habilita features incríveis, como o time-travel debugging.
Alguns outros benefícios:
Digamos que um usuário reportou um bug em sua aplicação.
Você possui, por outro lado, uma collection de cada action que aconteceu na sessão daquele usuário.
Mutabilidade em uma aplicação joga fora o seu possível conhecimento em saber como um estado chegou até o momento em que ele se encontra.
🔶 Zero bugs de time-dependency
Ao consertar um bug em uma aplicação, talvez você perceba que há várias invocações de funções em sequência. Você tenta refatorá-las mas, ao mover uma função de um local para outro, as coisas param de funcionar de repente.
Quando a sua manipulação de estado é uma sequência de action objects, você pode simplesmente "rebobinar" essas ações.
Se você não manter um track de como o estado está sendo manipulado, é complicado fazer isso.
Para que qualquer uma dessas features funcione, é um requisito que reducers sejam a única "fonte da verdade" do estado de um app.
Todo o valor do redux consiste no fato de você ter um estado reproduzível e determinístico.
Isso é um valor inegociável e, na minha inocência, pensei que todas as pessoas que usam o redux saberiam disso.
Uma função pura, ao receber um mesmo input, sempre retornará o mesmo output.
Isso significa que o seu reducer não deve gerar ID's aleatórios e especificá-los como payload, por exemplo, rs.
Um reducer deve saber de nada, exceto o que você passa como argumento.
Reducers não devem disparar efeitos colaterais.
A busca pela reprodução determinística de estado é a razão fundamental pela qual Flux e Redux existem.
Essa action vai para um dispatcher, e o dispatcher envia a ação para os reducers.
Os reducers são os únicos responsáveis por tocar/fazer o update do estado.
Perceba que todas as setas do gráfico vão em apenas uma direção. É um circuito simples e fácil de entender.
Essa nova ação diz "terminei o que eu estava fazendo, coloque os novos dados da API na view".
Essa não é a única forma de isolar side effects. Middlewares e action creators não são obrigatórios.
O propósito de tudo isso é separar as responsabilidades. Você faz com que partes diferentes da sua aplicação sejam responsáveis por certas coisas.
Uma parte é responsável por renderizar a view e escutar eventos do usuário.
E uma outra parte separada é responsável por atualizar o estado.
Um dos grandes benefícios do Redux é que ele simplifica os testes unitários em sua aplicação.
Por que?
Por que não existe coisa mais fácil no mundo do que testar funções puras.
E você não precisa fazer mocking de muitas coisas.
Funções puras requerem menos mocking, por que elas funcionam isolada e independentemente.
Se você precisa fazer mockings complexos em uma aplicação, isso é um indicativo que ela possui acoplamento.
Se você tem testes simples, sem mocks, em que você apenas recebe um certo input e faz asserções esperando um certo output, é muito fácil fazer a implementação desse código.
No exemplo acima, testamos o valor cumulativo de múltiplas actions.
Em adição aos testes unitários, faça testes end-2-end.
Ferramentas como o Cypress permitirão que você simule o comportamento do usuário em sua aplicação, e isso testará de forma real a integração do projeto com o banco de dados, por exemplo.
Espero que essa introdução sobre o Redux tenha esclarecido um pouco do que o Redux é, como ele funciona e como ele pode ser uma ferramenta poderosa para você evoluir como programador(a).