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.
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.
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.
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();
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>
);
E agora, como utilizar a Store?
Por meio das Actions e Reducers que o Redux disponibiliza.
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
}
}
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
}
*/
}
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,
});
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 });
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.
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'))
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.
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;
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;
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 (
//...
)
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;
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;
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;
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.
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.
Resultado
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.
Top comments (1)
Muito bom!