
O Redux é uma ferramenta fundamental em grande parte dos APPs construídos com React, por tanto dominar o uso dele é extremamente importante 😁
Neste artigo você vai aprender os fundamentos do Redux na prática criando uma página de loja de livros do zero.
Bora começar essa jornada?
O que é Redux?
Redux é uma biblioteca JavaScript criada por Dan Abramov e Andrew Clark (inspirada na arquitetura Flux do Facebook) que tem como objetivo gerenciar estados globais da nossa aplicação front-end, seguindo a arquitetura Flux (vamos ver adiante).
Dessa forma o Redux ficará responsável por:
- Retornar os estados globais solicitados pelos components;
- Abstrair a lógica de negócio dos estados, disponibilizando uma interface simples para os componentes requisitarem algumas ações sobre os estados, sem se preocuparem de como isso será feito;
- Manter os componentes atualizados, sempre que os estados forem alterados.
Ou seja, ele resolve os problemas de compartilhamento de estados globais e cria uma interface simples para os componentes utilizarem.
Podemos ter como exemplo o carrinho de compras e perfil de usuário, que são dados utilizados por vários components.
O que vamos criar?
Para entender como o Redux nos auxilia, vamos criar uma aplicação web de uma loja virtual de livros, chamada de OneBitBooks.
Neste app, utilizaremos o Redux para gerenciar o estado do carrinho de compras, permitindo que várias páginas e components tenham acesso de forma simples aos dados deste estado.
Página de listagem de livros:
Página de detalhes:
Para acessar os códigos completos do projeto: https://github.com/OneBitCodeBlog/onebitbooks
O que vamos usar:
- Node.js
- React.js
- Axios
- Json Server
- Redux
- React Router Dom
- React Icons
Passo a Passo da criação do APP
Criando projeto
1 – Para criar nosso projeto, execute o comando:
1 |
npx create-react-app onebitbooks |
Limpando projeto
Após isso, vamos limpar o nosso projeto.
1- Primeiro remova os seguintes arquivos que não vamos utilizar:
- public/favicon.ico
- public/logo192.png
- public/logo512.png
- public/manifest.json
- public/robots.txt
- src/App.test.js
- src/App.css
- src/logo.svg
- src/serviceWorker.js
- src/setupTests.js
Em seguida, vamos limpar o conteúdo de alguns arquivos:
2 – No arquivo public/index.html, deixe-o da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 |
<!DOCTYPE html> <html lang="pt-br"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>OneBitBooks</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html> |
3 – No src/App.js vamos deixá-lo dessa forma:
1 2 3 4 5 6 7 8 9 |
import React from 'react'; function App() { return ( <h1>OneBitBooks</h1> ); } export default App; |
4 – No src/index.css vamos deixar em branco para colocar nossa própria estilização global mais para frente.
5 – No src/index.js deixe-o da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 |
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); |
Configurando rotas
Neste projeto teremos duas páginas, a Home que é mostrado a lista de produtos e Cart que é a página do carrinho com todos os itens.
1 – Para isso crie a seguinte estrutura:
Para que não tenhamos problemas na configuração da rota, precisamos ter um componente básico criado em cada página.
2 – No src/pages/Cart/index.js poderá deixar da seguinte forma:
1 2 3 4 5 6 7 8 9 |
import React from 'react'; import './styles.css'; export default function Cart() { return ( <h1>Cart</h1> ); } |
3 – No src/pages/Home/index.js poderá deixar da seguinte forma:
1 2 3 4 5 6 7 8 9 |
import React from 'react'; import './styles.css'; export default function Home() { return ( <h1>Home</h1> ); } |
4 – Instale o React Router Dom, que será responsável pelo roteamento das páginas:
1 |
yarn add react-router-dom |
5 – Crie o arquivo src/routes.js que terá as configurações das nossas rotas, ele ficará da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React from 'react'; import { Route, Switch } from 'react-router-dom'; import Home from './pages/Home'; import Cart from './pages/Cart'; export default function Routes() { return ( <Switch> <Route path="/" exact component={Home} /> <Route path="/cart" exact component={Cart} /> </Switch> ); } |
6 – Em seguida no src/App.js, ao invés de renderizar a página diretamente, vamos chamar o nosso arquivo de rotas, que ficará responsável por decidir qual página será renderizada.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React from 'react'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; import Routes from './routes'; function App() { return ( <BrowserRouter> <Routes /> </BrowserRouter> ); } export default App; |
Após isso já vamos conseguir alternar entre as páginas.
Configurando o layout
Global
Vamos começar fazendo uma estilização que servirá para todas as páginas, resetando alguns valores padrões dos navegadores, definindo a cor do fundo, fonte, etc.
1 – Para isso modifique o arquivo src/index.css da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
* { margin: 0; padding: 0; outline: 0; box-sizing: border-box; } html, body, #root { height: 100%; } body { font: 400 14px Roboto, sans-serif; background: #F0f0f5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } input, button, textarea { font: 400 18px Roboto, sans-serif; } button { cursor: pointer; } .container { max-width: 1020px; margin: 64px auto; padding: 0 20px; } |
Header
Como o Header é algo que será o mesmo nas duas páginas, será mais interessante criá-lo como um componet.
Antes de criar diretamente o component, vamos adicionar ao projeto seus recursos externos.
1 – Para baixar o logo, acesse https://www.flaticon.com/free-icon/book_710330?term=book&page=1&position=11 e escolha este ícone na cor #FFFFFF e tamanho 32px.
2 – Em seguida crie a pasta src/assets e coloque dentro o ícone que acabamos de baixar.
3 – Em seguida instale o pacote de ícones do React:
1 |
yarn add react-icons |
Agora sim podemos criar nosso component.
4 – Crie a pasta src/components/Header e dentro crie os arquivos index.js e styles.css.
Vamos começar com a estrutura da tela e já utilizar as nossas rotas.
5 – No src/components/Header/index.js o código ficará desta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import React from 'react'; import { Link } from 'react-router-dom'; import { FiShoppingBag } from 'react-icons/fi'; import './styles.css'; import logo from '../../assets/book.png'; export default function Header() {; return ( <header className="header"> <Link to="/" className="logo"> <img className="logo-icon" src={logo} alt="Rocketshoes" /> <span className="logo-text">OneBitBooks</span> </Link> <Link to="/cart" className="header-cart"> <div> <strong>Sacola</strong> <span> <strong>4</strong> livros </span> </div> <FiShoppingBag size={36} color="#FFF" /> </Link> </header> ); } |
Agora vamos para a estilização do component.
6 – No src/components/Header/styles.css o código ficará desta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
.header { display: flex; justify-content: space-between; align-items: center; background: #33BFCB; padding: 16px 128px; } .logo { display: flex; align-items: center; text-decoration: none; transition: opacity 0.5s; } .logo:hover { opacity: 0.5; } .logo-icon { height: 32px; } .logo-text { margin-left: 8px; color: #FFF; font-size: 20px; font-weight: bold; } .header-cart { display: flex; align-items: center; text-decoration: none; transition: opacity 0.5s; } .header-cart:hover { opacity: 0.5; } .header-cart div { text-align: right; margin-right: 10px; } .header-cart div > strong { display: block; color: #B6EDF2; } .header-cart div span { font-size: 12px; color: #FFF; font-weight: bold; } .header-cart div span strong { font-size: 12px; color: #FFF; } |
7 – Com o nosso Header criado, vamos disponibilizá-lo em todas as telas, para isto basta inserí-lo no src/App.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React from 'react'; import { BrowserRouter } from 'react-router-dom'; import Routes from './routes'; import Header from './components/Header'; function App() { return ( <BrowserRouter> <Header /> <Routes /> </BrowserRouter> ); } export default App; |
Página Home
Esta é a página onde vamos listar todos os livros a venda.
Vamos começar pela estrutura da página, com alguns dados fixos por enquanto.
1 – No arquivo src/pages/Home/index.js deixe-o da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
import React from 'react'; import { FiShoppingBag } from 'react-icons/fi'; import './styles.css'; export default function Home() { return ( <main className="container"> <ul className="book-catalog"> <li className="book-container"> <img src="https://images-na.ssl-images-amazon.com/images/I/51w53T12s8L.jpg" alt="JavaScript: O Guia Definitivo" /> <strong>JavaScript: O Guia Definitivo</strong> <span>R$ 146,08</span> <button type="button" onClick={() => {}}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} 0 </div> <span>Adiconar</span> </button> </li> <li className="book-container"> <img src="https://images-na.ssl-images-amazon.com/images/I/511j6cza5bL.jpg" alt="JavaScript: The Good Parts: The Good Parts" /> <strong>JavaScript: The Good Parts: The Good Parts</strong> <span>R$ 44,69</span> <button type="button" onClick={() => {}}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} 0 </div> <span>Adiconar</span> </button> </li> <li className="book-container"> <img src="https://images-na.ssl-images-amazon.com/images/I/51ZL3TV7D1L._SX360_BO1,204,203,200_.jpg" alt="Padrões JavaScript" /> <strong>Padrões JavaScript</strong> <span>R$ 47,68</span> <button type="button" onClick={() => {}}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} 0 </div> <span>Adiconar</span> </button> </li> <li className="book-container"> <img src="https://images-na.ssl-images-amazon.com/images/I/51wX6cd1iiL._SX357_BO1,204,203,200_.jpg" alt="Aprendendo Node: Usando JavaScript no Servidor" /> <strong>Aprendendo Node: Usando JavaScript no Servidor</strong> <span>R$ 66,75</span> <button type="button" onClick={() => {}}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} 0 </div> <span>Adiconar</span> </button> </li> <li className="book-container"> <img src="https://images-na.ssl-images-amazon.com/images/I/51TUG%2BmeWnL._SX342_BO1,204,203,200_.jpg" alt="Princípios de Orientação a Objetos em JavaScript" /> <strong>Princípios de Orientação a Objetos em JavaScript</strong> <span>R$ 36,00</span> <button type="button" onClick={() => {}}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} 0 </div> <span>Adiconar</span> </button> </li> <li className="book-container"> <img src="https://images-na.ssl-images-amazon.com/images/I/41357uLpB-L.jpg" alt="Cangaceiro JavaScript: Uma aventura no sertão da programação" /> <strong>Cangaceiro JavaScript: Uma aventura no sertão da programação</strong> <span>R$ 39,90</span> <button type="button" onClick={() => {}}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} 0 </div> <span>Adiconar</span> </button> </li> </ul> </main> ); } |
Agora vamos partir para a estilização desta página.
2 – No arquivo src/pages/Home/styles.css deixe-o da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
.book-catalog { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: 20px; list-style: none; } .book-container { display: flex; flex-direction: column; background: #fff; border-radius: 4px; padding: 16px; width: 220px; } .book-container img { align-self: center; max-width: 180px; } .book-container > strong { font-size: 16px; line-height: 20px; color: #878C99; margin-top: 8px; } .book-container > span { font-size: 24px; font-weight: bold; margin: 8px 0; } .book-container button { background: transparent; color: #878C99; border: 2px solid #33BFCB; border-radius: 4px; overflow: hidden; margin-top: auto; display: flex; align-items: center; transition: background 0.5s, color 0.5s; } .book-container button:hover { border: 2px solid #33BFCB; color: #FFF; background: #33BFCB; } .book-container button div { display: flex; align-items: center; padding: 12px; color: #878C99; background: #FFF; border-right: 2px solid #33BFCB; } .book-container button div svg { margin-right: 5px; } .book-container button span { flex: 1; text-align: center; font-weight: bold; } |
Página Cart
Esta é a página onde exibirá os produtos selecionados e os totais da compra.
Vamos começar pela estrutura da página, com alguns dado fixos por enquanto.
1 – No arquivo src/pages/Cart/index.js deixe-o da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
import React from 'react'; import { FiPlusCircle, FiMinusCircle, FiXCircle } from 'react-icons/fi' import './styles.css'; export default function Cart() { return ( <main className="container"> <div className="bag-container"> <table className="book-table"> <thead> <tr> <th /> <th>Livro</th> <th>Quantidade</th> <th>Subtotal</th> <th /> </tr> </thead> <tbody> <tr> <td> <img src="https://images-na.ssl-images-amazon.com/images/I/51w53T12s8L.jpg" alt="JavaScript: O Guia Definitivo" /> </td> <td> <strong>JavaScript: O Guia Definitivo</strong> <span>R$ 146,08</span> </td> <td> <div> <button type="button" onClick={() => {}}> <FiMinusCircle size={20} color="#33BFCB" /> </button> <input type="number" readOnly value="1" /> <button type="button" onClick={() => {}}> <FiPlusCircle size={20} color="#33BFCB" /> </button> </div> </td> <td> <strong>R$ 1000,00</strong> </td> <td> <button type="button" onClick={() => {}} > <FiXCircle size={20} color="#33BFCB" /> </button> </td> </tr> </tbody> </table> <footer> <button type="button">Encomendar</button> <div className="total"> <span>Total</span> <strong>R$ 1000,00</strong> </div> </footer> </div> </main> ); } |
Agora vamos partir para a estilização desta página.
2 – No arquivo src/pages/Cart/styles.css deixe-o da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
.bag-container { padding: 30px; background: #fff; border-radius: 4px; } .bag-container footer { margin-top: 30px; display: flex; justify-content: space-between; align-items: center; } .bag-container footer button { background: transparent; color: #33BFCB; border: 2px solid #33BFCB; border-radius: 4px; padding: 12px 20px; font-weight: bold; transition: background 0.5s, color 0.5s; } .bag-container footer button:hover { color: #FFF; background: #33BFCB; } .book-table { width: 100%; } .book-table thead th { color: #878C99; text-align: left; padding: 12px; } .book-table tbody td { padding: 12px; border-bottom: 1px solid #eee; } .book-table img { height: 100px; } .book-table strong { color: #333; display: block; } .book-table span { display: block; margin-top: 5px; font-size: 18px; font-weight: bold; } .book-table div { display: flex; align-items: center; } .book-table div input { border: 1px solid #ddd; border-radius: 4px; color: #878C99; padding: 6px; width: 50px; } .book-table button { background: none; border: 0; padding: 6px; } .total { display: flex; align-items: baseline; } .total span { color: #878C99; font-weight: bold; } .total strong { font-size: 28px; margin-left: 5px; } |
Integração com API
Ao invés de ter dados na nossa tela de forma estática na página Home, vamos consumir estes dados através de uma API.
Para não precisarmos ter trabalho na criação de um backend para isso, vamos utilizar o json-server, que oferece dados via API através de um arquivo json como se fosse um banco de dados.
1 – Para instalar o json-server, vamos instalar o mesmo como dependência de desenvolvimento.
1 |
yarn add json-server -D |
2 – E agora vamos criar na raiz do projeto o arquivo server.json que conterá os dados dos nossos livros.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
{ "books": [ { "id": 1, "title": "JavaScript: O Guia Definitivo", "price": 146.08, "image": "https://images-na.ssl-images-amazon.com/images/I/51w53T12s8L.jpg" }, { "id": 2, "title": "JavaScript: The Good Parts: The Good Parts", "price": 44.69, "image": "https://images-na.ssl-images-amazon.com/images/I/511j6cza5bL.jpg" }, { "id": 3, "title": "Padrões JavaScript", "price": 47.68, "image": "https://images-na.ssl-images-amazon.com/images/I/51ZL3TV7D1L._SX360_BO1,204,203,200_.jpg" }, { "id": 4, "title": "Aprendendo Node: Usando JavaScript no Servidor", "price": 66.75, "image": "https://images-na.ssl-images-amazon.com/images/I/51wX6cd1iiL._SX357_BO1,204,203,200_.jpg" }, { "id": 5, "title": "Princípios de Orientação a Objetos em JavaScript", "price": 36.00, "image": "https://images-na.ssl-images-amazon.com/images/I/51TUG%2BmeWnL._SX342_BO1,204,203,200_.jpg" }, { "id": 6, "title": "Cangaceiro JavaScript: Uma aventura no sertão da programação", "price": 39.90, "image": "https://images-na.ssl-images-amazon.com/images/I/41357uLpB-L.jpg" } ] } |
3 – Para levantar o serviço do json-server na porta 3333, execute o comando:
1 |
json-server server.json -p 3333 |
Agora vamos configurar a nossa aplicação para pegar os dados da API, para isso vamos utilizar o Axios.
4 – Para instalá-lo execute o comando.
1 |
yarn add axios |
5 – Crie o arquivo de configuração do axios src/services/api.js.
1 2 3 4 5 6 7 |
import axios from 'axios'; const api = axios.create({ baseURL: 'http://localhost:3333', }); export default api; |
6 – Feito isso, agora vamos novamente ao src/pages/Home/index.js para receber os dados via API dos produtos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import React, { useState, useEffect } from 'react'; import { FiShoppingBag } from 'react-icons/fi'; import api from '../../services/api'; import './styles.css'; export default function Home() { const [books, setBooks] = useState([]); useEffect(() => { async function loadBooks() { const response = await api.get('/books'); setBooks(response.data); } loadBooks(); }, []); return ( <main className="container"> <ul className="book-catalog"> {books.map(book => ( <li key={book.id} className="book-container"> <img src={book.image} alt={book.title} /> <strong>{book.title}</strong> <span>R$ {book.price}</span> <button type="button" onClick={() => {}}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} 0 </div> <span>Adiconar</span> </button> </li> ))} </ul> </main> ); } |
Desta forma, já teremos nossos dados vindo da API.
Configurando o Redux
Agora sim que temos toda a base do projeto criada, vamos finalmente ver o Redux.
Primeiramente vamos instalar o redux que trata do próprio Redux em si.
O react-redux que lida com a integração do Redux com o React.js, pois o Redux em si, pode ser utilizado também no angular, vue, dentre outros frameworks e bibliotecas JavaScript.
O @reduxjs/toolkit é uma biblioteca oficial do time do Redux e indicada por eles, que torna a configuração do Redux, muito mais fácil.
1 |
yarn add redux react-redux @reduxjs/toolkit |
Feita a instalação, vamos entender rapidamente como é o fluxo de dados do Redux.
Na imagem abaixo, em roxo estão os elementos principais do Redux e em verde um componente genérico, que nosso caso seria o Header, Cart ou Home.
Os estados globais são armazenados na store, que é imutável.
Um component pode a qualquer momento solicitar diretamente os dados para a store e também será notificado quando os dados são alterados.
Se um component quer modificar algo um dado na store, ele deve chamar uma Action.
As actions são as ações possíveis que o component pode solicitar, como adicionar um item, remover um item, etc.
Vai ter action para cada ação e podem receber um determinado valor como parâmetro.
Quando uma action é chamada, o Reducer é acionado, e é ele que será responsável por atualizar os dados na store.
É ele que contém toda a lógica de negócio.
Essa que é a famosa arquitetura Flux, que o Redux implementa.
Vamos a seguir configurar toda essa estrutura do Redux e nos aprofundar mais sobre cada um desses elementos.
Store
A store é um único objeto que armazena todo o nosso conjunto de estados globais da nossa aplicação.
Este um único objeto segue o conceito de “um único ponto de verdade”, ou seja, ele garante a consistência e confiabilidade dos dados.
Além disso, esses dados são imutáveis, para reaproveitar as vantagens da programação funcional, evitando alguns problemas de estados inconsistentes.
Será através da store, que os components vão buscar os dados e ser notificados quando houver alguma alteração.
Dito isso, vamos criar nossa store.
1 – Crie a pasta src/store, que será onde ficará toda a nossa lógica do Redux.
Dentro desta pasta crie o arquivo index.js, onde vamos criar a nossa store.
1 2 3 4 5 |
import { configureStore } from '@reduxjs/toolkit'; const store = configureStore(); export default store; |
2 – Agora, para que a nossa store fique disponível para nossa aplicação, vamos voltar no src/App.js e vamos utilizar o Provider do redux, que vai servir para a nossa aplicação a store que acabamos de criar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import React from 'react'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import Routes from './routes'; import Header from './components/Header'; import store from './store'; function App() { return ( <Provider store={store}> <BrowserRouter> <Header /> <Routes /> </BrowserRouter> </Provider> ); } export default App; |
Neste momento a nossa aplicação vai dar erro, mas calma que ainda temos que configurar mais algumas coisas.
Action
A action é um objeto que disponibiliza uma interface com as possíveis as ações, que os components podem utilizar.
Uma action basicamente tem 2 atributos essenciais:
- type: É um código identificador, no formato de string, que servirá para o reducer identificar as actions, é como a carteira de identidade de cada action;
- payload: São os dados que o component passa para a action, para realizar alguma ação.
Nem toda action tem um payload, mas em casos como uma action para adicionar itens a uma lista, ele precisa que o component passe esse item a ser adicionado.
Agora vamos criar nossa estrutura para configurar nossa action.
Para deixar nossa aplicação organizada e escalável futuramente, vamos criar uma pasta chamada src/store/modules, onde vamos separar os reducers e actions por responsabilidade.
1 – Crie uma pasta src/store/modules/cart que terá apenas a responsabilidade de gerenciar os dados relacionados ao carrinho.
Caso futuramente quiséssemos adicionar também estados para gerenciar perfil de usuário, autenticação, dentre outros, basta criar uma pasta para cada um dentro de src/store/modules/.
2 – Agora vamos criar o arquivo de nossas actions, o src/store/modules/cart/actions.js e dentro dele criar 3 actions para que nossos components possam utilizar.
a – A primeira para adicionar um item ao carrinho, que vamos chamar de addToCart.
b – A segunda para remover um item do carrinho, chamada de removeFromCart.
c – E uma terceira para atualizar a quantidade de cada item que está no carrinho, com o nome de updateAmaount.
Para isso, utilizaremos a função createAction, passando apenas o type (identificador).
Esse type é apenas uma string, mas o recomendado é seguir o padrão de colocar o prefixo com o nome da pasta o qual a action está imediatamente contida e depois da barra deve descrever o que a action faz.
Seu arquivo deve ficar assim:
1 2 3 4 5 |
import { createAction } from '@reduxjs/toolkit'; export const addToCart = createAction('cart/add_book'); export const removeFromCart = createAction('cart/remove_book'); export const updateAmount = createAction('cart/update_amount'); |
Mas e o payload?
Graças ao Redux Toolkit, o payload é definido por baixo dos panos como um objeto flexível.
Além disso devemos exportar cada action para serem utilizadas pelo reducer e pelos components.
Reducer
Como foi citado antes, na nossa store os dados são imutáveis, mas em alguns momentos será necessário alterar esses estados, para isso existem os reducers, que são funções puras, responsáveis por “alterar” os estados.
Na verdade ele cria um novo estado com novos estados, assim como é feito nos estados de qualquer component.
O reducer também tem o papel de centralizar a lógica de alteração dos estados e evitar que cada component tenha essa responsabilidade.
1 – Para definir nosso reducer, crie o arquivo src/store/components/cart/reducer.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import { createReducer } from '@reduxjs/toolkit'; import { addToCart, removeFromCart, updateAmount, } from './actions'; const cart = createReducer([], { [addToCart]: (state, action) => { const { payload } = action; const { id } = payload; const bookExists = state.find(book => book.id === id); if (bookExists) { bookExists.amount = bookExists.amount + 1; } else { payload.amount = 1; state.push(payload); } }, [removeFromCart]: (state, action) => { const productIndex = state.findIndex(book => book.id === action.payload); if (productIndex >= 0) { state.splice(productIndex, 1); } }, [updateAmount]: (state, action) => { const { id, amount } = action.payload; const bookExists = state.find(book => book.id === id); if (bookExists) { console.log(action.payload) const bookIndex = state.findIndex(book => book.id === bookExists.id); if (bookIndex >= 0 && amount >= 0) { state[bookIndex].amount = Number(amount); } } return state; }, }); export default cart |
Nele importamos nossas actions criadas anteriormente.
A função createReducer, permite criar nosso reducer em si, ele recebe 2 parâmetros.
O primeiro é o estado inicial, ou seja, assim que nossa aplicação for iniciada, ele vai popular o state com esses dados.
O segundo é um objeto que conterá a nossa lógica de negócio para cada uma das actions.
Deverá passar a action como chave e como valor, uma função que recebe state e action.
O state vem com os dados da nossa store e o action contem os dados da nossa action com os atributos type e o payload.
É através do payload que vamos pegar os dados passados pelo component.
Considerando que o component passou como payload estes dados abaixo para a action addToCart:
1 2 3 4 5 6 |
{ "id": 1, "title": "JavaScript: O Guia Definitivo", "price": 146.08, "image": "https://images-na.ssl-images-amazon.com/images/I/51w53T12s8L.jpg" } |
Vamos analisar o código desta action para entender melhor:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[addToCart]: (state, action) => { const { payload } = action; const { id } = payload; const bookExists = state.find(book => book.id === id); if (bookExists) { bookExists.amount = bookExists.amount + 1; } else { payload.amount = 1; state.push(payload); } } |
Nele extraímos o payload da action e extraímos o id do item de carrinho que deverá ser adicionado.
Em seguida vamos tentar buscar na store se já existe esse item.
Caso ele exista, vamos apenar incrementa a quantidade dele no carrinho.
Caso ele ainda não exista, vamos adicionar o campo amount com quantidade 1 e adiciona-lo à store.
Note que, apesar de o state representar a store, que é imutável, podemos modifica-la como um objeto ou array qualquer e após isso será automaticamente alterado o estado global.
Isso ocorre, pois o Redux Toolkit faz a mágica por baixo do panos, facilitando nossa vida em modificar um estado imutável, sem precisarmos recorrer a técnicas de desestruturação, que podem se tornar um verdadeiro desafio dependendo do caso.
Tendo entendido esses conceitos, as demais ações ficam muito simples de se entender o que fazem.
Mas ainda falta um detalhe, precisamos vincular nosso reducer a store.
2 – Porém temos outro detalhe, para permitir no futuro que nossa store possa registrar mais de um reducer, precisamos de uma configuração a mais, para isso vamos criar primeiro um arquivo src/store/modules/rootReducer.js que vai centralizar todos os nossos reducers da aplicação.
Bastando utilizar o método combineReducers para adicionar todos os nossos reducers que vamos utilizar.
1 2 3 4 5 6 7 |
import { combineReducers } from 'redux'; import cart from './cart/reducer'; export default combineReducers({ cart, }); |
Agora sim, vamos pegar todos os reducers de uma só vez do rootReducer.js e adicionar na nossa store.
3 – Indo novamente em src/srote/index.js e vamos passar o rootReduce para o método configureStore.
1 2 3 4 5 6 7 |
import { configureStore } from '@reduxjs/toolkit' import rootReducer from './modules/rootReducer'; const store = configureStore({ reducer: rootReducer }) export default store; |
Feito isso, nossa estrutura de configuração do Redux está pronta.
Redux em ação
Agora sim vamos ver o Redux em ação.
Página Home
1 – Vamos fazer nossa última alteração na página Home e ver a explicação em seguida.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { FiShoppingBag } from 'react-icons/fi'; import api from '../../services/api'; import * as CartActions from '../../store/modules/cart/actions'; import './styles.css'; export default function Home() { const [books, setBooks] = useState([]); const amount = useSelector(state => state.cart.reduce((sumAmount, book) => { sumAmount[book.id] = book.amount; return sumAmount; }, {}) ); const dispatch = useDispatch(); useEffect(() => { async function loadBooks() { const response = await api.get('/books'); setBooks(response.data); } loadBooks(); }, []); function handleAddProduct(book) { dispatch(CartActions.addToCart(book)); } return ( <main className="container"> <ul className="book-catalog"> {books.map(book => ( <li key={book.id} className="book-container"> <img src={book.image} alt={book.title} /> <strong>{book.title}</strong> <span>R$ {book.price}</span> <button type="button" onClick={() => handleAddProduct(book)}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} {amount[book.id] || 0} </div> <span>Adiconar</span> </button> </li> ))} </ul> </main> ); } |
Vamos entender o código por partes.
Primeiro importamos do react-redux dois módulos:
- useDispatch: Faz a chamada de uma action.
- useSelector: Recupera os dados da store.
Ele recebe uma função anônima como parâmetro, e essa função anônima recebe o state como argumento, que representa o estado global.
Em seguida importamos nossas actions.
1 2 3 4 5 6 7 |
import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { FiShoppingBag } from 'react-icons/fi'; import api from '../../services/api'; import * as CartActions from '../../store/modules/cart/actions'; |
Depois é criado a constante amount que vai utilizar o useSelector para obter os dados da store, especificamente do cart, ou seja, do carrinho e vai armazenar a quantidade de cada item.
1 2 3 4 5 6 7 |
const amount = useSelector(state => state.cart.reduce((sumAmount, book) => { sumAmount[book.id] = book.amount; return sumAmount; }, {}) ); |
Em seguida vamos utilizá-lo para substituir o valor 0 estático da quantidade de cada livro no carrinho, por um valor dinâmico baseado no id do livro.
1 2 3 4 5 6 7 8 |
<button type="button" onClick={() => handleAddProduct(book)}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} {amount[book.id] || 0} </div> <span>Adiconar</span> </button> |
Agora será necessário criar uma instância do dispatch, para poder utiliza-lo.
1 |
const dispatch = useDispatch(); |
Com o dispatch criado, que é quem dispara as actions, criamos a função handleAddProduct que recebe todos os dados do livro e chama a action addToCart passando os dados do livro como payload.
1 2 3 |
function handleAddProduct(book) { dispatch(CartActions.addToCart(book)); } |
Agora no botão adicionar, adicionamos essa função passando o livro atual que está.
1 2 3 4 5 6 7 8 |
<button type="button" onClick={() => handleAddProduct(book)}> <div> <FiShoppingBag size={16} color="#33BFCB" />{' '} {amount[book.id] || 0} </div> <span>Adiconar</span> </button> |
Assim, quando o usuário clicar, ele vai adicionar um livro ao nosso carrinho e automaticamente o amount será recarregado mostrando a quantidade atual do livro.
No próximo clique, no mesmo produto, ele vai apenas incrementar a quantidade.
Component Header
1 – O nosso header será mais simples, pois exibe apenas do lado direito da tela a quantidade dos livros no carrinho (components/Header/index.js).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import React from 'react'; import { Link } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { FiShoppingBag } from 'react-icons/fi'; import './styles.css'; import logo from '../../assets/book.png'; export default function Header() {; const cartSize = useSelector(state => state.cart.length); return ( <header className="header"> <Link to="/" className="logo"> <img className="logo-icon" src={logo} alt="Rocketshoes" /> <span className="logo-text">OneBitBooks</span> </Link> <Link to="/cart" className="header-cart"> <div> <strong>Sacola</strong> <span> <strong>{cartSize}</strong> livros </span> </div> <FiShoppingBag size={36} color="#FFF" /> </Link> </header> ); } |
Como precisamos apenas exibir os dados, será necessário apenas importar o useSelector.
1 |
import { useSelector } from 'react-redux'; |
Em seguida utilizamos ele para criar a constante cartSize que vai calcular a quantidade total de itens dentro do state.cart.
1 |
const cartSize = useSelector(state => state.cart.length); |
Agora é só exibir o cartSize de forma dinâmica.
1 2 3 4 5 6 7 8 9 |
<Link to="/cart" className="header-cart"> <div> <strong>Sacola</strong> <span> <strong>{cartSize}</strong> livros </span> </div> <FiShoppingBag size={36} color="#FFF" /> </Link> |
Agora sempre que um livro for adicionado ou removido, essa quantidade no Header será alterada.
Página Cart
1 – Segue a linha de pensamento da página Home (components/pages/Cart/index.js).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { FiPlusCircle, FiMinusCircle, FiXCircle } from 'react-icons/fi' import * as CartActions from '../../store/modules/cart/actions'; import './styles.css'; export default function Cart() { const cart = useSelector(state => state.cart.map(book => ({ ...book, subtotal: book.price * book.amount, })) ); const total = useSelector(state => state.cart.reduce((totalSum, product) => { return totalSum + product.price * product.amount; }, 0) ); const dispatch = useDispatch(); function increment(book) { dispatch(CartActions.updateAmount({ id: book.id, amount: book.amount + 1, })); } function decrement(book) { dispatch(CartActions.updateAmount({ id: book.id, amount: book.amount - 1, })); } return ( <main className="container"> <div className="bag-container"> <table className="book-table"> <thead> <tr> <th /> <th>Livro</th> <th>Quantidade</th> <th>Subtotal</th> <th /> </tr> </thead> <tbody> {cart.map(book => ( <tr key={book.id}> <td> <img src={book.image} alt={book.title} /> </td> <td> <strong>{book.title}</strong> <span>R$ {book.price}</span> </td> <td> <div> <button type="button" onClick={() => decrement(book)}> <FiMinusCircle size={20} color="#33BFCB" /> </button> <input type="number" readOnly value={book.amount} /> <button type="button" onClick={() => increment(book)}> <FiPlusCircle size={20} color="#33BFCB" /> </button> </div> </td> <td> <strong>R$ {book.subtotal.toFixed(3).slice(0,-1)}</strong> </td> <td> <button type="button" onClick={() => dispatch(CartActions.removeFromCart(book.id))} > <FiXCircle size={20} color="#33BFCB" /> </button> </td> </tr> ))} </tbody> </table> <footer> <button type="button">Encomendar</button> <div className="total"> <span>Total</span> <strong>R$ {total.toFixed(3).slice(0,-1)}</strong> </div> </footer> </div> </main> ); } |
Será utilizado o useDispatch e useSelector, então eles devem ser importados e o useDispatch instanciado.
A constante cart, vai coletar todos os nossos itens armazenados na store de cart, e vamos utilizar o método map para calcular o subtotal de cada item.
1 2 3 4 5 6 |
const cart = useSelector(state => state.cart.map(book => ({ ...book, subtotal: book.price * book.amount, })) ); |
O cart é utilizado mais adiante para listar cada um dos itens do carrinho e suas respectivas informações.
1 2 3 4 5 6 |
{cart.map(book => ( <tr key={book.id}> <td> <img src={book.image} alt={book.title} /> </td> ... |
Em seguida criamos a constante total, que calculará o valor total de todos os itens do carrinho.
1 2 3 4 5 |
const total = useSelector(state => state.cart.reduce((totalSum, product) => { return totalSum + product.price * product.amount; }, 0) ); |
Esse total é exibido no fim da página.
1 2 3 4 |
<div className="total"> <span>Total</span> <strong>R$ {total.toFixed(3).slice(0,-1)}</strong> </div> |
Agora com relação as actions, será declarado uma função increment para adicionar mais um a quantidade do item e decrement para subtrair uma quantidade do item do carrinho.
Tanto a quantidade, como o id do produto, são repassados para a payload da action updateAmount.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function increment(book) { dispatch(CartActions.updateAmount({ id: book.id, amount: book.amount + 1, })); } function decrement(book) { dispatch(CartActions.updateAmount({ id: book.id, amount: book.amount - 1, })); } |
Essas funções são chamadas pelos botões de mais e menos.
E entre ambos, aparece a quantidade atual do produto.
1 2 3 4 5 6 7 8 9 10 11 |
<td> <div> <button type="button" onClick={() => decrement(book)}> <FiMinusCircle size={20} color="#33BFCB" /> </button> <input type="number" readOnly value={book.amount} /> <button type="button" onClick={() => increment(book)}> <FiPlusCircle size={20} color="#33BFCB" /> </button> </div> </td> |
Feito isso a nossa aplicação está pronta.
Conclusão
Com esse APP, vimos todos os conceitos principais do Redux, como store, action e reducer, além de entender a utilidade dele ao trabalhar com estados globais.
Agora para continuar seus estudos, saiba que o Redux tem outros módulos que se conectam a ele, o que mais indico de início é o saga, que fica responsável por tratar efeitos colaterais dentro do Redux, já que o Reducer só permite funções puras.
https://github.com/redux-saga/redux-saga
Além disso também indico dar uma olhada no método createSlice do Redux Toolkit, que simplifica mais ainda a criação de actions e reducer.
https://redux-toolkit.js.org/api/createSlice
Se você gostou deste artigo e quer mais conteúdo sobre o mundo Javascript deixa um comentário ai em baixo.
Obrigado
Primeira vez no OneBitCode? Curtiu esse conteúdo?
O OneBitCode tem muito mais para você!
O OneBitCode traz conteúdos de qualidade, e em português, sobre programação com foco em Ruby on Rails e também JavaScript.
Além disso, aqui sempre levamos à você conteúdos valiosos sobre a carreira de programação, dicas sobre currículos, portfólios, perfil profissional, soft skills, enfim, tudo o que você precisa saber para continuar evoluindo como Programador(a)!
Fique por dentro de todos os conteúdos o/
Nossas redes sociais:
📹 • https://youtube.com/Onebitcode [Live todas as terças-feiras às 19h)
💻 • https://linkedin.com/company/onebitcode
🙂 • the dating ring
📱 • are there any free dating sites
🐦 • https://onebitcode.com/kid-dating-sites-12-under/
Nossos cursos:
🥇 • dating in san diego
💎 • https://onebitcode.com/flirt-dating-in-usa/
⚙ • Minicurso: API Rails 5 Completo
🐞 • Minicurso de Testes para Ruby on Rails com RSpec
Espero que curta nossos conteúdos e sempre que precisar de ajuda, fala com a gente!
Estamos aqui para você 🙂
Bem-vindo à família OneBitCode o/
Muito Bom!!!
Eu estou recebendo o seguinte erro na função anonima na pagina home TypeError: Cannot read property ‘reduce’ of undefined
Fiz e refiz as instruções o sistema roda mas não carrega as informações do server.json, nã apresenta nenhum erro simplesmente a tela Home vem em branco. já procurei erros de diretórios, nome de arquivos, etc.. gostaria e uma dica?