Redux: desvendando a arquitetura com Flux
Origem
Em meados de 2016, período em que o React ainda estava escalando para o topo, como melhor framework, e desenvolvedores e desenvolvedoras ainda desbravaram a ferramenta e alguns problemas, que o React ainda não podia resolver, começaram a aparecer, o principal deles foi o Prop Drilling.
O Prop Drilling é um problema que pode ser replicado até hoje, e ele consiste em você usar os props para compartilhar informação(oes) entre um componente muito acima e um muito abaixo na cadeia de componentes, como representado abaixo:
Caso queira saber mais sobre Prop Drilling, temos um artigo sobre isso aqui na Alura.
Pois bem, como resolver esse problema??
Em 2014, o Facebook já havia criado uma arquitetura chamada arquitetura Flux, que é uma forma de escalar projetos React e resolver o problema de Prop Drilling adicionando um estado global e alimentando os componentes com ele, esse estado global é chamado de Store.
Beleza, a arquitetura tá aí, mas o que isso tem a ver?
Aí que o Redux entrou! O Redux foi criado em 2016 e teve sua primeira release lançada dia 2 de Junho de 2015.
O Redux utiliza essa arquitetura Flux ao pé da letra, para poder resolver esse problema de escalabilidade que o React tinha, mas ela pode ser utilizada em qualquer aplicação JS. Atualmente é recomendado utilizar o Redux Toolkit, que facilita a escrita e criação do redux, mas vamos criar um com redux 'puro' para que possamos entender como o redux funciona.
Conceito
O Redux trabalha com 4 termos principais, a View, as Actions, os Dispatcher, os Reducers e o Store, ficando no final isso daqui:
Vamos criar uma aplicação que consiste em um contador, o estado desse contador fica em um estado global utilizando redux e, enquanto criamos essa aplicação, explicaremos sobre as nomenclaturas.
No Creat React App rode o seguinte comando:
npx create-react-app contador-redux
Este comando irá criar uma pasta com a nossa aplicação, utilizando o nome contador-redux
. Após isso, precisamos instalar o redux na nossa aplicação e, como eu falei que ele funciona para qualquer aplicação JS, também precisamos instalar um pacote que nos permite utilizar o Redux no React, sendo assim, iremos escrever no terminal:
npm install redux react-redux
View
A View é onde adicionamos nossos componentes React, então é lá que adicionaremos o componente que tem o contador. Dentro de src/App.js
faremos isso:
import './App.css';
function App({ contador }) {
return (
<div className="App">
Contador: {contador}
</div>
);
}
export default App;
Actions
As Actions são ações que devem ser disparadas quando queremos que algo seja mudado no nosso estado global.
Vamos criar 2 ações, uma de incrementar e outra de decrementar. Para isso, vamos criar uma pasta actions e colocar um arquivo chamado contador.js
, onde ficarão todas as ações referentes ao contador.
Dentro do arquivo src/actions/contador.js
:
export const INCREMENTAR = 'CONTADOR::INCREMENTAR';
export const DECREMENTAR = CONTADOR::'DECREMENTAR';
export const incrementarContador = () => ({
type: INCREMENTAR
});
export const decrementarContador = () => ({
type: DECREMENTAR
})
Vemos que a Action é uma função que retorna um objeto, contando um type
e, caso precisássemos passar um valor nessa ação, poderíamos adicionar um payload
nesse objeto. Não são nomes obrigatórios, mas foram nomes que a comunidade adotou, o que faz bastante sentido, pois type
significa "tipo" (faz referência ao tipo da ação) e payload
significa "carga útil" (ou seja, o conteúdo que será útil naquela ação).
É possível perceber que o nome que damos às Actions está em letra maiúscula, isso é bem comum quando estamos lidando com constantes.
Também temos um CONTADOR::
no começo. Isso não é obrigatório, mas caso tenhamos muitas Actions no futuro esse prefixo nos ajudará a identificar qual estado estamos alterando, e também evita que haja alguma outra Action com o mesmo nome. Estamos exportando essas constantes, isso será útil nos Reducers.
Reducers
Os Reducers são 'pedaços' do nosso estado global (ou o nosso estado global inteiro caso tenhamos apenas um Reducer), que nos permite saber o valor atual do estado e também saber o que deve ser mudado de acordo com cada Action. Não precisamos necessariamente de mais de um Reducer, mas criaremos como se tivéssemos mais de um para mostrar que é possível. Para isso, criaremos uma pasta chamada reducers
, e dentro dela vamos gerar um arquivo chamado contador.js
, onde guardaremos o Reducer do nosso contador.
Dentro de src/reducers/contador
:
import { INCREMENTAR, DECREMENTAR } from '../actions/contador';
const initialState = {
contador: 0
}
const contadorReducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENTAR:
return {
...state,
contador: state.contador + 1,
};
case DECREMENTAR:
return {
...state,
contador: state.contador - 1,
};
default:
return state;
}
};
export default contadorReducer;
Nota-se que o Reducer é uma função que recebe 2 valores, um é o estado atual dessa parte do estado global, o que chamamos de state
, e o outro é a Action que vamos enviar para ele, que chamamos de action
mesmo. Podemos ver aqui a importância de se exportar o nome que demos às Actions também, assim não precisamos ficar reescrevendo, isso evita erros de escrita.
O retorno do Redux Sempre deve ser o estado atual, pois é o retorno que o estado global utiliza para poder concatenar essas partes dos estados.
Também precisamos dar um valor inicial ao estado, pois o valor inicial dele não pode ser undefined
, por isso criamos uma constante initialState
, pois caso o state
não tenha um valor inicial, ele terá o valor dessa constante.
Store
Store é o estado global em si, então precisamos criar o Store e concatenar os Reducers nele, vamos criar então uma pasta store
e criar um arquivo index.js
nele.
Dentro de src/store/index.js
:
import { createStore, combineReducers } from "redux";
import contadorReducer from "../reducers/contador";
const reducers = combineReducers({ contadorReducer });
const store = createStore(reducers);
export default store;
Aqui temos nosso primeiro contato com algumas funções do redux, o createStore
e o combineReducers
. O createStore
cria o nosso estado global e o combineReducers
concatena quantos Reducers quisermos em um só estado global, no nosso caso só temos o contadorReducer
, então manteremos somente ele.
Nosso estado global agora está criado e será exportado dentro dessa constante store
, mas para que a nossa aplicação fique ciente dela, precisamos prover esse Store.
Dentro de src/index.js
, vamos fazer isso aqui:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Aqui temos o nosso primeiro contato com o react-redux
, ele nos provê esse componente Provider
, que nos permite conectar o estado global a nossa View.
Ok, já está tudo configurado, mas como se usa isso?
Para podermos utilizar o Redux, precisamos executar uma Action
Dispatcher
Dispatch
é despachar ou enviar em inglês, ou seja, se falarmos Dispatch an action estamos falando em Enviar uma ação, e é exatamente isso que precisamos.
Dentro de src/App.js
:
import './App.css';
import { connect } from 'react-redux'
import { decrementarContador, incrementarContador } from './actions/contador';
function App({ contador, incrementar, decrementar }) {
return (
<>
<div className="App">Contador: {contador}</div>
<button onClick={incrementar}>Incrementar</button>
<button onClick={decrementar}>Decrementar</button>
</>
);
}
const mapStateToProps = state => ({
contador: state.contadorReducer.contador,
});
const mapDispatchToProps = (dispatch) => ({
incrementar: () => dispatch(incrementarContador()),
decrementar: () => dispatch(decrementarContador()),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
Aqui temos nosso segundo contato com o react-redux, agora com a função connect
. A função connect
conecta (AHÁ) os estados e os DISPATCHERS ao componente, sendo a sintaxe connect(estadoQueDesejaUsar, dispatchersQueDesejaUsar)(ComponenteQueDesejaConectar)
.
A nomenclatura mapStateToProps
e mapDispatchToProps
também é muito difundida na comunidade, e se trata de uma função que recebe um state como parâmetro e esse state é todo o estado global da nossa aplicação. Como precisamos apenas do contador nesse componente, retornamos apenas o contador deste estado.
A função mapDispatchToProps
também é uma função, porém ela recebe como parâmetro a função dispatch
, que nos permite enviar a ação como falamos anteriormente.
As duas funções normalmente retornam um objeto e ele pode ser recuperado via props no próprio componente que utilizamos dentro do connect, ou seja, como temos o contador
dentro do objeto do retorno do mapStateToProps
e incrementar
e decrementar
dentro do objeto de retorno do mapDispatchToProps
, todos eles estão disponíveis via props no componente!
Neste artigo conseguimos utilizar o Redux do início ao fim!
Aprendemos as nomenclaturas mais utilizadas pela comunidade e o que está por trás dessa ferramenta!
Bons estudos!