📚 Módulo 08: Frontend - Carrinho Reativo (Hooks) e CRUD Admin
Neste módulo, programaremos a inteligência interativa do frontend da TecLoja 03. Criaremos um Custom Hook chamado useCart para gerenciar de forma totalmente reativa o estado do carrinho de compras e recalcular totais de forma instantânea. Desenvolveremos a tela de Faturamento de Pedido (Checkout) enviando requisições à API NestJS, e criaremos a interface de administração para o CRUD de Produtos completa com validação visual de formulários.
🔄 Reatividade de Estado (Hook)
A grande vantagem de centralizar a inteligência em um Hook customizado é que múltiplos componentes podem ler os dados e reagir simultaneamente sem duplicação de código.
stateDiagram-v2
[*] --> HookuseCart
HookuseCart --> LocalStorage : Salva (Persistência)
HookuseCart --> TelaCarrinho : Retorna Arrays e Funções
HookuseCart --> Navbar : Retorna totalItens (Notificação)
state TelaCarrinho {
adicionar()
remover()
}
🛒 1. O Gancho do Carrinho Reativo (useCart)
No React, os Custom Hooks permitem extrair a lógica de estado de um componente para que ela seja reaproveitada de forma modular em qualquer parte da aplicação.
Criaremos a inteligência que controla a inserção, remoção, alteração de quantidades de produtos no carrinho e recálculo dinâmico de totais. Crie o arquivo em src/hooks/useCart.ts:
// src/hooks/useCart.ts
import { useState, useEffect } from 'react';
import { Produto, ItemPedidoInput } from '../types';
interface ItemCarrinho {
produto: Produto;
quantidade: number;
}
export const useCart = () => {
const [itens, setItens] = useState<ItemCarrinho[]>([]);
// 1. Carregar carrinho persistido no localStorage na inicialização
useEffect(() => {
const carrinhoSalvo = localStorage.getItem('tecloja_carrinho');
if (carrinhoSalvo) {
setItens(JSON.parse(carrinhoSalvo));
}
}, []);
// 2. Gravar no localStorage sempre que o carrinho for modificado
const persistirCarrinho = (novosItens: ItemCarrinho[]) => {
localStorage.setItem('tecloja_carrinho', JSON.stringify(novosItens));
setItens(novosItens);
};
const adicionarItem = (produto: Produto, quantidade = 1) => {
const novosItens = [...itens];
const index = novosItens.findIndex((item) => item.produto.id === produto.id);
if (index > -1) {
// Se o produto já está no carrinho, incrementa a quantidade
novosItens[index].quantidade += quantidade;
} else {
// Se não, adiciona um novo item
novosItens.push({ produto, quantidade });
}
persistirCarrinho(novosItens);
};
const removerItem = (produtoId: number) => {
const novosItens = itens.filter((item) => item.produto.id !== produtoId);
persistirCarrinho(novosItens);
};
const alterarQuantidade = (produtoId: number, quantidade: number) => {
if (quantidade < 1) return;
const novosItens = itens.map((item) => {
if (item.produto.id === produtoId) {
return { ...item, quantidade };
}
return item;
});
persistirCarrinho(novosItens);
};
const limparCarrinho = () => {
persistirCarrinho([]);
};
// Recálculo dinâmico de totais (Equivalente reativo a Signals/Computed)
const totalItens = itens.reduce((acumulado, item) => acumulado + item.quantidade, 0);
const valorTotal = itens.reduce((acumulado, item) => acumulado + Number(item.produto.preco) * item.quantidade, 0);
return {
itens,
totalItens,
valorTotal,
adicionarItem,
removerItem,
alterarQuantidade,
limparCarrinho,
};
};
💳 2. Integrando a Tela de Checkout (Faturamento)
Abaixo, criaremos o componente de visualização da tela do Carrinho de Compras em src/views/Carrinho.tsx, integrando o gancho useCart e enviando os dados de compra atômica para a nossa API transacional do NestJS:
// src/views/Carrinho.tsx
import React, { useState } from 'react';
import { useCart } from '../hooks/useCart';
import api from '../services/api';
import { CheckoutInput } from '../types';
const Carrinho: React.FC = () => {
const { itens, valorTotal, removerItem, alterarQuantidade, limparCarrinho } = useCart();
const [comprado, setComprado] = useState(false);
const [carregando, setCarregando] = useState(false);
const [erro, setErro] = useState<string | null>(null);
const handleFinalizarCompra = async () => {
setCarregando(true);
setErro(null);
const payload: CheckoutInput = {
clienteId: 1, // Simulação de Cliente de teste semeado no banco no Módulo 02
itens: itens.map((item) => ({
produtoId: item.produto.id,
quantidade: item.quantidade,
})),
};
try {
// POST para a API NestJS
await api.post('/pedidos/checkout', payload);
limparCarrinho();
setComprado(true);
} catch (err: any) {
console.error(err);
// Exibe a mensagem limpa formatada pelo HttpExceptionFilter do NestJS
setErro(err.response?.data?.message || 'Erro inesperado ao processar o faturamento.');
} finally {
setCarregando(false);
}
};
if (comprado) {
return (
<div className="card-glass text-center">
<h2>🎉 Pedido Aprovado!</h2>
<p>Sua compra foi processada com sucesso no banco de dados.</p>
<button className="btn-premium btn-primario" onClick={() => setComprado(false)}>Voltar ao Catálogo</button>
</div>
);
}
return (
<div className="grid-carrinho">
<div className="card-glass">
<h2>🛒 Seu Carrinho</h2>
{itens.length === 0 ? (
<p>O seu carrinho está vazio no momento.</p>
) : (
itens.map((item) => (
<div key={item.produto.id} className="item-carrinho">
<span>{item.produto.nome}</span>
<input
type="number"
min="1"
value={item.quantidade}
onChange={(e) => alterarQuantidade(item.produto.id, Number(e.target.value))}
/>
<span>R$ {(Number(item.produto.preco) * item.quantidade).toFixed(2)}</span>
<button onClick={() => removerItem(item.produto.id)}>Remover</button>
</div>
))
)}
</div>
<div className="card-glass">
<h3>Resumo do Pedido</h3>
<p>Total Geral: R$ {valorTotal.toFixed(2)}</p>
{erro && <div className="erro-alerta">{erro}</div>}
<button
className="btn-premium btn-primario"
disabled={itens.length === 0 || carregando}
onClick={handleFinalizarCompra}
>
{carregando ? 'Processando transação...' : 'Confirmar Compra'}
</button>
</div>
</div>
);
};
export default Carrinho;
💻 3. Formulário de CRUD com Validações Manuais Reativas
Para gerenciar o cadastro e atualização de eletrônicos no e-commerce, criaremos o formulário administrativo em src/views/admin/ProdutoForm.tsx. O componente usará validações manuais acionadas por propriedades em tempo de preenchimento, fornecendo um feedback visual impecável ao usuário antes de salvar dados via requisição HTTP:
```tsx // src/views/admin/ProdutoForm.tsx import React, { useState, useEffect } from ‘react’; import { useNavigate, useParams } from ‘react-router-dom’; import api from ‘../../services/api’; import { Categoria } from ‘../../types’;
const ProdutoForm: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const editando = !!id;
const [nome, setNome] = useState(‘’); const [descricao, setDescricao] = useState(‘’); const [preco, setPreco] = useState(0); const [estoque, setEstoque] = useState(0); const [categoriaId, setCategoriaId] = useState(0); const [categorias, setCategorias] = useState<Categoria[]>([]);
// Estados de Validação do Formulário (Reação nativa sem dependências) const [erros, setErros] = useState<{ [campo: string]: string }>({}); const [carregando, setCarregando] = useState(false);
useEffect(() => { // Carregar categorias existentes para preencher a seleção api.get<Categoria[]>(‘/categorias’).then((res) => setCategorias(res.data));
if (editando) {
api.get(`/produtos/${id}`).then((res) => {
setNome(res.data.nome);
setDescricao(res.data.descricao || '');
setPreco(Number(res.data.preco));
setEstoque(Number(res.data.estoque));
setCategoriaId(res.data.categoriaId);
});
} }, [id, editando]);
// Função validadora manual reativa const validarFormulario = (): boolean => { const errosDetectados: { [campo: string]: string } = {};
if (!nome.trim()) errosDetectados.nome = 'O nome do produto é obrigatório.';
if (preco <= 0) errosDetectados.preco = 'O preço deve ser um valor positivo maior que zero.';
if (estoque < 0) errosDetectados.estoque = 'A quantidade de estoque não pode ser negativa.';
if (categoriaId <= 0) errosDetectados.categoriaId = 'Selecione uma categoria de produto válida.';
setErros(errosDetectados);
return Object.keys(errosDetectados).length === 0; };
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validarFormulario()) return; // Aborta envio se houver falhas
setCarregando(true);
const payload = { nome, descricao, preco, estoque, categoriaId };
try {
if (editando) {
await api.put(`/produtos/${id}`, payload);
} else {
await api.post('/produtos', payload);
}
navigate('/admin/produtos');
} catch (err: any) {
console.error(err);
alert(err.response?.data?.message || 'Erro ao registrar informações do produto.');
} finally {
setCarregando(false);
} };
return ( <div className="card-glass max-w-lg"> <h2>{editando ? ‘📝 Editar Eletrônico’ : ‘✨ Cadastrar Novo Produto’}</h2> <form onSubmit={handleSubmit} className=”form-estrutura”>
<div className="form-grupo">
<label>Nome do Produto</label>
<input
type="text"
className="form-campo"
value={nome}
onChange={(e) => setNome(e.target.value)}
/>
{erros.nome && <span className="erro-texto">{erros.nome}</span>}
</div>
<div className="form-grupo">
<label>Preço de Venda (R$)</label>
<input
type="number"
step="0.01"
className="form-campo"
value={preco}
onChange={(e) => setPreco(Number(e.target.value))}
/>
{erros.preco && <span className="erro-texto">{erros.preco}</span>}
</div>
<div className="form-grupo">
<label>Estoque Inicial</label>
<input
type="number"
className="form-campo"
value={estoque}
onChange={(e) => setEstoque(Number(e.target.value))}
/>
{erros.estoque && <span className="erro-texto">{erros.estoque}</span>}
</div>
<div className="form-grupo">
<label>Categoria de Catálogo</label>
<select
className="form-campo"
value={categoriaId}
onChange={(e) => setCategoriaId(Number(e.target.value))}
>
<option value="0">Selecione uma Categoria...</option>
{categorias.map((cat) => (
<option key={cat.id} value={cat.id}>{cat.nome}</option>
))}
</select>
{erros.categoriaId && <span className="erro-texto">{erros.categoriaId}</span>}
</div>
<button type="submit" className="btn-premium btn-primario" disabled={carregando}>
{carregando ? 'Salvando dados...' : 'Confirmar e Salvar'}
</button>
</form>
</div> ); };
export default ProdutoForm;
✅ Pré-Requisitos deste Módulo
Antes de passar para a fase final de testes, conteinerização Docker e pipelines de CI/CD, certifique-se de que:
- Os interceptadores do Axios e o estilo premium de Glassmorphism foram acoplados nos Módulos 06 e 07.
- A API NestJS está rodando localmente e possui conexão ativa com o banco Neon PostgreSQL.
🤔 Por que fizemos assim?
- Por que encapsular toda a lógica do carrinho em um Custom Hook (
useCart)? Permite reutilizar as regras de estado em qualquer componente da aplicação (como na Navbar para o contador de compras, na vitrine de produtos e na view de checkout) sem duplicar código. O hook expõe uma API limpa com funções de controle (adicionarItem,removerItem) e atributos computados que centralizam as mutações de dados de forma simples e de fácil manutenção. - Por que sincronizar o estado do carrinho de compras com o
localStoragedo navegador? Diferente de aplicações tradicionais baseadas em cookies de sessão do servidor, Single Page Applications (SPAs) sofrem perda total de variáveis reativas na memória ao recarregar o navegador (F5). Sincronizar o array de itens com olocalStoragegarante a persistência dos dados do usuário entre visitas, preservando a experiência de compra (UX). - Por que reutilizar o mesmo componente (
ProdutoForm.tsx) para o cadastro e edição de produtos? Criar componentes separados para cadastrar e editar geraria redundância de código de markup (formulário) e regras de validação idênticas. O hookuseParams()lê o parâmetro dinâmico:idda URL do React Router. Se presente, o componente inicia no modo “Editar”, carregando os valores atuais da API e salvando via requisiçãoPUT; se nulo, inicia limpo e salva viaPOST, facilitando correções visuais e atualizações de campos.
🔍 Checkpoint
- Reatividade nas Views: Adicione um produto ao carrinho na vitrine e navegue até a rota
/carrinho. Confirme se o item está listado e se a alteração de quantidade atualiza o preço total em tempo real de forma automática. - Validação de Preenchimento: Tente submeter o formulário de cadastro de produtos deixando o preço zerado ou a categoria em branco. Garanta que a aplicação impeça o envio e exiba alertas visuais abaixo dos respectivos campos.
- Transação Concluída: Finalize uma compra no carrinho e verifique se os estoques dos respectivos produtos no banco de dados (Neon) foram debitados atômicamente por meio das requisições privadas da API.
⚠️ Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
| Os itens do carrinho somem ou resetam após recarregar a tela (F5) | O useEffect responsável por recuperar os dados do local storage na inicialização do hook falhou ou a chave de armazenamento está incorreta. |
Garanta que o nome da chave usada no localStorage.getItem e setItem é exatamente igual (tecloja_carrinho) e realize o parse do JSON de forma protegida. |
Erro 400 Bad Request no checkout alegando que o clienteId não existe |
O ID de cliente enviado no payload da requisição de checkout não corresponde a nenhum registro físico no banco de dados Neon. | Certifique-se de preencher a variável clienteId com o identificador de um cliente existente semeado pelo script de seed (ex: clienteId: 1). |
| O seletor de categorias do formulário administrativo inicia vazio ou não preenche os dados | A rota pública /categorias do NestJS está inacessível ou ocorreu erro de conexões antes do componente React carregar. |
Verifique se a API do backend está ativa, garanta a correta importação e tratamento do array de categorias no useEffect de montagem e certifique-se de que o CORS está habilitado no backend. |
🏁 Conclusão
Com o carrinho reativo em funcionamento e as telas de administração e formulários de cadastros e modificações de eletrônicos finalizadas de forma brilhante, a experiência de interface do e-commerce da TecLoja 03 está totalmente pronta para interação!
No Módulo 09, entraremos na fase final do nosso curso: DevOps e Qualidade de Código. Programaremos a suíte de testes de backend utilizando o Jest, conteinerizaremos a API NestJS e o frontend React (usando Nginx de forma otimizada para rotas virtuais de SPA) com Docker Multi-Stage e configuraremos os pipelines automáticos de CI/CD do GitHub Actions.