Entendendo Redux com ReactJS

engwilson

Wilson Carvalho

Posted on June 16, 2022

Entendendo Redux com ReactJS

Introdução

Aplicações modernas de front-end são constantemente controladas por estados que provocam renderizações e definem os comportamentos das suas telas. É comum termos que compartilhar estados entre vários componentes. Entretanto, em aplicações maiores, a comunicação desses estados entre os componentes começa a se tornar mais complexa, visto que, muitas vezes, precisaremos compartilhá-los por meio de props entre componentes distantes, fazendo com que todos os outros componentes que ligam esses dois tenham acesso a esses estados, sem necessariamente precisarem deles.

Este problema resulta em dificuldades de leitura e manutenção do código, tornando-o extremamente acoplado, com componentes dependentes uns dos outros. Este comportamento de compartilhar estados entre diversos componentes que não precisam dos mesmos, apenas por estarem no caminho para o componente final, é conhecido como Prop Drilling.

Exemplo de Prop Drilling

Ilustração do Prop Drilling. Fonte: Alura

Como resolver o Prop Drilling?

Neste artigo, utilizaremos um gerenciador de estados globais conhecido como Redux. Ele utiliza de um conceito chamado de Store para salvar todos os estados que você precisar em um único lugar que pode ser obtido a qualquer momento, em qualquer parte da sua aplicação.

Store fornecendo os estados para todos os componentes

Store fornecendo os estados para todos os componentes

Como podemos começar a usar?

Criaremos um projeto em ReactJS que funcionará como uma lista de tarefas, onde poderemos adicionar uma nova string ao final de um array utilizando um input e um botão.

De início, inicie seu projeto com;
yarn create react-app projeto-redux
ou
npx create-react-app projeto-redux

E instale as bibliotecas que serão necessárias:
cd projeto-redux
yarn add @reduxjs/toolkit redux react-redux
ou
npm install @reduxjs/toolkit redux react-redux

Lembre-se sempre de consultar a documentação oficial para conferir se houve alguma atualização.

Com as bibliotecas instaladas, daremos início à organização de pastas e arquivos. Recomendo criar um index.js dentro da pasta store, que também será criada dentro da pasta src do projeto.

Imagem de onde o arquivo index está localizado, dentro da pasta src e, depois, store

Organização dos arquivos do projeto

Em seguida, iremos criar a nossa Store, iniciando-a apenas com a estrutura que será utilizada.

// src/store/index.js

import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore();
Enter fullscreen mode Exit fullscreen mode

Para que toda a aplicação tenha acesso à Store com nossos estados, iremos englobar todo o app dentro de um componente que o React-Redux nos proporciona chamado Provider, que requer uma prop que será justamente a instância da Store que acabamos de criar.

//index.js

import React from "react";
import ReactDOM from "react-dom/client";
// Redux config
import { Provider } from "react-redux";
import { store } from "./store";

import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    /*
      O Provider é o responsável por disponibilizar a Store para 
      toda a aplicação
    */
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

Enter fullscreen mode Exit fullscreen mode

E agora, como utilizar a Store?

Por meio das Actions e Reducers que o Redux disponibiliza.

Fluxo de atividade do Redux

Fluxo de eventos do Redux

As Actions são funções que serão executadas e seu retorno será utilizado pelos Reducers para atualizarmos os nossos estados da Store. Sendo assim, é onde entrará qualquer lógica e requisição Http que queira fazer.

Ao final da execução da sua Action é necessário que, no seu retorno, seja disponibilizado um objeto com os valores que serão salvos no estado e um atributo type, sendo ele uma string com um valor único para cada Action, que será utilizado pelos Reducers como um identificador.

Vamos, então, criar o nosso arquivo action.js dentro da pasta store, junto ao nosso arquivo index.js anteriormente criado.

Essa Action receberá o nome da nova tarefa como parâmetro e retornará um objeto com o seu type único e a tarefa que será salva.

// src/store/actions.js

export function addTask(newTask) {
  return {
    type: 'ADD_TASK',
    newTask
  }
}
Enter fullscreen mode Exit fullscreen mode

Os Reducers são funções que utilizarão do retorno das Actions
como parâmetros para salvar os estados na Store. Ao invés de executarmos o Reducer como uma função comum, eles estão sempre ouvindo todas as Actions que estão sendo chamadas e, então, os Reducers identificarão o que fazer a partir de cada Action executadas. Como isso ocorre? A partir do atributo type que é retornado de todas as Actions. Se temos uma Action com type: "ADD_TASK", então teremos um Reducer que irá tomar uma ação a partir dessa string.

function myNewReducer(state, action) {
  /*
    switch(action.type) {
      case "ADD_TASK":
        // retornar o estado com o novo array atualizado
    }
  */
}
Enter fullscreen mode Exit fullscreen mode

Os Reducers sempre receberão 2 parâmetros: state, onde teremos os estados atuais da Store; e action, onde teremos todos os atributos retornados pela Action que foi executada.

Utilizando de uma condicional como o switch para definirmos qual ação será tomada por cada type, vamos utilizar o retorno da Action para definir como será o novo estado.

Da mesma forma que com as Actions, criaremos um reducers.js onde guardaremos todos os Reducers da nossa aplicação. O nome da função de cada Reducer será o nome do atributo como será salvo no objeto da nossa Store - se criarmos um Reducer chamado tasks, acessaremos esse valor futuramente como state.tasks.

Podemos, também, definir um estado inicial para nosso Reducer, para definirmos qual valor aquele estado terá antes de qualquer Action ser executada. Nesse caso, queremos que a lista de tarefas seja apenas um array vazio, que será preenchido com as tarefas que virão da Action.

// src/store/reducers.js

import { combineReducers } from "redux";

const initialState = { taskList: [] };

function tasks(state = initialState, action) {
  switch (action.type) {
    case "ADD_TASK":
      return { ...state, taskList: [...state.taskList, action.newTask] };
    default:
      return { ...state };
  }
}

export default combineReducers({
  tasks,
});
Enter fullscreen mode Exit fullscreen mode

Uma atenção especial para a sintaxe de como retornar o novo estado. Ele deverá utilizar os 3 pontos ... (chamados de spread operator) para copiarmos o estado atual, e depois alteramos apenas o que queremos. Dessa forma, o Redux identifica que houve uma alteração na Store e evita problemas de componentes não recebendo o estado atualizado. Detalhes mais profundos podem ser encontrados na documentação oficial.

Para uma melhor organização do código, unimos todos os Reducers em um único objeto usando o combineReducers() que será consumido pela Store.

A partir daqui, essa será nossa Store:

// src/store/index.js

import { configureStore } from "@reduxjs/toolkit";
import reducers from "./reducers";

export const store = configureStore({ reducer: reducers });

Enter fullscreen mode Exit fullscreen mode

Como unir esse fluxo com a nossa View?

Por meio do dispatch para executar as Actions e do selector(também chamado de subscribe) para acessarmos a Store e resgatarmos os estados que quisermos, para poder controlarmos as renderizações em tela.

Fluxo de atividade do Redux

Revisão do fluxo do Redux

Para começarmos a utilizar o dispatch, utilizaremos o Hook useDispatch disponibilizado pela biblioteca React-Redux que instalamos, e importaremos a Action que criamos, como no exemplo abaixo.

import { useDispatch } from "react-redux";
import { addTask } from './actions'

const dispatch = useDispatch();

dispatch(addTask('Prepare some coffee'))

Enter fullscreen mode Exit fullscreen mode

Respeitando as regras do Hooks, a condição para utilizarmos o Hook do useDispatch é que utilizemos o mesmo dentro de um Componente Funcional.

Para nossa aplicação, criaremos um componente de Home para testarmos nosso fluxo. Ele será um arquivo index.js dentro da pasta Home, que será o nome do nosso componente, e está dentro de uma pasta própria para páginas chamada pages, a fim de melhor organização dos arquivos.

Estrutura de pastas ilustrando o componente de Home sendo criado dentro de src e pages

Organização das pastas do projeto

Iniciaremos o componente apenas exportando-o e retornando uma tag div.

// src/pages/Home/index.js

import React from "react";

function Home() {
  return <div />;
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

Finalizamos com a importação do componente na raíz do nosso projeto, no arquivo App.js, que ficará da seguinte forma:

// App.js
import Home from "./pages/Home";

function App() {
  return <Home />;
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Agora que podemos respeitas a regra de uso de um Hook dentro de um componente funcional, daremos início à importação do useDispatch disponibilizado pela biblioteca React-Redux para adicionarmos uma nova tarefa.

import React, { useState } from "react";
import { useDispatch } from "react-redux";

import { addTask } from "../../store/actions";

function Home() {
  const dispatch = useDispatch();

  const handleAddTask = () => {
    dispatch(addTask('nova tarefa aqui'));
  };

  return (
    //...
  )
Enter fullscreen mode Exit fullscreen mode

Para podermos adicionar uma nova tarefa, utilizaremos de um estado derivado do Hook useState do próprio React para capturar o valor de uma tag input e executarmos o handleAddTask a partir do clique de uma tag button.

import React, { useState } from "react";
import { useDispatch } from "react-redux";

import { addTask } from "../../store/actions";

function Home() {
  const [newTask, setNewTask] = useState("");
  const dispatch = useDispatch();

  const handleAddTask = (e) => {
    /*
      Verificação para não adicionar tarefas vazias
    */
    if (newTask !== "") {
      dispatch(addTask(newTask));
    }

    /*
      Limpa o input assim que termina de adicionar a nova tarefa
    */
    setNewTask("");

    /* 
      Essa linha evitará que a página seja atualizada
      ao clicar no botão
    */
    e.preventDefault();
  };

  return (
    <main>
      <form action="">
        <input
          type="text"
          name="task"
          value={newTask}
          placeholder="Qual a próxima tarefa?"
          onChange={(e) => setNewTask(e.target.value)}
        />

        <button onClick={(e) => handleAddTask(e)}>Adicionar</button>
      </form>
    </main>
  );
}

export default Home;

Enter fullscreen mode Exit fullscreen mode

A partir de agora, já é possível adicionarmos novas tarefas à Store apenas com o input e o button. Com o dispatch finalizado, precisaremos obter o array de tarefas da store e renderizá-los em tela para disponibilizar a lista para o usuário. Aqui é onde entrará o Hook do useSelector também da biblioteca React-Redux.

import { useSelector, useDispatch } from "react-redux";

/*
  ...
*/

function Home() {
  /*
    Com o Hook declarado e tendo acesso à Store, basta
    definirmos quais elementos queremos obter. Nesse caso,
    queremos o elemento **taskList** que declaramos dentro do 
    Reducer **tasks**, e podemos obtê-lo da seguinte forma:
  */
  const { taskList } = useSelector((state) => state.tasks);
  /*
    ...
  */

  return (
    /*
      ...
    */
  );
}

export default Home;

Enter fullscreen mode Exit fullscreen mode

Estamos prontos para utilizarmos o array de tarefas como quisermos. Para nossa aplicação, será renderizada uma simples lista com as tags ul e li.

import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";

import { addTask } from "../../store/actions";

function Home() {
  const { taskList } = useSelector((state) => state.tasks);
  const [newTask, setNewTask] = useState("");
  const dispatch = useDispatch();

  const handleAddTask = (e) => {
    dispatch(addTask(newTask));

    e.preventDefault();
  };

  return (
    <main>
      <form action="">
        <label>Qual a próxima tarefa?</label>
        <input
          type="text"
          name="task"
          value={newTask}
          placeholder="Qual a próxima tarefa?"
          onChange={(e) => setNewTask(e.target.value)}
        />

        <button onClick={(e) => handleAddTask(e)}>Adicionar</button>
      </form>

      /*
        Para uma melhor UI, adicionaremos uma contagem de quantas
        tarefas temos adicionadas até o momento.
      */
      <span>Minhas tarefas - {taskList.length}</span>

      /*
        Verificação para só renderizar a lista de o taskList não
        estiver vazio.
      */
      {taskList.length > 0 && (
        <ul>
          {taskList.map((task) => (
            <li>{task}</li>
          ))}
        </ul>
      )}
    </main>
  );
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

Debug

Para que tenhamos uma melhor visão de como os estados estão se comportando durante a execução da aplicação, existem ferramentas de debug que podemos utilizar para facilitar essa visualização. A recomendação atual é a de instalar uma extensão no navegador chamada Redux Devtools.

Devtools na loja do Chrome

Extensão Redux Devtools

Ele será responsável por ouvir toda a sua aplicação e detalhar como está a árvore de estados dentro da Store, além de listar todas as Actions que foram disparadas e outras funcionalidades que não serão necessárias por agora.

Lista de Actions disparadas

Lista de Actions disparadas com o resultado do estado atual na Store

Resultado

Aplicação com lista de tarefas

Aplicação pronta

Para o resultado final do projeto, a construção do layout com CSS foi omitida de forma que nos preocupamos apenas com o funcionamento do Redux. É possível acessar o projeto no Github para ver o código-fonte da estilização utilizada por mim, mas sinta-se livre para estilizar da sua forma.

Conclusão

Com esse projeto, foi possível aprender quando usar Redux e qual a função dele dentro de uma aplicação. Passamos por todos os conceitos principais e construímos a base para tópicos mais complexos, como o Redux-Thunk, que será tema do próximo artigo.

Para reforçar o conteúdo, recomendo adicionar um desafio para criar uma Action que irá remover uma tarefa do array.

Me siga para acompanhar o lançamento dos novos conteúdos, fique à vontade para enviar qualquer dúvida ou feedback e lembre-se de curtir e compartilhar se gostou do artigo e lhe foi útil.

Nos vemos logo mais.

LinkedIn
Github
Twitter

💖 💪 🙅 🚩
engwilson
Wilson Carvalho

Posted on June 16, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related