Aprenda Redux em 15 Minutos

Por mais que afirmem o contrário, entender Redux é bem simples.

É sério - comparado com outros conceitos atuais, a filosofia por baixo dele é realmente bem tranquila.

O problema são as inúmeras terminologias e complexidades - as vezes desnecessárias - que são aplicadas a ele.

Quando entender qual é o motor que faz o Redux girar, tudo fica mais fácil.

E essa é minha ideia aqui - te mostrar que o Redux segue um padrão simples e fácil de entender.

Mas antes, você precisa entender o que é state.

1 - State

A ideia do Redux é controlar o estado - ou state - de uma aplicação.

Mas o que é state?

O state é a combinação de todos os dados de uma aplicação.

Por exemplo - em um app que mostra a temperatura de uma cidade ao usuário:

Perceba que o state é sempre a combinação dos dados que a aplicação possui. Como o exemplo é bem simples, só temos dois dados - cidade e temperatura.

Em aplicações maiores, a coisa muda.

Imagine o state do Instagram.

  • Usuário logado
  • Dados dos seguidores
  • Dados do feed

...E por aí vai.

É muita coisa.

Se não houver um cuidado em arquitetar esses dados, rapidamente a aplicação pode sair de controle.

Essa é a ideia do Redux - cuidar do state da aplicação.

E aí vem o primeiro detalhe:

Para o Redux, o state da aplicação nada mais é do que um objeto.

Esse é o primeiro conceito crucial.

O state da aplicação nada mais é do que um objeto.

Sim, ele é somente um objeto. A não ser que você tenha só um valor em seu state, aí o state será equivalente a esse valor (mas isso dificilmente acontecerá no mundo real).

No caso do app de temperatura, teríamos um objeto nesse estilo:

{
temperatura: "32",
cidade: "Tóquio"
}

A ideia é que esse objeto seja um "Single Source of Truth" (ou Fonte Única de Verdade) do state da aplicação - o que centraliza em somente um lugar todos os dados do app.

Isso garante que todos os locais da aplicação leiam os mesmos dados de forma sincronizada.

O Redux mantém o state (esse objeto com todos os dados) do app em algo chamado store.

Esse é o segundo conceito crucial.

Repita comigo - o state (que é a combinação dos dados de uma aplicação - tudo dentro de um objeto) vive na store.

A store é como se fosse uma "caixa" que guarda o state da aplicação.

Para abrir a caixa e modificar os dados (ou, modificar esse objeto), existem certas regras que você deve seguir.

É como se a store fosse uma biblioteca, e o state os livros da biblioteca. Quando alguém quer emprestar um livro, é necessário passar pelo sistema de regras da biblioteca para efetuar o empréstimo (e a devolução).

A primeira regra é que você somente pode ler o state, e não modificá-lo diretamente.

Pois é - caso você queira alterar algum dado no state - precisa avisar a store.

É burocrático - mas salva muita dor de cabeça.

Sempre que quiser modificar algo no state, precisa - segundo a terminologia do Redux - despachar uma ação indicando a intenção de modificar os dados.

Usando a analogia da biblioteca, é como se você tivesse que preencher um formulário avisando que quer emprestar (ou devolver) um livro.

Vamos quebrar isso melhor.

2 - Actions

A ação (ou action) é um objeto contendo duas coisas:

  • o tipo (que identifica a ação) - no caso da biblioteca, poderia ser EMPRESTAR_LIVRO ou DEVOLVER_LIVRO.
  • e um payload opcional utilizado para passar mais informações para a store (como por exemplo o nome do livro).

Para demonstrarmos a intenção de emprestar um livro, poderíamos ter um objeto nesse estilo:

const action = {
type: "EMPRESTAR_LIVRO",
payload: { nome: "Harry Potter" }
}

E para demonstrarmos a intenção de devolvermos um livro:

const action = {
type: "DEVOLVER_LIVRO",
payload: { nome: "Harry Potter" }
}

Logo abaixo, veja como a implementação hipotética de uma biblioteca ficaria.

Observe o painel de "Ações despachadas" e veja como - após a ação de "emprestar" ou "devolver" um livro - as actions refletem suas intenções.

Lista de Livros
Harry Potter
1984
Fahrenheit 451
Livros Emprestados
Ações Despachadas
State Atual
{
"livros": [
{
"id": 1,
"nome": "Harry Potter"
},
{
"id": 2,
"nome": "1984"
},
{
"id": 3,
"nome": "Fahrenheit 451"
}
],
"livrosEmprestados": []
}

É exatamente assim que uma aplicação com Redux funciona - a diferença é que em larga escala, o state seria muito maior.

Novamente - com essas informações, consegue imaginar como o state de uma aplicação como o Instagram pareceria?

Só há mais um detalhe - toda vez que fazemos dispatch em uma action, essa informação chega em um reducer.

Reducers unem o state e as actions.

3 - Reducers

Os reducers são funções puras que recebem o state atual (que é o objeto com os dados da aplicação) + uma ação (que é o objeto com type e payload) e retornam um novo state baseado nessa ação.

Em outros termos - os reducers atualizam o state.

Eles se parecem com isso aqui:

function reducer(state, action) {}

Funções "puras" são funções em que dado um certo input, haverá sempre o mesmo output. Além disso, elas não tem side-effects, ou seja, não modificam nada. No caso dos reducers, eles recebem o state e retornam um novo state - não modificando nada diretamente.

O assunto de funções puras é meio confuso - por isso vou mostrar alguns exemplos.

Uma função pura bem clássica é a de adição:

function somar(x, y) {
return x + y;
}

Percebe que - dado um mesmo input - essa função sempre retornará o mesmo output?

Além disso, veja que ela não modifica nada diretamente, apenas retorna um valor.

Outro exemplo seria uma função que calcula a área de um círculo:

function calcularArea(raio) {
return Math.PI * raio * raio;
}

Percebe que essa função tem a mesma característica? Não há side-effects e - dado um input - sempre haverá o mesmo output.

Voltando para o contexto do Redux.

Quando você clica em "emprestar" ou "devolver" um livro, existe um reducer que recebe a action (veja as Ações Despachadas - cada item listado lá é uma action) e o state do app (aquele objeto dentro que você vê em "State Atual").

Então, temos algo nesse estilo:

function bibliotecaReducer(state, action) {
switch (action.type) {
case EMPRESTAR_LIVRO:
return {
...state,
livrosEmprestados: [...state.livrosEmprestados, action.payload.nome],
livros: state.livros.filter(livro => livro.nome !== action.payload.nome)
};
case DEVOLVER_LIVRO:
return {
...state,
livrosEmprestados: state.livrosEmprestados.filter(livro => livro.nome !== action.payload.nome),
livros: [...state.livros, action.payload.nome]
};
default:
return state;
}
}

Clique em alguma linha do código para exibir explicações mais detalhadas.

... E essa é a ideia do Redux. Não há segredos.

  1. Uma ação é despachada;
  2. A ação chega no Reducer;
  3. O Reducer atualiza o state;
  4. Todos os locais da aplicação que consomem dados são atualizados;

Mas existe um outro conceito relevante: middlewares.

4 - Middlewares

Eu acho que os middlewares acabam sendo um dos pontos de maior confusão do Redux.

Mas a ideia é bem simples: Toda ação que é despachada passa por uma função antes de chegar no reducer.

Atualizando o fluxo descrito anteriormente, teríamos:

  1. Uma ação é despachada;
  2. A ação passa pelo middleware;
  3. A ação chega no Reducer;
  4. O Reducer atualiza o state;
  5. Todos os locais da aplicação que consomem dados são atualizados;

E qual é a utilidade disso?

  • Como os reducers são puros - isto é - não podem ter side-effects, podemos usar middlewares para realizar ações assíncronas (como requests para APIs) antes de chegarmos no reducer;
  • Podemos utilizar para analytics ou logging (por exemplo - avisar algum serviço toda vez que uma ação for despachada);
  • Também podemos realizar transformações no payload antes de ele chegar ao reducer.

São várias possibilidades.

5 - ... E o React?

Se você entendeu os 4 itens acima - você já sabe a filosofia do Redux e já consegue implementá-lo independentemente da tecnologia.

Sendo assim, integrar Redux com React é bastante fácil supondo que você já tenha uma experiência com React.

Não sou um grande fã de tutoriais especificos sobre integração de tecnologias pois geralmente ficam desatualizados rápido, mas como todo mundo aprende Redux para usar com React, acho que vale uma tentativa.

Mas antes, um detalhe que vale mencionar:

Hoje em dia existem 2 maneiras de utilizar Redux - sem RTK e com RTK.

O RTK (Redux Toolkit) é uma biblioteca oficial, opinionada que foi criada para facilitar a implementação do Redux com as melhores convenções - isso inclui alguns middlewares já configurados, simplificação de configurações, entre outros.

Mas você também pode utilizar o Redux com React sem o RTK e configurar tudo manualmente. Fica a seu critério.

Minha opinião - use o RTK. O RTK foi criado já pensando nas dificuldades que as pessoas tiveram ao longo dos anos usando Redux com React - e acelera muito o desenvolvimento.

Agora - sobre o código:

Vou utilizar um exemplo bem famoso - o contador. Ele é utilizado na documentação oficial do RTK: https://redux-toolkit.js.org/tutorials/quick-start.

Supondo que você já tenha um ambiente com React configurado, instale o Redux Toolkit e o react-redux:

npm install @reduxjs/toolkit react-redux

O RTK tem um conceito chamado slice. A ideia da slice é representar um "pedaço" do state do app - modularizando e isolando os reducers relacionados à um certo modulo.

Em resumo, o slice é utilizado para quebrar o state do app em arquivos separados.

Em projetos muito grandes, isso facilita muito.

Vamos criar um slice com os reducers do contador - pode ser em um arquivo chamado src/features/counterSlice.js:

import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'contador',
initialState: {
value: 0,
},
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
reset: state => {
state.value = 0;
},
},
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;

Clique em alguma linha do código para exibir explicações mais detalhadas.

Agora que temos o slice, configure a store (que receberá nossos reducers) - pode ser em um arquivo chamado src/store.js:

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});

No index.js do projeto - configure o Provider:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './store';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

E aí, criamos o component do counter em si - pode chamar de Counter.js

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset } from './features/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => dispatch(increment())}>Increase</button>
<button onClick={() => dispatch(decrement())}>Decrease</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
}
export default Counter;

Pronto, aí é só chamar o Counter:

<Counter />

Note como os conceitos não mudam muito - é a mesma coisa que você viu nos outros 4 itens.

A única coisa que irá mudar é o tanto de slices que você tem (ou middlewares, caso precise).

É isso.

-Rapozo