Redux: desvendando a arquitetura com Flux

Redux: desvendando a arquitetura com Flux
Luiz Fernando Ribeiro
Luiz Fernando Ribeiro

Compartilhe

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:

Fluxograma com 5 retangulos em formato Stadium (com as extremidades levemente arredondadas), um em baixo do outro, com uma seta apontando do retângulo de cima para o retângulo de baixo

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 podemos 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:

Um fluxograma mostrando o fluxo do Redux, começando pelo View, Actions, Dispatcher, Reducer, Store, State e após isso voltando para o View

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!

Veja outros artigos sobre Front-end