📘 CRUD de Pessoas Físicas e Jurídicas com Armazenamento no Backend
Parte 1 — Evolução da arquitetura: do localStorage à API REST
No capítulo anterior, desenvolvemos uma aplicação ReactJS capaz de cadastrar, listar, filtrar, visualizar, editar e excluir Pessoas Físicas (PF) e Pessoas Jurídicas (PJ). Para armazenar os registros, utilizamos o localStorage do navegador, com uma camada de acesso a dados formada pelos arquivos PFDAOLocal.mjs e PJDAOLocal.mjs.
Agora, vamos evoluir a mesma aplicação para utilizar uma API REST. O objetivo não é reconstruir os formulários ou abandonar a organização já desenvolvida, mas compreender como a aplicação passa a enviar requisições a um servidor e aguardar suas respostas.
No material anterior sobre API REST de Pessoas, conhecemos os recursos disponibilizados pelo serviço:
- URL base:
https://backend-pessoas.vercel.app/ - Pessoa Física:
/pf - Pessoa Jurídica:
/pj - Métodos HTTP:
GET,POST,PUTeDELETE.
Neste capítulo, esses conhecimentos serão aplicados ao projeto ReactJS.
Não crie um novo projeto ReactJS. Continue utilizando a aplicação desenvolvida no capítulo de armazenamento local. A nova implementação será acrescentada progressivamente, preservando a versão anterior para comparação.
1. O que muda na arquitetura da aplicação?
1.1. Arquitetura utilizada no capítulo anterior
Na versão anterior, os componentes ReactJS utilizavam os DAOs locais para ler e gravar informações no navegador.
Usuário
↓
Componentes ReactJS (formulários, listagem e visualização)
↓
PFDAOLocal.mjs / PJDAOLocal.mjs
↓
localStorage do navegador
Por exemplo, ao salvar um cadastro, o DAO executava a operação de armazenamento no próprio navegador. Não era necessário enviar uma requisição HTTP a outro computador.
1.2. Arquitetura que será utilizada neste capítulo
Com a nova versão, a interface continuará sendo construída em ReactJS, mas a camada DAO passará a utilizar a API REST.
Usuário
↓
Componentes ReactJS (formulários, listagem e visualização)
↓
PFDAOBackEnd.mjs / PJDAOBackEnd.mjs
↓
Requisição HTTP (fetch)
↓
API REST de Pessoas (backend)
↓
Resposta HTTP (JSON)
↓
DAO → atualização do estado React → interface
Observe que os componentes não precisam conhecer todos os detalhes das requisições HTTP. A responsabilidade pela comunicação ficará concentrada nos novos DAOs.
A camada de apresentação é responsável pela interação com o usuário. A camada DAO é responsável por acessar os dados. Essa separação facilita a evolução da aplicação, pois podemos substituir o mecanismo de persistência sem reconstruir todas as classes de domínio e todos os formulários.
1.3. Comparando as duas versões
| Característica | Versão com localStorage | Versão com backend |
|---|---|---|
| Local de armazenamento | Navegador | Persistência gerenciada pelo servidor |
| Camada de acesso a dados | PFDAOLocal e PJDAOLocal | PFDAOBackEnd e PJDAOBackEnd |
| Consulta | Leitura local | Requisição GET |
| Cadastro | Gravação local | Requisição POST |
| Atualização | Alteração local | Requisição PUT |
| Exclusão | Remoção local | Requisição DELETE |
| Identificação dos registros | Identificadores da implementação local | Identificadores _id retornados pela API |
| Tempo de resposta | Geralmente imediato | Depende da comunicação e do processamento remoto |
| Falhas possíveis | Erros locais | Erros locais, de rede ou do servidor |
A funcionalidade percebida pelo usuário continua semelhante. Entretanto, a maneira como os dados são obtidos e persistidos muda significativamente.
2. Por que precisamos aguardar o backend?
2.1. Operações locais e operações assíncronas
Quando acessamos o localStorage, a operação é síncrona: o JavaScript executa a instrução e obtém seu resultado antes de continuar para a próxima.
const registros = JSON.parse(localStorage.getItem('pessoas') || '[]');
console.log(registros);
Na comunicação com uma API, não sabemos exatamente quanto tempo o servidor levará para responder. A requisição pode depender da conexão, do processamento remoto e da disponibilidade do serviço.
async function consultarPessoas() {
const resposta = await fetch('https://backend-pessoas.vercel.app/pf');
if (!resposta.ok) {
throw new Error('Não foi possível consultar as pessoas físicas.');
}
const pessoas = await resposta.json();
console.log(pessoas);
}
Nesse exemplo, fetch() inicia a requisição HTTP e retorna uma Promise. A palavra await, utilizada dentro de uma função async, permite aguardar o resultado antes de continuar o processamento daquela função.
Os conceitos de Promise, async/await e tratamento de exceções já foram apresentados nos estudos de JavaScript. Aqui, eles serão aplicados à integração entre os componentes ReactJS e a API REST.
2.2. O que o usuário vê enquanto aguarda?
Imagine que um estudante acesse a tela de listagem. A aplicação envia uma requisição GET /pf, mas o servidor ainda não respondeu.
Se a interface não apresentar nenhuma indicação, o usuário poderá acreditar que a aplicação está travada ou que não existem registros.
Por isso, a nova versão precisa distinguir pelo menos três situações:
- Carregando: a requisição foi iniciada e a resposta ainda não chegou.
- Sucesso: os dados foram recebidos e podem ser apresentados.
- Erro: a operação não pôde ser concluída e o usuário precisa ser informado.
Uma estratégia comum em ReactJS é utilizar um estado de carregamento:
const [loading, setLoading] = useState(false);
O componente poderá usar esse estado para apresentar o Spin do Ant Design durante o processamento. Ao final, deverá atualizar a interface ou comunicar a falha.
O estado loading não substitui a operação assíncrona. Ele apenas representa, na interface, que a operação ainda está em andamento. O código também precisa aguardar corretamente a resposta do DAO antes de utilizar os dados recebidos.
3. Evolução dos arquivos do projeto
Assim como no capítulo anterior, classificaremos os arquivos em REAPROVEITADOS, MODIFICADOS e NOVOS.
3.1. Arquivos reaproveitados
Continuaremos utilizando as classes de domínio já desenvolvidas:
src/objetos/pessoas/
├── Pessoa.mjs
├── PF.mjs
├── PJ.mjs
├── Endereco.mjs
├── Telefone.mjs
├── Titulo.mjs
└── IE.mjs
Os componentes de formulário específicos de PF e PJ e os componentes auxiliares de endereço e telefone também poderão ser reaproveitados. Não precisamos reimplementar os campos de cadastro apenas porque a persistência mudou.
Os DAOs locais e os componentes da versão anterior serão mantidos como referência para comparação.
3.2. Arquivos novos
O documento da versão com backend apresenta os seguintes arquivos principais:
| Arquivo | Responsabilidade |
|---|---|
src/objetos/dao/PFDAOBackEnd.mjs | Comunicar-se com os endpoints de Pessoa Física |
src/objetos/dao/PJDAOBackEnd.mjs | Comunicar-se com os endpoints de Pessoa Jurídica |
src/components/listapessoas/ListaPessoasBack.jsx | Listar, filtrar e excluir pessoas utilizando a API |
src/components/visualizapessoa/VisualizarPessoaBack.jsx | Consultar e apresentar detalhes de um cadastro remoto |
src/components/cadastrapessoa/PessoaFormBack.jsx | Cadastrar e editar registros utilizando o backend |
AppRoutesBack.jsx | Definir as rotas da versão integrada à API REST |
O nome Back ou BackEnd permite distinguir a implementação remota daquela utilizada anteriormente com localStorage.
3.3. Arquivos modificados ou ajustados
| Arquivo | Alteração prevista |
|---|---|
src/App.jsx | Selecionar a estrutura de rotas da versão com backend |
src/layout/MainLayout.jsx | Reaproveitar o layout, realizando ajustes apenas quando necessários à navegação |
Não altere App.jsx neste momento. Assim como fizemos no capítulo anterior, deixaremos a integração das rotas para a etapa final. Isso evita importar componentes que ainda não foram criados.
3.4. Visão da evolução
src/
├── App.jsx [MODIFICADO AO FINAL]
├── routes/
│ ├── AppRoutes.jsx [MANTIDO — versão local]
│ └── AppRoutesBack.jsx [NOVO]
├── layout/
│ └── MainLayout.jsx [REAPROVEITADO/AJUSTADO]
├── components/
│ ├── cadastrapessoa/
│ │ ├── PFForm.jsx [REAPROVEITADO]
│ │ ├── PJForm.jsx [REAPROVEITADO]
│ │ └── PessoaFormBack.jsx [NOVO]
│ ├── listapessoas/
│ │ └── ListaPessoasBack.jsx [NOVO]
│ └── visualizapessoa/
│ └── VisualizarPessoaBack.jsx [NOVO]
└── objetos/
├── pessoas/ [CLASSES REAPROVEITADAS]
└── dao/
├── PFDAOLocal.mjs [MANTIDO]
├── PJDAOLocal.mjs [MANTIDO]
├── PFDAOBackEnd.mjs [NOVO]
└── PJDAOBackEnd.mjs [NOVO]
A árvore destaca os arquivos envolvidos nesta evolução; ela não representa todos os arquivos existentes no projeto. A localização exata de
AppRoutesBack.jsxdeverá acompanhar a organização de rotas já adotada no projeto.
4. O que permanece igual e o que precisa evoluir?
Considere a operação de listar pessoas físicas.
Na versão local, a interface solicita os registros ao DAO, que os recupera do navegador. Na versão com backend, o componente continuará solicitando os registros ao DAO, mas agora precisará aguardar a consulta HTTP.
O mesmo princípio vale para o cadastro:
Versão local:
Formulário → DAO local → localStorage → atualização da interface
Versão com backend:
Formulário → DAO remoto → POST /pf ou POST /pj
↓
resposta do servidor
↓
atualização da interface
Observe que a atualização da interface deve refletir o resultado confirmado pelo servidor, e não apenas a intenção do usuário de cadastrar ou alterar um registro.
4.1. Responsabilidades da nova camada DAO
Os novos DAOs deverão concentrar a comunicação com a API REST e lidar com aspectos como:
- Montagem das URLs e seleção dos métodos HTTP;
- Envio e recebimento de dados em JSON;
- Conversão entre objetos de domínio e estruturas utilizadas pela API;
- Tratamento dos identificadores
_id; - Identificação de respostas HTTP malsucedidas;
- Gerenciamento do cache previsto na implementação original.
4.2. Responsabilidades dos componentes ReactJS
Os componentes continuarão responsáveis por formulários, tabelas, navegação e interação com o usuário. Entretanto, também precisarão:
- Controlar estados de carregamento;
- Aguardar o término das operações;
- Atualizar os dados exibidos após uma resposta bem-sucedida;
- Informar falhas de comunicação;
- Evitar que uma operação seja apresentada como concluída antes da confirmação do backend.
O documento original utiliza cache nos DAOs de backend. Esse cache é diferente do localStorage utilizado na versão anterior: ele pode manter temporariamente dados consultados, mas não deve ser confundido com a persistência definitiva do servidor. Nas próximas partes, examinaremos como esse mecanismo interfere na atualização da interface.
5. Preparação para a próxima etapa
Antes de modificar os arquivos, localize no projeto atual:
- Os DAOs
PFDAOLocal.mjsePJDAOLocal.mjs; - Os componentes de listagem, visualização e cadastro;
- O arquivo de rotas
AppRoutes.jsx; - O componente
MainLayout.jsx; - O arquivo de entrada
App.jsx.
Verifique como os componentes obtêm os dados dos DAOs e em que momento atualizam seus estados.
Nesta parte, nenhuma substituição de código é necessária. O objetivo é reconhecer a arquitetura existente antes de introduzir a comunicação assíncrona.
6. Exercício de fixação — Analisando a evolução do projeto
Considere a seguinte situação:
Na versão com
localStorage, um usuário cadastra uma Pessoa Física e a listagem é atualizada imediatamente. Na nova versão, o mesmo usuário envia o formulário, mas a resposta do servidor demora alguns segundos.
Com base na arquitetura estudada, responda:
- Qual camada será responsável por enviar os dados do cadastro ao servidor?
- Qual método HTTP será utilizado para cadastrar uma nova Pessoa Física?
- Por que o formulário não deve informar sucesso antes de receber a confirmação do servidor?
- Que informação visual poderá ser apresentada enquanto a operação estiver em andamento?
- Quais classes e componentes do projeto anterior poderão ser reaproveitados?
- Por que é interessante manter
PFDAOLocal.mjsePFDAOBackEnd.mjscomo arquivos distintos durante o estudo?
Não é necessário implementar novos códigos. Consulte a estrutura do projeto e os exemplos do capítulo anterior para fundamentar suas respostas.
7. Síntese da Parte 1
Nesta parte, compreendemos que:
- A aplicação ReactJS continuará sendo a mesma, mas utilizará outra estratégia de persistência;
- A API REST será acessada por meio dos novos DAOs de backend;
- A comunicação HTTP introduz operações assíncronas e a necessidade de aguardar respostas;
- Os componentes ReactJS precisarão representar estados de processamento, sucesso e erro;
- As classes de domínio e os componentes de formulário poderão ser reaproveitados;
- A implementação será realizada gradualmente, preservando a versão local para comparação.
Na Parte 2, estudaremos como async/await, useState, useEffect e os indicadores de carregamento serão utilizados para controlar as operações assíncronas na interface ReactJS.
Parte 2 — Comunicação assíncrona e estados de processamento no ReactJS
Na Parte 1, compreendemos como a aplicação ReactJS será modificada para substituir a persistência local pela comunicação com uma API REST.
Até o momento, os componentes do CRUD utilizam os DAOs PFDAOLocal.mjs e PJDAOLocal.mjs, responsáveis por armazenar e recuperar os registros no localStorage.
Entretanto, quando passamos a utilizar um backend, precisamos considerar uma característica importante: o resultado de uma operação não estará necessariamente disponível no momento em que ela for solicitada.
Isso significa que os componentes ReactJS precisarão aguardar o processamento realizado pelo servidor e atualizar a interface quando a resposta estiver disponível.
Nesta parte, estudaremos como realizar esse controle utilizando async/await, useState, useEffect e os componentes de feedback do Ant Design.
Nesta etapa, não substituiremos os DAOs nem os componentes existentes.
Os exemplos apresentados servirão para compreender os mecanismos de comunicação assíncrona que serão utilizados nas próximas partes, durante a implementação do CRUD com backend.
1. Relembrando o funcionamento das operações locais
Na versão anterior, os dados eram armazenados no próprio navegador.
Considere o seguinte exemplo:
const pessoas = JSON.parse(
localStorage.getItem("pessoas") || "[]"
);
console.log(pessoas);
O método getItem() realiza uma operação síncrona.
Isso significa que a execução do programa aguarda a conclusão dessa instrução antes de continuar.
O mesmo comportamento ocorre quando utilizamos:
localStorage.setItem(
"pessoas",
JSON.stringify(pessoas)
);
Embora essas operações possam levar algum tempo, o JavaScript somente prossegue depois que a instrução termina.
1.1. O que acontece quando utilizamos o backend?
Imagine que o usuário acesse a tela de listagem de pessoas.
Na nova versão, a aplicação precisará:
- Enviar uma requisição HTTP ao backend;
- Aguardar o processamento da requisição;
- Receber a resposta do servidor;
- Interpretar os dados recebidos;
- Atualizar a interface ReactJS.
Esse processamento pode levar alguns milissegundos ou vários segundos, dependendo das condições da rede e do servidor.
Portanto, não podemos tratar a comunicação HTTP da mesma maneira que uma leitura síncrona do localStorage.
2. Utilizando async/await para aguardar uma resposta
Nos estudos anteriores de JavaScript, conhecemos o funcionamento das funções assíncronas.
Agora, aplicaremos esse conhecimento à comunicação com a API REST de Pessoas Físicas e Jurídicas.
2.1. Consultando a API REST
Considere o seguinte exemplo:
async function consultarPessoas() {
const resposta = await fetch(
"https://backend-pessoas.vercel.app/pf"
);
if (!resposta.ok) {
throw new Error(
"Não foi possível consultar as pessoas físicas."
);
}
const dados = await resposta.json();
console.log(dados);
}
Observe os elementos utilizados:
| Elemento | Responsabilidade |
|---|---|
async | Define uma função assíncrona |
fetch() | Envia uma requisição HTTP |
await | Aguarda a resolução de uma Promise |
resposta.ok | Verifica se a resposta HTTP indica sucesso |
resposta.json() | Interpreta o corpo JSON da resposta |
throw new Error() | Sinaliza uma situação de erro |
O await não bloqueia toda a aplicação ReactJS enquanto a resposta é aguardada.
Ele suspende a continuidade daquela função assíncrona, permitindo que o JavaScript continue executando outras tarefas.
2.2. Por que existem dois await?
Observe:
const resposta = await fetch(url);
const dados = await resposta.json();
O primeiro await aguarda a resposta da requisição HTTP.
O segundo aguarda a leitura e interpretação do corpo da resposta no formato JSON.
Isso ocorre porque fetch() e response.json() retornam Promises.
2.3. Tratando possíveis erros
Uma requisição pode falhar por diferentes motivos, como problemas de conexão, indisponibilidade do servidor ou respostas HTTP que indicam erro.
Podemos utilizar try/catch para tratar essas situações:
async function consultarPessoas() {
try {
const resposta = await fetch(
"https://backend-pessoas.vercel.app/pf"
);
if (!resposta.ok) {
throw new Error(
`Erro HTTP: ${resposta.status}`
);
}
const dados = await resposta.json();
console.log(dados);
} catch (erro) {
console.error(
"Erro ao consultar pessoas:",
erro.message
);
}
}
Observe que o fetch() não rejeita automaticamente sua Promise quando o servidor retorna, por exemplo, um erro HTTP 404 ou 500.
Por isso, verificamos resposta.ok antes de utilizar os dados.
Neste exemplo, a requisição HTTP está sendo realizada diretamente na função consultarPessoas() para facilitar a compreensão.
Na implementação definitiva do projeto, essa responsabilidade ficará concentrada nos arquivos PFDAOBackEnd.mjs e PJDAOBackEnd.mjs.
Os componentes ReactJS utilizarão os métodos disponibilizados por esses DAOs.
3. Gerenciando estados de processamento no ReactJS
Na versão anterior, utilizamos useState para controlar informações dos formulários, filtros e listas.
Agora, utilizaremos o mesmo recurso para representar diferentes situações de uma operação assíncrona.
Considere os seguintes estados:
const [pessoas, setPessoas] = useState([]);
const [loading, setLoading] = useState(false);
const [erro, setErro] = useState(null);
Cada estado possui uma responsabilidade:
| Estado | Finalidade |
|---|---|
pessoas | Armazenar os registros recebidos |
loading | Indicar se existe uma operação em andamento |
erro | Armazenar uma mensagem de falha |
3.1. Compreendendo o estado loading
Quando uma operação é iniciada, podemos definir:
setLoading(true);
Quando a operação termina, definimos:
setLoading(false);
Entretanto, é importante garantir que o estado seja atualizado mesmo quando ocorrer algum erro.
Para isso, utilizaremos finally.
async function carregarPessoas() {
setLoading(true);
try {
// Aguarda a consulta ao backend.
} catch (erro) {
// Trata uma possível falha.
} finally {
setLoading(false);
}
}
O bloco finally será executado tanto quando a operação for concluída com sucesso quanto quando ocorrer uma exceção.
Não devemos executar setLoading(false) imediatamente após iniciar uma requisição sem aguardá-la.
Nesse caso, o indicador de carregamento poderá desaparecer antes que o servidor tenha respondido.
4. Utilizando useEffect para carregar dados
No ReactJS, o useEffect permite executar efeitos relacionados ao funcionamento do componente.
Uma aplicação frequente é realizar o carregamento inicial dos dados quando um componente é montado.
Considere:
import { useEffect, useState } from "react";
function ExemploConsulta() {
const [pessoas, setPessoas] = useState([]);
useEffect(() => {
async function carregarPessoas() {
const resposta = await fetch(
"https://backend-pessoas.vercel.app/pf"
);
if (!resposta.ok) {
throw new Error("Falha na consulta.");
}
const dados = await resposta.json();
setPessoas(dados);
}
carregarPessoas().catch(console.error);
}, []);
return (
<div>
<p>Consulta de pessoas físicas</p>
</div>
);
}
export default ExemploConsulta;
Observe:
useEffect(() => {
// Código executado pelo efeito.
}, []);
O array vazio indica que o efeito não depende de valores reativos que precisem provocar sua reexecução.
Em uma montagem normal, ele será executado após a renderização inicial do componente.
Quando o projeto utiliza StrictMode, o React pode executar um ciclo adicional de configuração e limpeza dos efeitos durante o desenvolvimento.
Isso pode fazer com que uma consulta inicial seja observada mais de uma vez. Esse comportamento ajuda a identificar efeitos que não realizam corretamente sua limpeza.
4.1. Por que não declarar o useEffect como async?
Observe a implementação abaixo:
// Não recomendado
useEffect(async () => {
const resposta = await fetch(url);
}, []);
Essa abordagem não é adequada porque a função passada ao useEffect deve retornar uma função de limpeza ou não retornar nada.
Uma função async sempre retorna uma Promise.
Por isso, utilizamos uma função assíncrona dentro do efeito:
useEffect(() => {
async function carregar() {
// Operação assíncrona.
}
carregar();
}, []);
5. Construindo um exemplo de consulta com feedback visual
Agora, vamos reunir os conceitos apresentados.
O exemplo a seguir demonstra como utilizar useState, useEffect, async/await, try/catch/finally e o componente Spin do Ant Design.
Este componente é independente e tem finalidade demonstrativa.
Ainda não representa a implementação definitiva de ListaPessoasBack.jsx, que será desenvolvida posteriormente utilizando os DAOs do projeto.
Para evitar depender do formato interno dos registros retornados pela API, apresentaremos apenas a quantidade de elementos recebidos, quando a resposta for um array.
import { useEffect, useState } from "react";
import { Alert, Card, Spin, Typography } from "antd";
const { Title, Text } = Typography;
function ExemploConsultaBackend() {
const [pessoas, setPessoas] = useState([]);
const [loading, setLoading] = useState(true);
const [erro, setErro] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function carregarPessoas() {
setLoading(true);
setErro(null);
try {
const resposta = await fetch(
"https://backend-pessoas.vercel.app/pf",
{ signal: controller.signal }
);
if (!resposta.ok) {
throw new Error(
`Erro HTTP: ${resposta.status}`
);
}
const dados = await resposta.json();
if (!Array.isArray(dados)) {
throw new Error(
"Formato de resposta inesperado."
);
}
if (!controller.signal.aborted) {
setPessoas(dados);
}
} catch (error) {
if (!controller.signal.aborted) {
setErro(error.message);
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
carregarPessoas();
return () => {
controller.abort();
};
}, []);
return (
<Card>
<Title level={4}>
Consulta de Pessoas Físicas
</Title>
{loading ? (
<Spin tip="Consultando pessoas..." size="large">
<div style={{ minHeight: 100 }} />
</Spin>
) : erro ? (
<Alert
type="error"
title="Erro na consulta"
description={erro}
showIcon
/>
) : (
<Text>
Total de registros recebidos: {pessoas.length}
</Text>
)}
</Card>
);
}
export default ExemploConsultaBackend;
5.1. Analisando o funcionamento
Primeiro: o componente é renderizado com loading igual a true.
const [loading, setLoading] = useState(true);
Segundo: o useEffect inicia a consulta ao backend.
const resposta = await fetch(
"https://backend-pessoas.vercel.app/pf",
{ signal: controller.signal }
);
Terceiro: enquanto a consulta está em andamento, o componente apresenta o indicador de carregamento.
<Spin tip="Consultando pessoas..." size="large">
<div style={{ minHeight: 100 }} />
</Spin>
Quarto: quando a resposta é recebida e interpretada, o estado pessoas é atualizado.
setPessoas(dados);
Quinto: o bloco finally encerra o estado de carregamento, desde que a requisição não tenha sido cancelada.
setLoading(false);
Se ocorrer algum erro, o componente apresenta uma mensagem utilizando Alert.
5.2. Por que utilizamos AbortController?
O AbortController permite cancelar a requisição quando o efeito é encerrado.
Isso é útil, por exemplo, quando o usuário sai da tela antes de o servidor responder.
return () => {
controller.abort();
};
Assim, evitamos que uma requisição antiga continue sendo utilizada para atualizar um componente que já não precisa daquele resultado.
Esse cuidado será especialmente relevante nas telas de consulta e visualização detalhada.
6. Aplicando o conceito às operações CRUD
O gerenciamento de estados assíncronos não será utilizado apenas na listagem.
Ele também será necessário nas operações de cadastro, atualização e exclusão.
6.1. Cadastro de uma pessoa
Imagine que o usuário preencha o formulário e clique no botão Salvar.
O componente deverá:
- Validar os campos do formulário;
- Iniciar o estado de processamento;
- Solicitar ao DAO o envio dos dados;
- Aguardar a confirmação do backend;
- Informar o resultado da operação;
- Encerrar o estado de processamento.
Considere o seguinte exemplo conceitual:
async function salvarPessoa(pessoa) {
setLoading(true);
try {
await dao.salvar(pessoa);
message.success(
"Pessoa cadastrada com sucesso!"
);
} catch (erro) {
message.error(
"Não foi possível cadastrar a pessoa."
);
} finally {
setLoading(false);
}
}
O exemplo pressupõe que dao.salvar() retorne uma Promise que somente seja resolvida após a confirmação da operação e que rejeite em caso de falha.
Essa condição deverá ser observada na implementação dos novos DAOs.
Não devemos apresentar a mensagem de sucesso imediatamente após chamar dao.salvar() sem aguardar a conclusão da operação.
O servidor pode rejeitar o cadastro ou ocorrer uma falha de comunicação.
6.2. Desabilitando o botão durante o processamento
Outra melhoria importante é impedir que o usuário envie a mesma operação repetidamente enquanto aguarda a resposta.
O Ant Design permite associar o estado loading a um botão:
<Button
type="primary"
htmlType="submit"
loading={loading}
>
Salvar
</Button>
Enquanto loading estiver ativo, o botão apresentará um indicador de processamento e impedirá novos cliques.
Isso ajuda a evitar envios duplicados, embora o backend também deva tratar adequadamente a possibilidade de requisições repetidas.
6.3. Exclusão de um registro
Na exclusão, também será necessário aguardar a confirmação do servidor antes de atualizar a listagem.
O fluxo será:
Usuário solicita exclusão
↓
Confirma a operação
↓
Componente solicita exclusão ao DAO
↓
DAO envia DELETE ao backend
↓
Aguarda a resposta
↓
┌──────┴──────┐
↓ ↓
Sucesso Erro
↓ ↓
Atualiza Informa
listagem a falha
Observe que a confirmação do usuário e a confirmação do servidor são momentos diferentes.
A primeira autoriza a tentativa de exclusão. A segunda informa o resultado efetivo da operação.
7. Relacionando os conceitos aos arquivos do projeto
Os conhecimentos apresentados nesta parte serão aplicados nos seguintes arquivos:
| Arquivo | Aplicação dos conceitos |
|---|---|
PFDAOBackEnd.mjs | fetch, async/await, respostas HTTP e tratamento de erros |
PJDAOBackEnd.mjs | Comunicação assíncrona com os endpoints de PJ |
ListaPessoasBack.jsx | useEffect, estados de carregamento, consulta e exclusão |
VisualizarPessoaBack.jsx | Carregamento assíncrono de um registro específico |
PessoaFormBack.jsx | Envio assíncrono, edição, feedback e controle dos botões |
A divisão de responsabilidades continuará sendo:
Componente ReactJS
│
├── Controla os estados da interface
│
├── Apresenta carregamento e mensagens
│
└── Solicita operações ao DAO
│
▼
DAO de Backend
│
├── Monta a requisição HTTP
├── Envia os dados
├── Aguarda a resposta
├── Trata o resultado
└── Retorna os dados ou sinaliza erro
Os componentes ReactJS não precisam implementar diretamente todas as requisições HTTP.
Nas próximas partes, a comunicação será encapsulada nos DAOs. Os componentes serão responsáveis por utilizar os resultados dessas operações para atualizar a interface.
8. Exercício de fixação — Compreendendo o processamento assíncrono
Considere o código:
async function carregarPessoas() {
setLoading(true);
try {
const dados = await dao.carregarLista();
setPessoas(dados);
} catch (erro) {
setErro(erro.message);
} finally {
setLoading(false);
}
}
Considere que dao.carregarLista() é um método assíncrono que retorna os registros consultados no backend.
Responda:
1. Qual é a finalidade da instrução setLoading(true)?
2. Por que utilizamos await antes de chamar dao.carregarLista()?
3. O que poderá acontecer se removermos o await dessa instrução?
4. Qual é a responsabilidade do bloco catch?
5. Por que a instrução setLoading(false) foi colocada dentro de finally?
6. Como o estado loading poderá ser utilizado em um componente Table ou Button do Ant Design?
7. Na versão com localStorage, por que não era necessário utilizar esse mesmo fluxo assíncrono para consultar os dados locais?
8.1. Atividade prática complementar
Utilizando o exemplo ExemploConsultaBackend, realize os seguintes testes:
- Execute a consulta e observe o indicador de carregamento.
- Utilize as ferramentas de desenvolvedor do navegador para inspecionar a requisição HTTP.
- Na aba Network, simule uma conexão mais lenta e observe o comportamento da interface.
- Altere temporariamente a URL para um endereço inválido e observe o tratamento do erro.
- Restaure a URL original e verifique se a consulta volta a funcionar.
O objetivo é observar o comportamento assíncrono da aplicação.
Não é necessário modificar os DAOs locais ou substituir os componentes do CRUD já desenvolvido.
9. Síntese da Parte 2
Nesta parte, compreendemos que:
- As operações realizadas com
localStoragesão síncronas; - As requisições HTTP utilizam operações assíncronas;
async/awaitpermite organizar o código que depende de respostas futuras;try/catch/finallyauxilia no tratamento de erros e no encerramento do processamento;useStatepermite controlar os dados, o carregamento e as mensagens de erro;useEffectpode ser utilizado para iniciar consultas quando um componente é montado;Spin,AlerteButtonpermitem apresentar feedback ao usuário;- Os componentes ReactJS devem aguardar a confirmação do backend antes de apresentar uma operação como concluída.
Com esses conhecimentos, estamos preparados para iniciar a evolução da camada de persistência.
Na Parte 3 — Evoluindo a camada DAO para utilização da API REST, implementaremos os arquivos PFDAOBackEnd.mjs e PJDAOBackEnd.mjs, comparando seus métodos com os DAOs locais e explicando como as operações CRUD passam a utilizar requisições HTTP.
Parte 3 — Evoluindo a camada DAO para utilização da API REST
Na Parte 2, estudamos como o ReactJS pode controlar operações assíncronas utilizando async/await, useState, useEffect e mecanismos de tratamento de erros.
Agora, vamos aplicar esses conhecimentos à evolução da camada de persistência do projeto.
Na versão anterior, os dados eram armazenados no navegador, utilizando os arquivos:
PFDAOLocal.mjsPJDAOLocal.mjs
Nesta parte, adicionaremos duas novas implementações:
PFDAOBackEnd.mjsPJDAOBackEnd.mjs
Esses arquivos permitirão que nossa aplicação realize operações CRUD utilizando a API REST apresentada no capítulo anterior.
Não crie um novo projeto ReactJS.
Continue utilizando o projeto desenvolvido nas etapas anteriores. Os DAOs locais deverão ser preservados, pois serão utilizados para comparar as duas estratégias de persistência.
Nesta etapa, adicionaremos os novos arquivos, mas ainda não modificaremos os componentes ReactJS.
1. Relembrando a responsabilidade da camada DAO
No capítulo anterior, utilizamos o padrão DAO (Data Access Object) para separar a lógica de persistência dos componentes ReactJS.
Os DAOs locais eram responsáveis por armazenar, consultar, atualizar e excluir registros utilizando o localStorage.
Agora, manteremos essa separação, mas utilizaremos requisições HTTP.
VERSÃO ANTERIOR
Componente ReactJS
↓
PFDAOLocal / PJDAOLocal
↓
localStorage
NOVA VERSÃO
Componente ReactJS
↓
PFDAOBackEnd / PJDAOBackEnd
↓
fetch() — Requisições HTTP
↓
API REST de Pessoas
↓
Persistência gerenciada pelo backend
Observe que a responsabilidade da camada DAO permanece semelhante.
O que muda é a maneira como os dados são acessados.
1.1. Métodos que serão implementados
Os dois novos DAOs terão operações equivalentes.
| Método | Responsabilidade | Método HTTP |
|---|---|---|
carregarLista() | Consultar os registros no backend | GET |
listar() | Obter a lista disponível | GET, quando necessário |
buscarPorId(id) | Consultar um registro específico | GET |
salvar(pessoa) | Cadastrar uma pessoa | POST |
atualizar(id, pessoa) | Atualizar um cadastro | PUT |
excluir(id) | Excluir um cadastro | DELETE |
mapPF() / mapPJ() | Adaptar o JSON recebido | — |
toPlain() | Converter os dados para envio | — |
Os métodos mapPF(), mapPJ() e toPlain() não realizam requisições HTTP. Eles são responsáveis pela conversão dos dados.
2. A principal mudança: métodos assíncronos
Na versão local, era possível obter os dados armazenados no navegador imediatamente:
const pessoas = dao.listar();
Na nova implementação, a consulta poderá depender da resposta do servidor.
Por isso, utilizaremos:
const pessoas = await dao.listar();
Essa diferença exige atenção.
O método listar() passará a retornar uma Promise, cujo resultado estará disponível quando a operação for concluída.
O DAO apresentado no documento de referência utiliza um método listar() síncrono, que retorna imediatamente o cache e, quando necessário, inicia uma consulta em segundo plano.
Essa estratégia pode fazer com que o componente ReactJS receba uma lista vazia antes da conclusão da consulta.
Para manter a coerência com os conceitos estudados na Parte 2, adotaremos nesta implementação um método listar() assíncrono, que aguarda carregarLista().
Também utilizaremos exceções para informar falhas aos componentes, em vez de ocultar os erros.
Esses são ajustes didáticos e técnicos em relação ao código original, preservando sua organização, seus métodos principais e o mecanismo de cache.
3. Criando o DAO de Pessoa Física
3.1. Arquivo que será adicionado
Na pasta:
src/objetos/dao/
Crie o arquivo:
PFDAOBackEnd.mjs
Situação: NOVO ARQUIVO.
Não substitua PFDAOLocal.mjs.
3.2. Código completo — PFDAOBackEnd.mjs
Copie o código abaixo para o arquivo criado.
// src/objetos/dao/PFDAOBackEnd.mjs
export default class PFDAO {
constructor() {
this.baseUrl = "https://backend-pessoas.vercel.app/pf";
this.cache = [];
this.cacheCarregado = false;
}
// Consulta os registros no backend
async carregarLista() {
const resposta = await fetch(this.baseUrl);
if (!resposta.ok) {
throw new Error(
`Erro ao listar PFs: ${resposta.status}`
);
}
const dados = await resposta.json();
if (!Array.isArray(dados)) {
throw new Error(
"Formato inesperado na consulta de PFs."
);
}
this.cache = dados.map((pf) => this.mapPF(pf));
this.cacheCarregado = true;
return [...this.cache];
}
// Retorna os registros disponíveis
async listar() {
if (!this.cacheCarregado) {
return await this.carregarLista();
}
return [...this.cache];
}
// Cadastra uma Pessoa Física
async salvar(pf) {
const objeto = this.toPlain(pf);
const resposta = await fetch(this.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(objeto)
});
if (!resposta.ok) {
throw new Error(
`Erro ao salvar PF: ${resposta.status}`
);
}
const dados = await resposta.json();
const novaPF = this.mapPF(dados);
this.cache.push(novaPF);
return novaPF;
}
// Atualiza uma Pessoa Física
async atualizar(id, pf) {
const objeto = this.toPlain(pf);
const resposta = await fetch(
`${this.baseUrl}/${encodeURIComponent(id)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(objeto)
}
);
if (!resposta.ok) {
throw new Error(
`Erro ao atualizar PF: ${resposta.status}`
);
}
const dados = await resposta.json();
const atualizada = this.mapPF(dados);
const indice = this.cache.findIndex(
(pessoa) => pessoa.id === id
);
if (indice >= 0) {
this.cache[indice] = atualizada;
} else {
this.cache.push(atualizada);
}
return atualizada;
}
// Exclui uma Pessoa Física
async excluir(id) {
const resposta = await fetch(
`${this.baseUrl}/${encodeURIComponent(id)}`,
{
method: "DELETE"
}
);
if (!resposta.ok) {
throw new Error(
`Erro ao excluir PF: ${resposta.status}`
);
}
this.cache = this.cache.filter(
(pessoa) => pessoa.id !== id
);
return true;
}
// Busca uma Pessoa Física pelo identificador
async buscarPorId(id) {
console.log("ID recebido:", id);
// 1. Procura inicialmente no cache
const existente = this.cache.find(
(pessoa) => String(pessoa.id) === String(id)
);
if (existente) {
return existente;
}
// 2. Se não encontrou, carrega a lista do backend
const lista = await this.carregarLista();
// 3. Procura o registro na lista carregada
const pessoa = lista.find(
(item) => String(item.id) === String(id)
);
// 4. Retorna o registro encontrado ou null
return pessoa ?? null;
}
// Converte JSON recebido em estrutura utilizada no frontend
mapPF(pf) {
return {
id: pf._id,
nome: pf.nome,
email: pf.email,
cpf: pf.cpf,
endereco: pf.endereco
? {
cep: pf.endereco.cep,
logradouro: pf.endereco.logradouro,
bairro: pf.endereco.bairro,
cidade: pf.endereco.cidade,
uf: pf.endereco.uf,
regiao: pf.endereco.regiao
}
: {},
telefones: (pf.telefones || []).map(
(telefone) => ({
ddd: telefone.ddd,
numero: telefone.numero
})
),
titulo: pf.titulo
? {
numero: pf.titulo.numero,
zona: pf.titulo.zona,
secao: pf.titulo.secao
}
: {}
};
}
// Converte objeto PF em estrutura JSON
toPlain(pf) {
if (!pf) {
return {};
}
const endereco = pf.getEndereco?.();
const titulo = pf.getTitulo?.();
const telefones = pf.getTelefones?.() || [];
return {
nome: pf.getNome?.(),
email: pf.getEmail?.(),
cpf: pf.getCPF?.(),
endereco: endereco
? {
cep: endereco.getCep?.(),
logradouro: endereco.getLogradouro?.(),
bairro: endereco.getBairro?.(),
cidade: endereco.getCidade?.(),
uf: endereco.getUf?.(),
regiao: endereco.getRegiao?.()
}
: {},
telefones: telefones.map(
(telefone) => ({
ddd: telefone.getDdd?.(),
numero: telefone.getNumero?.()
})
),
titulo: titulo
? {
numero: titulo.getNumero?.(),
zona: titulo.getZona?.(),
secao: titulo.getSecao?.()
}
: {}
};
}
}
O código utiliza os campos e os métodos de conversão previstos para esta versão didática.
Ele pressupõe que os objetos enviados para salvar() e atualizar() sejam instâncias das classes de domínio utilizadas no projeto, com métodos como getNome(), getEndereco() e getTitulo().
Também pressupõe que as respostas de cadastro e atualização retornem o registro criado ou atualizado, incluindo seu identificador _id.
Essas correspondências deverão ser verificadas com as classes existentes e com o contrato da API antes dos testes de escrita.
4. Compreendendo o funcionamento do PFDAOBackEnd
4.1. O construtor
constructor() {
this.baseUrl = "https://backend-pessoas.vercel.app/pf";
this.cache = [];
this.cacheCarregado = false;
}
O construtor inicializa três atributos.
baseUrl identifica o endereço utilizado nas requisições de Pessoa Física.
cache armazena temporariamente os registros recebidos.
cacheCarregado informa se a lista completa já foi consultada.
Observe que o construtor não inicia automaticamente uma requisição HTTP.
A consulta ocorrerá quando o componente solicitar os registros.
4.2. O método carregarLista()
async carregarLista() {
const resposta = await fetch(this.baseUrl);
if (!resposta.ok) {
throw new Error("Erro ao consultar pessoas.");
}
const dados = await resposta.json();
this.cache = dados.map(
(pf) => this.mapPF(pf)
);
this.cacheCarregado = true;
return [...this.cache];
}
Esse método realiza uma requisição GET, interpreta os dados recebidos e atualiza o cache.
O uso de mapPF() permite transformar cada registro retornado pelo backend em uma estrutura conhecida pelos componentes ReactJS.
4.3. O método listar()
async listar() {
if (!this.cacheCarregado) {
return await this.carregarLista();
}
return [...this.cache];
}
Observe que o método verifica se a lista já foi carregada.
Se ainda não foi, aguarda a consulta.
Caso contrário, retorna uma cópia do cache atual.
Uma lista vazia não significa necessariamente que os dados ainda não foram consultados.
O backend pode retornar corretamente uma lista sem registros.
Por isso, utilizamos cacheCarregado para distinguir essas situações.
4.4. O método salvar()
No cadastro, utilizamos POST.
const resposta = await fetch(this.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(objeto)
});
A propriedade body contém os dados que serão enviados ao backend.
Como os dados devem ser transmitidos em JSON, utilizamos JSON.stringify().
A operação somente será considerada concluída quando o servidor responder adequadamente.
4.5. O método atualizar()
Na atualização, utilizamos PUT.
A principal diferença em relação ao cadastro é que precisamos informar qual registro será modificado.
const resposta = await fetch(
`${this.baseUrl}/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(objeto)
}
);
O identificador recebido do backend é utilizado na URL.
4.6. O método excluir()
A exclusão utiliza o método HTTP DELETE.
const resposta = await fetch(
`${this.baseUrl}/${id}`,
{
method: "DELETE"
}
);
Após a confirmação do servidor, removemos o registro do cache.
Dessa forma, o cache acompanha o resultado da operação.
4.7. Tratamento de erros
Observe:
if (!resposta.ok) {
throw new Error("Erro ao excluir PF");
}
A exceção será propagada para o código que chamou o DAO.
Isso permite que o componente ReactJS apresente uma mensagem adequada ao usuário.
Por exemplo:
try {
await dao.excluir(id);
message.success("Pessoa excluída com sucesso!");
} catch (erro) {
message.error("Não foi possível excluir a pessoa.");
}
O exemplo acima será aplicado de maneira completa quando implementarmos a listagem de pessoas.
5. Criando o DAO de Pessoa Jurídica
Agora, implementaremos o DAO responsável pela comunicação com os endpoints de Pessoa Jurídica.
A estrutura será semelhante à utilizada em PFDAOBackEnd.mjs.
As principais diferenças estão nos atributos específicos de Pessoa Jurídica, como CNPJ e Inscrição Estadual.
5.1. Arquivo que será adicionado
Na pasta:
src/objetos/dao/
Crie o arquivo:
PJDAOBackEnd.mjs
Situação: NOVO ARQUIVO.
Não substitua PJDAOLocal.mjs.
5.2. Código completo — PJDAOBackEnd.mjs
// src/objetos/dao/PJDAOBackEnd.mjs
export default class PJDAO {
constructor() {
this.baseUrl = "https://backend-pessoas.vercel.app/pj";
this.cache = [];
this.cacheCarregado = false;
}
// Consulta os registros no backend
async carregarLista() {
const resposta = await fetch(this.baseUrl);
if (!resposta.ok) {
throw new Error(
`Erro ao listar PJs: ${resposta.status}`
);
}
const dados = await resposta.json();
if (!Array.isArray(dados)) {
throw new Error(
"Formato inesperado na consulta de PJs."
);
}
this.cache = dados.map((pj) => this.mapPJ(pj));
this.cacheCarregado = true;
return [...this.cache];
}
// Retorna os registros disponíveis
async listar() {
if (!this.cacheCarregado) {
return await this.carregarLista();
}
return [...this.cache];
}
// Cadastra uma Pessoa Jurídica
async salvar(pj) {
const objeto = this.toPlain(pj);
const resposta = await fetch(this.baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(objeto)
});
if (!resposta.ok) {
throw new Error(
`Erro ao salvar PJ: ${resposta.status}`
);
}
const dados = await resposta.json();
const novaPJ = this.mapPJ(dados);
this.cache.push(novaPJ);
return novaPJ;
}
// Atualiza uma Pessoa Jurídica
async atualizar(id, pj) {
const objeto = this.toPlain(pj);
const resposta = await fetch(
`${this.baseUrl}/${encodeURIComponent(id)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(objeto)
}
);
if (!resposta.ok) {
throw new Error(
`Erro ao atualizar PJ: ${resposta.status}`
);
}
const dados = await resposta.json();
const atualizada = this.mapPJ(dados);
const indice = this.cache.findIndex(
(pessoa) => pessoa.id === id
);
if (indice >= 0) {
this.cache[indice] = atualizada;
} else {
this.cache.push(atualizada);
}
return atualizada;
}
// Exclui uma Pessoa Jurídica
async excluir(id) {
const resposta = await fetch(
`${this.baseUrl}/${encodeURIComponent(id)}`,
{
method: "DELETE"
}
);
if (!resposta.ok) {
throw new Error(
`Erro ao excluir PJ: ${resposta.status}`
);
}
this.cache = this.cache.filter(
(pessoa) => pessoa.id !== id
);
return true;
}
// Busca uma Pessoa Jurídica pelo identificador
async buscarPorId(id) {
console.log("ID recebido:", id);
// 1. Procura inicialmente no cache
const existente = this.cache.find(
(pessoa) => String(pessoa.id) === String(id)
);
if (existente) {
return existente;
}
// 2. Se não encontrou, carrega a lista do backend
const lista = await this.carregarLista();
// 3. Procura o registro na lista carregada
const pessoa = lista.find(
(item) => String(item.id) === String(id)
);
// 4. Retorna o registro encontrado ou null
return pessoa ?? null;
}
// Converte JSON recebido em estrutura utilizada no frontend
mapPJ(pj) {
return {
id: pj._id,
nome: pj.nome,
email: pj.email,
cnpj: pj.cnpj,
endereco: pj.endereco
? {
cep: pj.endereco.cep,
logradouro: pj.endereco.logradouro,
bairro: pj.endereco.bairro,
cidade: pj.endereco.cidade,
uf: pj.endereco.uf,
regiao: pj.endereco.regiao
}
: {},
telefones: (pj.telefones || []).map(
(telefone) => ({
ddd: telefone.ddd,
numero: telefone.numero
})
),
ie: pj.ie
? {
numero: pj.ie.numero,
estado: pj.ie.estado,
dataRegistro: pj.ie.dataRegistro
}
: {}
};
}
// Converte objeto PJ em estrutura JSON
toPlain(pj) {
if (!pj) {
return {};
}
const endereco = pj.getEndereco?.();
const ie = pj.getIE?.();
const telefones = pj.getTelefones?.() || [];
return {
nome: pj.getNome?.(),
email: pj.getEmail?.(),
cnpj: pj.getCNPJ?.(),
endereco: endereco
? {
cep: endereco.getCep?.(),
logradouro: endereco.getLogradouro?.(),
bairro: endereco.getBairro?.(),
cidade: endereco.getCidade?.(),
uf: endereco.getUf?.(),
regiao: endereco.getRegiao?.()
}
: {},
telefones: telefones.map(
(telefone) => ({
ddd: telefone.getDdd?.(),
numero: telefone.getNumero?.()
})
),
ie: ie
? {
numero: ie.getNumero?.(),
estado: ie.getEstado?.(),
dataRegistro: ie.getDataRegistro?.()
}
: {}
};
}
}
6. Comparando os DAOs de PF e PJ
Observe que os dois arquivos possuem uma estrutura semelhante.
| Elemento | PFDAOBackEnd | PJDAOBackEnd |
|---|---|---|
| Endpoint | /pf | /pj |
| Identificador recebido | _id | _id |
| Documento principal | CPF | CNPJ |
| Atributo específico | Título de Eleitor | Inscrição Estadual |
| Conversão de resposta | mapPF() | mapPJ() |
| Conversão para envio | toPlain() | toPlain() |
| Métodos CRUD | GET, POST, PUT, DELETE | GET, POST, PUT, DELETE |
Essa semelhança demonstra como a organização orientada a objetos facilita a manutenção de implementações relacionadas.
Apesar de trabalharem com entidades diferentes, os DAOs seguem o mesmo padrão de comunicação.
7. Compreendendo a conversão dos objetos
Um aspecto importante da nova implementação é a diferença entre os objetos utilizados no frontend e as estruturas JSON transmitidas pela API.
7.1. Objetos de domínio
No projeto anterior, desenvolvemos classes como PF, PJ, Endereco, Telefone, Titulo e IE.
Essas classes possuem atributos e métodos que representam os elementos do domínio da aplicação.
Por exemplo:
pf.getNome();
pf.getCPF();
pf.getEndereco();
Entretanto, quando enviamos uma requisição HTTP, precisamos transmitir os dados em um formato que possa ser interpretado pelo backend.
7.2. O método toPlain()
O método toPlain() transforma os dados obtidos dos objetos de domínio em um objeto JavaScript simples, adequado à serialização em JSON.
Por exemplo:
const dados = dao.toPlain(pf);
console.log(dados);
O resultado poderá apresentar uma estrutura semelhante a:
{
"nome": "Maria Silva",
"email": "maria@exemplo.com",
"cpf": "12345678900",
"endereco": {
"cep": "72000000",
"logradouro": "Rua Exemplo",
"bairro": "Centro",
"cidade": "Brasília",
"uf": "DF",
"regiao": "Centro-Oeste"
},
"telefones": [
{
"ddd": "61",
"numero": "999999999"
}
],
"titulo": {
"numero": "123456789012",
"zona": "10",
"secao": "20"
}
}
Esse objeto poderá ser convertido para JSON utilizando:
JSON.stringify(dados);
7.3. O método mapPF()
O processo inverso acontece quando recebemos os dados do backend.
A API utiliza o atributo _id para identificar os registros.
Entretanto, os componentes do frontend utilizam o atributo id.
O método mapPF() realiza essa adaptação:
mapPF(pf) {
return {
id: pf._id,
nome: pf.nome,
email: pf.email,
cpf: pf.cpf
};
}
O exemplo acima apresenta apenas os atributos principais. O método completo já foi implementado no DAO.
O frontend não precisa utilizar exatamente a mesma representação interna do backend.
A camada DAO pode adaptar os dados recebidos e enviados, preservando a organização das demais partes da aplicação.
8. Verificando os arquivos adicionados
Ao concluir esta parte, a pasta DAO deverá conter os arquivos anteriores e os novos:
src/
└── objetos/
└── dao/
├── PFDAOLocal.mjs
├── PJDAOLocal.mjs
├── PFDAOBackEnd.mjs
└── PJDAOBackEnd.mjs
Os arquivos locais continuam disponíveis.
Os novos DAOs já possuem os métodos necessários para consultar, cadastrar, atualizar e excluir registros por meio da API REST.
Entretanto, os componentes ReactJS ainda não foram modificados para utilizá-los.
Isso será realizado nas próximas partes.
8.1. Verificação inicial sem modificar a interface
Para verificar a comunicação de leitura, podemos utilizar temporariamente o console do navegador.
Com o projeto em execução, abra as ferramentas de desenvolvedor e utilize um módulo importado dinamicamente.
Ajuste o caminho conforme a organização e o endereço de execução do projeto:
const modulo = await import(
"/src/objetos/dao/PFDAOBackEnd.mjs"
);
const PFDAO = modulo.default;
const dao = new PFDAO();
const pessoas = await dao.listar();
console.log(pessoas);
Esse teste consulta a lista de Pessoas Físicas utilizando o novo DAO.
Se a requisição for concluída com sucesso, o resultado será apresentado no console.
Caso a API esteja indisponível ou ocorra uma falha de comunicação, será apresentada uma mensagem de erro.
Neste momento, recomendamos testar apenas as operações de consulta.
As operações de cadastro, atualização e exclusão modificam dados no backend e serão verificadas de maneira orientada quando os respectivos componentes ReactJS forem implementados.
Também é importante lembrar que a API utilizada neste projeto é um serviço remoto compartilhado, não um armazenamento isolado no navegador de cada estudante.
9. Exercício de fixação — Analisando a evolução dos DAOs
Considere os códigos implementados nesta parte e responda:
1. Qual é a principal diferença entre PFDAOLocal.mjs e PFDAOBackEnd.mjs?
2. Por que os métodos listar(), salvar(), atualizar() e excluir() foram implementados como funções assíncronas?
3. Qual é a finalidade da propriedade baseUrl?
4. Por que utilizamos JSON.stringify() nas operações POST e PUT?
5. Qual é a diferença entre os métodos toPlain() e mapPF()?
6. Por que o método listar() precisa aguardar a conclusão de carregarLista() quando o cache ainda não foi carregado?
7. Qual é a finalidade do atributo cacheCarregado?
8. O que acontecerá se um componente ReactJS chamar dao.listar() sem utilizar await ou outro mecanismo para tratar a Promise retornada?
9.1. Atividade prática
Após adicionar os arquivos PFDAOBackEnd.mjs e PJDAOBackEnd.mjs, realize as seguintes atividades:
- Execute o projeto ReactJS.
- Abra as ferramentas de desenvolvedor do navegador.
- Utilize o exemplo de importação dinâmica para consultar Pessoas Físicas.
- Repita a operação utilizando
PJDAOBackEnd.mjs. - Observe as requisições na aba Network.
- Identifique o método HTTP utilizado, o código de resposta e o formato dos dados recebidos.
- Compare os atributos retornados pela API com aqueles apresentados pelos métodos
mapPF()emapPJ().
Não é necessário modificar os componentes ReactJS nesta atividade.
10. Síntese da Parte 3
Nesta parte, evoluímos a camada DAO do projeto ReactJS.
Foram adicionados dois arquivos completos:
PFDAOBackEnd.mjs, responsável pela comunicação com os endpoints de Pessoa Física;PJDAOBackEnd.mjs, responsável pela comunicação com os endpoints de Pessoa Jurídica.
Compreendemos como utilizar fetch, async/await, métodos HTTP, JSON, tratamento de erros e cache para implementar operações CRUD.
Também identificamos a importância de converter os dados entre os objetos de domínio e as estruturas utilizadas pela API REST.
Os DAOs locais foram preservados, permitindo comparar as duas estratégias de persistência.
Na Parte 4 — Evoluindo a listagem de pessoas, implementaremos o componente ListaPessoasBack.jsx, utilizando os novos DAOs para consultar, filtrar e excluir registros.
Será nesse momento que aplicaremos os estados de carregamento, o useEffect e os componentes de feedback do Ant Design estudados na Parte 2.
Parte 4 — Evoluindo a listagem de pessoas: consultas assíncronas, filtros e exclusão
Na Parte 3, adicionamos os arquivos PFDAOBackEnd.mjs e PJDAOBackEnd.mjs, responsáveis pela comunicação com a API REST.
Agora, vamos utilizar esses DAOs para implementar uma nova versão do componente de listagem de pessoas.
No capítulo anterior, desenvolvemos ListaPessoas.jsx, que utilizava os DAOs locais para consultar, filtrar e excluir registros.
Nesta etapa, criaremos ListaPessoasBack.jsx, preservando as funcionalidades já conhecidas e incorporando o gerenciamento de operações assíncronas.
Não substitua ListaPessoas.jsx.
A nova implementação será adicionada ao mesmo projeto ReactJS. Assim, será possível comparar o funcionamento da listagem com localStorage e com o backend.
Nesta parte, ainda não modificaremos as rotas principais da aplicação. Essa integração será realizada ao final do capítulo.
1. Relembrando o componente de listagem anterior
Na versão com armazenamento local, o componente de listagem permitia:
- Alternar entre Pessoas Físicas e Jurídicas;
- Consultar os registros cadastrados;
- Filtrar pessoas pelo nome;
- Visualizar os dados de uma pessoa;
- Acessar a edição de um cadastro;
- Excluir registros mediante confirmação.
Essas funcionalidades serão preservadas.
Entretanto, precisamos considerar que a consulta e a exclusão passarão a depender da comunicação com um servidor remoto.
1.1. Comparando as implementações
| Operação | Versão local | Versão com backend |
|---|---|---|
| Listagem | Consulta ao DAO local | Consulta assíncrona à API |
| Filtro por nome | Filtragem dos dados locais | Filtragem dos dados recebidos |
| Visualização | Navegação para o registro local | Navegação para consulta remota |
| Edição | Utilização dos dados locais | Carregamento remoto do cadastro |
| Exclusão | Remoção no localStorage | Requisição HTTP DELETE |
| Atualização da tabela | Após operação local | Após confirmação da operação remota |
| Indicador de carregamento | Não era essencial | Necessário para informar a espera |
| Tratamento de erros | Erros locais | Falhas de comunicação e respostas HTTP |
Observe que a principal diferença está no momento em que os dados estarão disponíveis.
Na versão anterior, a consulta era síncrona. Agora, precisamos aguardar o resultado da operação assíncrona.
2. Como funcionará a nova listagem?
A nova versão continuará utilizando os componentes do Ant Design apresentados anteriormente:
Table: apresentação dos registros;Select: seleção entre PF e PJ;Input: filtro pelo nome;Button: atualização e ações sobre os registros;Popconfirm: confirmação da exclusão;Space: organização dos elementos.
Acrescentaremos o Spin para representar o carregamento e o Alert para informar falhas.
Também utilizaremos:
useState, para controlar os dados e os estados da interface;useEffect, para iniciar o carregamento;useNavigate, para acessar as telas de visualização e edição;useRef, para preservar as instâncias dos DAOs e controlar consultas concorrentes.
2.1. Fluxo de consulta
Usuário acessa a listagem
↓
Componente identifica o tipo (PF ou PJ)
↓
Ativa o estado de carregamento
↓
Seleciona o DAO correspondente
↓
DAO envia GET para a API REST
↓
Componente aguarda a resposta
↓
┌──────┴──────┐
↓ ↓
Sucesso Erro
↓ ↓
Aplica filtro Apresenta
e atualiza mensagem
a tabela de erro
↓ ↓
Encerra o estado de carregamento
O componente ReactJS não precisará conhecer os detalhes da requisição HTTP.
Ele solicitará os dados ao DAO, aguardará o resultado e atualizará a interface.
A implementação de fetch, a interpretação do JSON e o tratamento dos códigos HTTP continuarão concentrados na camada DAO.
3. Criando o componente ListaPessoasBack.jsx
3.1. Arquivo que será adicionado
Na pasta:
src/components/listapessoas/
Crie o arquivo:
ListaPessoasBack.jsx
Situação: NOVO ARQUIVO.
Não substitua ListaPessoa.jsx.
3.2. Código completo — ListaPessoasBack.jsx
Copie o código abaixo para o arquivo criado.
// src/components/listapessoas/ListaPessoasBack.jsx
import { useEffect, useRef, useState } from "react";
import {
Table,
Button,
Space,
Popconfirm,
message,
Input,
Select,
Spin,
Alert
} from "antd";
import {
EyeOutlined,
EditOutlined,
DeleteOutlined
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import PFDAO from "../../objetos/dao/PFDAOBackEnd.mjs";
import PJDAO from "../../objetos/dao/PJDAOBackEnd.mjs";
export default function ListaPessoasBack() {
const navigate = useNavigate();
// Estados da interface
const [tipo, setTipo] = useState("PF");
const [filtroNome, setFiltroNome] = useState("");
const [dados, setDados] = useState([]);
const [loading, setLoading] = useState(true);
const [excluindoId, setExcluindoId] = useState(null);
const [erro, setErro] = useState(null);
// Preserva as instâncias dos DAOs entre renderizações
const pfDAO = useRef(new PFDAO()).current;
const pjDAO = useRef(new PJDAO()).current;
// Identifica a consulta mais recente
const consultaAtual = useRef(0);
// Seleciona o DAO correspondente
function obterDAO(tipoPessoa) {
return tipoPessoa === "PF" ? pfDAO : pjDAO;
}
// Consulta os registros no backend
async function carregarLista(tipoPessoa = tipo) {
const numeroConsulta = ++consultaAtual.current;
setLoading(true);
setErro(null);
try {
const dao = obterDAO(tipoPessoa);
// Aguarda o carregamento dos registros
const lista = await dao.carregarLista();
// Atualiza somente se esta for a consulta mais recente
if (numeroConsulta === consultaAtual.current) {
setDados(lista);
}
} catch (error) {
if (numeroConsulta === consultaAtual.current) {
setDados([]);
setErro(error.message);
}
} finally {
if (numeroConsulta === consultaAtual.current) {
setLoading(false);
}
}
}
// Carrega os dados ao montar o componente
// e quando o tipo de pessoa é alterado
useEffect(() => {
carregarLista(tipo);
return () => {
consultaAtual.current++;
};
}, [tipo]);
// Aplica o filtro sobre os dados já carregados
const dadosFiltrados = dados.filter((pessoa) => {
const nome = pessoa.nome || "";
return nome
.toLowerCase()
.includes(filtroNome.toLowerCase());
});
// Exclui um registro no backend
async function excluirPessoa(id) {
setExcluindoId(id);
try {
const dao = obterDAO(tipo);
// Aguarda a confirmação da exclusão
await dao.excluir(id);
message.success(
"Registro excluído com sucesso!"
);
// Atualiza a tabela somente após a exclusão
await carregarLista(tipo);
} catch (error) {
message.error(
"Não foi possível excluir o registro."
);
setErro(error.message);
} finally {
setExcluindoId(null);
}
}
// Define as colunas da tabela
const colunas = [
{
title: "Nome",
dataIndex: "nome",
key: "nome"
},
{
title: "Email",
dataIndex: "email",
key: "email"
},
{
title: tipo === "PF" ? "CPF" : "CNPJ",
dataIndex: tipo === "PF" ? "cpf" : "cnpj",
key: "documento",
width: 200
},
{
title: "Ações",
key: "acoes",
render: (_, record) => (
<Space>
<Button
icon={<EyeOutlined />}
title="Visualizar"
onClick={() =>
navigate(
`/visualizar/${tipo}/${record.id}`
)
}
/>
<Button
icon={<EditOutlined />}
title="Editar"
onClick={() =>
navigate(
`/editar/${tipo}/${record.id}`
)
}
/>
<Popconfirm
title="Deseja realmente excluir?"
description="Esta operação não poderá ser desfeita."
okText="Sim"
cancelText="Não"
onConfirm={() => excluirPessoa(record.id)}
>
<Button
danger
icon={<DeleteOutlined />}
title="Excluir"
loading={excluindoId === record.id}
disabled={excluindoId !== null}
/>
</Popconfirm>
</Space>
)
}
];
return (
<div
style={{
maxWidth: 1000,
margin: "24px auto",
background: "#fff",
padding: 24,
borderRadius: 8,
boxShadow: "0 2px 8px rgba(0,0,0,0.05)"
}}
>
<h2
style={{
textAlign: "center",
marginBottom: 20
}}
>
Listagem de Pessoas
</h2>
<Space
wrap
style={{ marginBottom: 20 }}
>
<Select
value={tipo}
onChange={setTipo}
style={{ width: 200 }}
options={[
{
value: "PF",
label: "Pessoa Física"
},
{
value: "PJ",
label: "Pessoa Jurídica"
}
]}
/>
<Input
placeholder="Filtrar por nome"
value={filtroNome}
onChange={(e) =>
setFiltroNome(e.target.value)
}
allowClear
style={{ width: 300 }}
/>
<Button
type="primary"
loading={loading}
onClick={() => carregarLista(tipo)}
>
Atualizar
</Button>
</Space>
{erro && (
<Alert
type="error"
title="Erro na operação"
description={erro}
showIcon
closable
style={{ marginBottom: 20 }}
onClose={() => setErro(null)}
/>
)}
{loading ? (
<div
style={{
textAlign: "center",
marginTop: 40,
marginBottom: 40
}}
>
<Spin
size="large"
tip="Consultando pessoas..."
>
<div style={{ minHeight: 100 }} />
</Spin>
</div>
) : (
<Table
dataSource={dadosFiltrados}
columns={colunas}
rowKey="id"
pagination={{ pageSize: 6 }}
loading={excluindoId !== null}
/>
)}
</div>
);
}
O componente original já utilizava useEffect, useState, Spin, Table e os novos DAOs.
Nesta versão, realizamos alguns ajustes para adequar o componente à implementação assíncrona da Parte 3:
carregarLista()aguarda os registros retornados pelo DAO.excluirPessoa()utilizaasync/awaitpara aguardar a exclusão.try/catch/finallycontrola os erros e o encerramento das operações.useRefpreserva as instâncias dos DAOs entre renderizações.- O filtro por nome é aplicado sobre os dados já carregados.
- Um identificador de consulta evita que respostas antigas substituam resultados de consultas mais recentes.
4. Compreendendo os estados utilizados
Observe os estados definidos no início do componente:
const [tipo, setTipo] = useState("PF");
const [filtroNome, setFiltroNome] = useState("");
const [dados, setDados] = useState([]);
const [loading, setLoading] = useState(true);
const [excluindoId, setExcluindoId] = useState(null);
const [erro, setErro] = useState(null);
Cada estado possui uma responsabilidade específica.
| Estado | Responsabilidade |
|---|---|
tipo | Identificar se a listagem será de PF ou PJ |
filtroNome | Armazenar o texto utilizado no filtro |
dados | Armazenar os registros recebidos |
loading | Informar se a consulta está em andamento |
excluindoId | Identificar o registro em processo de exclusão |
erro | Armazenar uma mensagem de erro |
4.1. Por que utilizar estados diferentes para consulta e exclusão?
Imagine que o usuário esteja visualizando uma tabela e solicite a exclusão de um registro.
Nesse momento, não precisamos necessariamente ocultar toda a tabela.
Podemos manter os registros visíveis e apresentar um indicador de processamento enquanto a exclusão é realizada.
Por isso, utilizamos:
const [loading, setLoading] = useState(true);
const [excluindoId, setExcluindoId] = useState(null);
O primeiro estado representa a consulta.
O segundo identifica uma operação de exclusão.
Essa separação permite apresentar um feedback mais específico ao usuário.
5. Preservando as instâncias dos DAOs
Observe:
const pfDAO = useRef(new PFDAO()).current;
const pjDAO = useRef(new PJDAO()).current;
Na Parte 3, implementamos DAOs que possuem um cache interno.
Se criássemos uma nova instância do DAO em todas as renderizações, esse cache seria reinicializado constantemente.
Por isso, utilizamos useRef para manter uma instância durante a existência do componente.
O useRef permite armazenar uma referência entre renderizações sem provocar uma nova renderização quando seu valor é alterado.
Neste exemplo, utilizamos esse recurso para preservar os objetos DAO.
5.1. Selecionando o DAO
Criamos uma função auxiliar:
function obterDAO(tipoPessoa) {
return tipoPessoa === "PF"
? pfDAO
: pjDAO;
}
Assim, os métodos de consulta e exclusão não precisam repetir a lógica de seleção do DAO.
Quando tipoPessoa for "PF", utilizaremos PFDAOBackEnd.
Quando for "PJ", utilizaremos PJDAOBackEnd.
6. Carregando a lista de maneira assíncrona
A principal função do componente é carregarLista().
Observe o trecho:
async function carregarLista(tipoPessoa = tipo) {
const numeroConsulta = ++consultaAtual.current;
setLoading(true);
setErro(null);
try {
const dao = obterDAO(tipoPessoa);
const lista = await dao.carregarLista();
if (numeroConsulta === consultaAtual.current) {
setDados(lista);
}
} catch (error) {
if (numeroConsulta === consultaAtual.current) {
setDados([]);
setErro(error.message);
}
} finally {
if (numeroConsulta === consultaAtual.current) {
setLoading(false);
}
}
}
O processamento ocorre em etapas.
Primeira etapa: o componente informa que a consulta começou.
setLoading(true);
Segunda etapa: identifica qual DAO deverá ser utilizado.
const dao = obterDAO(tipoPessoa);
Terceira etapa: aguarda a consulta ao backend.
const lista = await dao.carregarLista();
Quarta etapa: atualiza os registros apresentados.
setDados(lista);
Quinta etapa: encerra o estado de carregamento.
setLoading(false);
Essa última instrução está dentro de finally, garantindo que o processamento seja encerrado mesmo quando ocorrer uma falha, desde que a consulta ainda seja a mais recente.
6.1. Por que utilizar um identificador de consulta?
Considere a seguinte situação:
- O usuário seleciona Pessoa Física.
- A aplicação inicia uma consulta ao backend.
- Antes de a resposta chegar, o usuário seleciona Pessoa Jurídica.
- Uma segunda consulta é iniciada.
- A resposta de Pessoa Jurídica chega primeiro.
- A resposta antiga de Pessoa Física chega depois.
Sem um controle adicional, a resposta antiga poderia substituir os registros da consulta mais recente.
Para evitar esse problema, utilizamos:
const consultaAtual = useRef(0);
A cada nova consulta, incrementamos seu identificador:
const numeroConsulta = ++consultaAtual.current;
Antes de atualizar os estados, verificamos se a resposta pertence à consulta mais recente:
if (numeroConsulta === consultaAtual.current) {
setDados(lista);
}
Esse problema ocorre porque as respostas de operações assíncronas não precisam chegar na mesma ordem em que as requisições foram iniciadas.
O identificador evita que uma resposta antiga altere a interface, embora não cancele a requisição HTTP que já foi enviada.
7. Utilizando useEffect para iniciar a consulta
Observe:
useEffect(() => {
carregarLista(tipo);
return () => {
consultaAtual.current++;
};
}, [tipo]);
O efeito será executado quando o componente for montado e sempre que o valor de tipo for alterado.
Isso significa que:
- Ao abrir a listagem, os dados de PF serão consultados;
- Ao selecionar PJ, uma nova consulta será realizada;
- Ao retornar para PF, os dados serão consultados novamente.
A função de limpeza invalida consultas anteriores quando o efeito é encerrado.
7.1. Por que o filtroNome não está nas dependências?
Na implementação original, o filtro por nome também participava do carregamento.
Entretanto, isso provocava uma nova consulta ao backend sempre que o usuário digitava uma letra.
Considere a palavra:
MARIA
Se cada alteração do filtro provocasse uma requisição, poderiam ser iniciadas várias consultas consecutivas.
Nesta versão, os dados são consultados quando o tipo muda ou quando o usuário solicita uma atualização.
O filtro é aplicado localmente sobre os registros recebidos.
Essa estratégia reduz a quantidade de requisições HTTP.
8. Aplicando o filtro por nome
Observe:
const dadosFiltrados = dados.filter((pessoa) => {
const nome = pessoa.nome || "";
return nome
.toLowerCase()
.includes(filtroNome.toLowerCase());
});
O método filter() percorre os registros armazenados no estado dados.
Para cada pessoa, verifica se o nome contém o texto informado pelo usuário.
Utilizamos toLowerCase() para permitir comparações sem diferenciar letras maiúsculas e minúsculas.
Por exemplo:
Nome cadastrado: Maria Silva
Filtro: maria
Resultado: encontrado
Filtro: SILVA
Resultado: encontrado
Filtro: joão
Resultado: não encontrado
Embora os dados sejam obtidos por meio de uma API REST, o filtro apresentado nesta etapa é aplicado no frontend.
Isso significa que o componente consulta os registros e depois utiliza JavaScript para selecionar aqueles que correspondem ao filtro.
Em aplicações com grandes volumes de dados, o filtro poderá ser realizado diretamente pelo backend. Entretanto, manteremos a abordagem utilizada no projeto anterior para facilitar a comparação entre as implementações.
9. Excluindo registros no backend
Na versão anterior, a exclusão removia os dados do localStorage.
Agora, a exclusão depende de uma requisição HTTP DELETE.
Observe:
async function excluirPessoa(id) {
setExcluindoId(id);
try {
const dao = obterDAO(tipo);
await dao.excluir(id);
message.success(
"Registro excluído com sucesso!"
);
await carregarLista(tipo);
} catch (error) {
message.error(
"Não foi possível excluir o registro."
);
setErro(error.message);
} finally {
setExcluindoId(null);
}
}
A principal diferença está nesta instrução:
await dao.excluir(id);
O componente aguarda a confirmação do DAO antes de apresentar a mensagem de sucesso.
Somente depois da exclusão, a lista é atualizada.
9.1. Por que aguardar a exclusão?
Imagine que o usuário solicite a exclusão de uma pessoa, mas o servidor esteja indisponível.
Se a aplicação apresentar imediatamente uma mensagem de sucesso, o usuário poderá acreditar que o registro foi removido.
Entretanto, a operação poderá não ter sido realizada.
Por isso, a mensagem de sucesso somente deverá ser apresentada depois que o backend confirmar a operação.
9.2. Confirmação da exclusão
Continuaremos utilizando Popconfirm:
<Popconfirm
title="Deseja realmente excluir?"
description="Esta operação não poderá ser desfeita."
okText="Sim"
cancelText="Não"
onConfirm={() => excluirPessoa(record.id)}
>
<Button
danger
icon={<DeleteOutlined />}
loading={excluindoId === record.id}
disabled={excluindoId !== null}
/>
</Popconfirm>
Esse componente solicita a confirmação do usuário antes de iniciar a exclusão.
O atributo loading indica qual registro está sendo processado.
A confirmação do usuário e a confirmação do backend são eventos diferentes.
O usuário confirma que deseja excluir.
O servidor confirma se a exclusão foi realizada.
A aplicação deve considerar os dois momentos.
10. Apresentando o indicador de carregamento
Na Parte 2, estudamos o componente Spin do Ant Design.
Agora, ele será utilizado na listagem real do projeto.
Observe:
{loading ? (
<div
style={{
textAlign: "center",
marginTop: 40,
marginBottom: 40
}}
>
<Spin
size="large"
tip="Consultando pessoas..."
>
<div style={{ minHeight: 100 }} />
</Spin>
</div>
) : (
<Table
dataSource={dadosFiltrados}
columns={colunas}
rowKey="id"
pagination={{ pageSize: 6 }}
/>
)}
A expressão condicional verifica o valor de loading.
Quando for true, o componente apresenta o indicador de carregamento.
Quando for false, apresenta a tabela.
Dessa forma, o usuário recebe uma informação visual sobre o processamento da consulta.
11. Tratando erros de comunicação
Além do indicador de carregamento, precisamos informar quando uma operação não puder ser concluída.
Para isso, utilizamos o estado:
const [erro, setErro] = useState(null);
Quando ocorre uma exceção, armazenamos sua mensagem:
catch (error) {
setErro(error.message);
}
A interface utiliza o componente Alert:
{erro && (
<Alert
type="error"
title="Erro na operação"
description={erro}
showIcon
closable
onClose={() => setErro(null)}
/>
)}
Assim, o usuário poderá identificar que a consulta ou a operação solicitada não foi concluída corretamente.
11.1. Por que não utilizar apenas console.error()?
O console é importante para o desenvolvedor, mas não representa um mecanismo adequado de comunicação com o usuário final.
Em uma aplicação ReactJS, devemos apresentar mensagens compreensíveis na própria interface.
O Alert permite informar problemas persistentes, enquanto message pode apresentar notificações breves sobre o resultado das operações.
12. Verificando a evolução dos arquivos
Após concluir esta parte, o projeto deverá conter:
src/
├── components/
│ └── listapessoas/
│ ├── ListaPessoas.jsx [MANTIDO]
│ └── ListaPessoasBack.jsx [NOVO]
│
└── objetos/
└── dao/
├── PFDAOLocal.mjs [MANTIDO]
├── PJDAOLocal.mjs [MANTIDO]
├── PFDAOBackEnd.mjs [CRIADO NA PARTE 3]
└── PJDAOBackEnd.mjs [CRIADO NA PARTE 3]
O componente ListaPessoasBack.jsx já está preparado para utilizar os novos DAOs.
Entretanto, ainda não modificamos as rotas da aplicação.
Por isso, o novo componente não aparecerá automaticamente no menu do sistema.
A integração definitiva ocorrerá na Parte 7, depois da implementação dos demais componentes.
Os botões já utilizam os caminhos:
/visualizar/:tipo/:id/editar/:tipo/:id
Esses caminhos serão associados aos novos componentes nas próximas partes.
Até que as rotas sejam integradas, os botões poderão não apresentar o comportamento esperado na versão com backend.
13. Exercício de fixação — Analisando a listagem assíncrona
Considere o componente ListaPessoasBack.jsx implementado nesta parte.
Responda:
1. Qual é a principal diferença entre o carregamento de dados em ListaPessoas.jsx e ListaPessoasBack.jsx?
2. Por que utilizamos await na chamada de dao.carregarLista()?
3. Qual é a finalidade do estado loading?
4. Por que o filtro por nome não precisa realizar uma nova requisição HTTP a cada alteração?
5. Qual é a responsabilidade do estado excluindoId?
6. Por que a mensagem de sucesso da exclusão somente deve ser apresentada depois da resposta do DAO?
7. Qual é a finalidade de utilizar try/catch/finally nas operações de consulta e exclusão?
8. Por que utilizamos useRef para preservar as instâncias dos DAOs?
9. O que poderá acontecer se duas consultas forem iniciadas em sequência e a primeira terminar depois da segunda?
10. Qual é a finalidade do identificador consultaAtual?
13.1. Atividade prática complementar
Considere o código desenvolvido e realize as seguintes atividades:
- Localize o método
carregarLista()e identifique onde a aplicação aguarda a resposta do backend. - Identifique os estados responsáveis por controlar a consulta, o filtro e a exclusão.
- Explique por que o componente utiliza
dao.carregarLista()em vez de acessar diretamentefetch(). - Identifique como o componente diferencia Pessoas Físicas e Jurídicas.
- Analise o funcionamento do filtro e explique por que ele é executado sobre o estado
dados. - Identifique o trecho que impede a apresentação de uma resposta antiga após uma consulta mais recente.
Nesta etapa, o objetivo é compreender a implementação do novo componente e verificar se o arquivo foi corretamente adicionado ao projeto.
Os testes completos de navegação, visualização, edição e exclusão serão realizados após a integração das rotas.
14. Síntese da Parte 4
Nesta parte, implementamos o arquivo ListaPessoasBack.jsx, responsável pela listagem de Pessoas Físicas e Jurídicas utilizando a API REST.
O componente preserva as funcionalidades desenvolvidas anteriormente, mas incorpora mecanismos necessários à comunicação assíncrona.
Compreendemos como:
- Utilizar os novos DAOs para consultar registros;
- Aguardar o carregamento dos dados;
- Apresentar indicadores visuais de processamento;
- Aplicar filtros sobre os registros recebidos;
- Excluir registros somente após confirmação do backend;
- Tratar possíveis erros de comunicação;
- Evitar que respostas antigas substituam resultados mais recentes;
- Preservar as instâncias dos DAOs durante as renderizações.
O arquivo ListaPessoas.jsx foi mantido, permitindo comparar as implementações local e remota.
Na Parte 5 — Evoluindo a visualização detalhada, implementaremos VisualizarPessoaBack.jsx, responsável por consultar um registro específico no backend e apresentar seus atributos na interface ReactJS.
Parte 5 — Evoluindo a visualização detalhada de pessoas
Na Parte 4, implementamos o componente ListaPessoasBack.jsx, responsável pela consulta, filtragem e exclusão de Pessoas Físicas e Jurídicas utilizando a API REST.
Agora, vamos desenvolver a tela de visualização detalhada dos registros.
No capítulo anterior, criamos o componente VisualizarPessoa.jsx, que apresentava os dados de uma pessoa armazenada no localStorage.
Nesta etapa, criaremos o componente VisualizarPessoaBack.jsx, que utilizará os novos DAOs para recuperar as informações diretamente do backend.
O objetivo é preservar a apresentação das informações já desenvolvida, incorporando o tratamento necessário às operações assíncronas.
Não substitua o componente de visualização utilizado na versão com armazenamento local.
Adicionaremos VisualizarPessoaBack.jsx ao projeto existente.
Os DAOs PFDAOBackEnd.mjs e PJDAOBackEnd.mjs, implementados na Parte 3, serão utilizados para consultar os registros.
A integração definitiva com as rotas será realizada na Parte 7.
1. Relembrando a visualização detalhada
Na versão anterior, o componente de visualização apresentava os dados completos de uma Pessoa Física ou Jurídica.
As informações eram organizadas utilizando o componente Descriptions do Ant Design.
Entre os atributos apresentados, destacavam-se:
- Nome e e-mail;
- CPF ou CNPJ;
- Endereço;
- Telefones;
- Título Eleitoral, para Pessoa Física;
- Inscrição Estadual, para Pessoa Jurídica.
Também disponibilizamos botões para editar o cadastro e retornar à listagem.
Essas funcionalidades serão preservadas.
1.1. O que muda na nova versão?
| Característica | Versão local | Versão com backend |
|---|---|---|
| Origem dos dados | localStorage | API REST |
| Consulta | DAO local | DAO de backend |
| Identificação | Identificador do registro | Identificador retornado pela API |
| Tempo de resposta | Geralmente imediato | Depende da comunicação HTTP |
| Carregamento | Dados disponíveis após consulta local | Necessário aguardar a resposta |
| Tratamento de erros | Principalmente erros locais | Erros de rede e respostas HTTP |
| Apresentação | Descriptions | Descriptions |
Observe que a apresentação dos dados permanecerá praticamente igual.
A principal mudança estará no processo de recuperação do registro.
2. Como funcionará a consulta de uma pessoa?
Na Parte 4, utilizamos carregarLista() para consultar todos os registros.
Entretanto, a tela de visualização detalhada precisa apresentar apenas uma pessoa.
Para isso, utilizaremos o método buscarPorId(id), implementado nos novos DAOs.
O processamento seguirá esta sequência:
Usuário seleciona Visualizar
↓
React Router identifica a rota
↓
useParams() recupera tipo e id
↓
Componente seleciona PFDAO ou PJDAO
↓
Ativa o estado de carregamento
↓
DAO executa buscarPorId(id)
↓
Requisição GET ao backend
↓
Componente aguarda a resposta
↓
┌─────┼─────┐
↓ ↓ ↓
Sucesso Não Erro
achou
↓ ↓ ↓
Exibe Informa Apresenta
dados ausência mensagem
↓ ↓ ↓
Encerra o carregamento
Não será necessário implementar uma nova requisição HTTP diretamente no componente.
O método buscarPorId() já foi desenvolvido na Parte 3 e concentra a lógica necessária para consultar um registro específico.
O componente ReactJS ficará responsável por solicitar a consulta, aguardar o resultado e apresentar os dados.
3. Recuperando os parâmetros da rota
Na Parte 4, os botões de visualização foram implementados utilizando useNavigate().
Observe:
navigate(`/visualizar/${tipo}/${record.id}`);
Esse código permite acessar uma URL semelhante a:
/visualizar/PF/68f123456789abcdef012345
Nesse exemplo:
visualizaridentifica a funcionalidade;PFinforma o tipo de pessoa;68f123456789abcdef012345representa um identificador ilustrativo do registro.
Para recuperar essas informações, utilizaremos useParams().
import { useParams } from "react-router-dom";
const { tipo, id } = useParams();
O React Router disponibiliza os valores definidos na URL.
Assim, o componente poderá identificar qual DAO deverá ser utilizado e qual registro deverá ser consultado.
Na versão com backend, o identificador do registro é retornado pela API.
Na Parte 3, utilizamos os métodos mapPF() e mapPJ() para adaptar o atributo _id, recebido do servidor, ao atributo id, utilizado pelos componentes ReactJS.
Dessa forma, os componentes continuam trabalhando com pessoa.id.
4. Criando o componente VisualizarPessoaBack.jsx
4.1. Arquivo que será adicionado
Na pasta:
src/components/visualizapessoa/
Crie o arquivo:
VisualizarPessoaBack.jsx
Situação: NOVO ARQUIVO.
Não substitua VisualizarPessoa.jsx.
4.2. Código completo — VisualizarPessoaBack.jsx
Copie o código abaixo para o arquivo criado.
// src/components/visualizapessoa/VisualizarPessoaBack.jsx
import { useEffect, useState } from "react";
import {
Card,
Descriptions,
Button,
Spin,
Alert,
Space
} from "antd";
import {
useParams,
useNavigate
} from "react-router-dom";
import PFDAO from "../../objetos/dao/PFDAOBackEnd.mjs";
import PJDAO from "../../objetos/dao/PJDAOBackEnd.mjs";
export default function VisualizarPessoaBack() {
const { tipo, id } = useParams();
const navigate = useNavigate();
// Estados da interface
const [pessoa, setPessoa] = useState(null);
const [loading, setLoading] = useState(true);
const [erro, setErro] = useState(null);
// Carrega os dados quando tipo ou id são alterados
useEffect(() => {
let ativo = true;
async function carregarPessoa() {
setLoading(true);
setPessoa(null);
setErro(null);
try {
// Verifica o tipo informado na rota
if (tipo !== "PF" && tipo !== "PJ") {
throw new Error(
"Tipo de pessoa inválido."
);
}
// Seleciona o DAO correspondente
const dao = tipo === "PF"
? new PFDAO()
: new PJDAO();
// Aguarda a consulta ao backend
const encontrada = await dao.buscarPorId(id);
// Atualiza somente se o efeito ainda estiver ativo
if (ativo) {
setPessoa(encontrada);
}
} catch (error) {
if (ativo) {
setErro(error.message);
}
} finally {
if (ativo) {
setLoading(false);
}
}
}
carregarPessoa();
// Evita atualizações de efeitos encerrados
return () => {
ativo = false;
};
}, [tipo, id]);
// Enquanto aguarda a resposta do backend
if (loading) {
return (
<div
style={{
textAlign: "center",
marginTop: 50
}}
>
<Spin
size="large"
tip="Consultando cadastro..."
>
<div style={{ minHeight: 100 }} />
</Spin>
</div>
);
}
// Quando ocorre uma falha na consulta
if (erro) {
return (
<div
style={{
maxWidth: 800,
margin: "40px auto"
}}
>
<Alert
type="error"
title="Erro ao consultar cadastro"
description={erro}
showIcon
/>
<div
style={{
textAlign: "center",
marginTop: 24
}}
>
<Button
type="primary"
onClick={() => navigate("/listar")}
>
Voltar à lista
</Button>
</div>
</div>
);
}
// Quando o registro não é encontrado
if (!pessoa) {
return (
<div
style={{
textAlign: "center",
marginTop: 40
}}
>
<h3>Nenhuma pessoa encontrada.</h3>
<Button
type="primary"
onClick={() => navigate("/listar")}
>
Voltar à lista
</Button>
</div>
);
}
// Apresenta os dados recebidos
return (
<div
style={{
maxWidth: 800,
margin: "24px auto",
background: "#fff",
padding: 24,
borderRadius: 8,
boxShadow: "0 2px 8px rgba(0,0,0,0.05)"
}}
>
<Card
title={
`Detalhes da ${
tipo === "PF"
? "Pessoa Física"
: "Pessoa Jurídica"
}`
}
variant="borderless"
>
<Descriptions
bordered
column={1}
>
<Descriptions.Item label="Nome">
{pessoa.nome}
</Descriptions.Item>
<Descriptions.Item label="E-mail">
{pessoa.email}
</Descriptions.Item>
{tipo === "PF" ? (
<Descriptions.Item label="CPF">
{pessoa.cpf}
</Descriptions.Item>
) : (
<Descriptions.Item label="CNPJ">
{pessoa.cnpj}
</Descriptions.Item>
)}
<Descriptions.Item label="Endereço">
{pessoa.endereco?.logradouro || "Não informado"}
{pessoa.endereco?.bairro
? `, ${pessoa.endereco.bairro}`
: ""}
{" - "}
{pessoa.endereco?.cidade || ""}
{pessoa.endereco?.uf
? `/${pessoa.endereco.uf}`
: ""}
<div>
CEP: {pessoa.endereco?.cep || "Não informado"}
</div>
<div>
Região: {pessoa.endereco?.regiao || "Não informada"}
</div>
</Descriptions.Item>
<Descriptions.Item label="Telefones">
{pessoa.telefones?.length > 0
? pessoa.telefones
.map(
(telefone) =>
`(${telefone.ddd}) ${telefone.numero}`
)
.join(" | ")
: "Não informado"}
</Descriptions.Item>
{tipo === "PF" ? (
<Descriptions.Item label="Título Eleitoral">
{pessoa.titulo?.numero
? `Nº ${pessoa.titulo.numero} - Zona ${
pessoa.titulo.zona
} / Seção ${pessoa.titulo.secao}`
: "Não informado"}
</Descriptions.Item>
) : (
<Descriptions.Item label="Inscrição Estadual">
{pessoa.ie?.numero
? `Nº ${pessoa.ie.numero} - ${
pessoa.ie.estado
} (${pessoa.ie.dataRegistro})`
: "Não informado"}
</Descriptions.Item>
)}
</Descriptions>
<div
style={{
textAlign: "center",
marginTop: 24
}}
>
<Space>
<Button
type="primary"
onClick={() =>
navigate(
`/editar/${tipo}/${pessoa.id}`
)
}
>
Editar
</Button>
<Button
onClick={() => navigate("/listar")}
>
Voltar
</Button>
</Space>
</div>
</Card>
</div>
);
}
O componente original realiza o carregamento completo da lista de pessoas e utiliza find() para localizar o registro solicitado.
Nesta versão, utilizamos diretamente dao.buscarPorId(id), aproveitando o método implementado na Parte 3.
Também acrescentamos:
- Tratamento de erros com
try/catch/finally; - Validação do tipo de pessoa informado na URL;
- Limpeza dos estados antes de uma nova consulta;
- Proteção contra atualização de estados após o encerramento do efeito;
- Mensagens específicas para erro e registro não encontrado.
A apresentação das informações continua baseada no componente Descriptions do Ant Design, conforme o documento original.
5. Compreendendo os estados utilizados
Observe:
const [pessoa, setPessoa] = useState(null);
const [loading, setLoading] = useState(true);
const [erro, setErro] = useState(null);
Esses estados representam as informações necessárias para controlar a visualização.
| Estado | Responsabilidade |
|---|---|
pessoa | Armazenar o registro recebido do backend |
loading | Indicar se a consulta está em andamento |
erro | Armazenar uma possível mensagem de falha |
5.1. Por que pessoa começa com null?
Quando o componente é iniciado, ainda não sabemos se o registro existe.
Por isso, utilizamos:
const [pessoa, setPessoa] = useState(null);
Após a consulta, esse estado poderá receber um objeto contendo os dados da pessoa.
Caso o DAO não encontre o registro, continuará com o valor null.
5.2. Por que loading começa com true?
Observe:
const [loading, setLoading] = useState(true);
O componente será inicialmente apresentado enquanto a consulta ainda não foi concluída.
Assim, evitamos exibir prematuramente a mensagem de que nenhuma pessoa foi encontrada.
Somente após o término da consulta será possível identificar se existe um registro, se ele não foi encontrado ou se ocorreu algum erro.
Um registro não encontrado é diferente de uma falha de comunicação.
Quando o backend informa que o registro não existe, o DAO poderá retornar null.
Quando ocorre uma falha na requisição, o DAO lança uma exceção.
O componente utiliza os estados pessoa e erro para representar essas situações separadamente.
6. Utilizando useEffect para consultar o registro
Observe o início do efeito:
useEffect(() => {
let ativo = true;
async function carregarPessoa() {
setLoading(true);
setPessoa(null);
setErro(null);
// Consulta ao backend
}
carregarPessoa();
return () => {
ativo = false;
};
}, [tipo, id]);
O useEffect será executado quando o componente for montado e sempre que tipo ou id forem alterados.
Isso permite utilizar o mesmo componente para visualizar diferentes registros.
Por exemplo:
/visualizar/PF/identificador-1
ou:
/visualizar/PJ/identificador-2
6.1. Por que tipo e id aparecem nas dependências?
Observe:
}, [tipo, id]);
Esses valores determinam qual registro deverá ser consultado.
Se o usuário navegar para outro cadastro utilizando a mesma estrutura de componente, o React poderá executar novamente o efeito para carregar os dados correspondentes.
6.2. Por que utilizamos a variável ativo?
Considere a seguinte situação:
- O usuário acessa a visualização de uma Pessoa Física.
- A aplicação inicia uma consulta ao backend.
- Antes de a resposta chegar, o usuário navega para outra página.
- A consulta anterior termina.
Nesse cenário, não queremos utilizar o resultado de uma consulta antiga para atualizar uma tela que já foi abandonada.
Por isso, utilizamos:
let ativo = true;
Antes de atualizar os estados, verificamos:
if (ativo) {
setPessoa(encontrada);
}
Quando o efeito é encerrado, executamos:
return () => {
ativo = false;
};
Assim, o resultado da operação anterior deixa de ser utilizado pelo componente.
Essa estratégia impede que respostas antigas atualizem os estados controlados pelo efeito encerrado.
Ela não cancela a requisição HTTP em andamento. O cancelamento efetivo poderia ser realizado com AbortController, desde que o DAO também recebesse e utilizasse o sinal de cancelamento.
Nesta etapa, manteremos o controle por meio da variável ativo, suficiente para demonstrar o problema de respostas assíncronas antigas.
7. Selecionando o DAO correspondente
Assim como fizemos na listagem, precisamos identificar se o registro pertence a uma Pessoa Física ou Jurídica.
Observe:
const dao = tipo === "PF"
? new PFDAO()
: new PJDAO();
Essa expressão seleciona a implementação correspondente ao valor recebido na rota.
Se tipo for "PF", utilizaremos PFDAOBackEnd.
Se tipo for "PJ", utilizaremos PJDAOBackEnd.
Antes disso, verificamos se o valor recebido é válido:
if (tipo !== "PF" && tipo !== "PJ") {
throw new Error(
"Tipo de pessoa inválido."
);
}
Essa validação evita que um valor inesperado seja tratado automaticamente como Pessoa Jurídica.
8. Consultando o registro pelo identificador
A principal operação desta parte é:
const encontrada = await dao.buscarPorId(id);
Observe a utilização de await.
O componente precisa aguardar a conclusão da consulta antes de atualizar o estado pessoa.
O método buscarPorId() foi implementado na Parte 3 e possui a responsabilidade de:
- Verificar se o registro já está disponível no cache do DAO;
- Consultar o endpoint correspondente, quando necessário;
- Interpretar a resposta recebida;
- Adaptar os dados utilizando
mapPF()oumapPJ(); - Retornar o registro encontrado ou
null, quando o servidor indicar que ele não existe.
8.1. Comparando com o documento original
No documento de referência, a visualização utiliza uma sequência semelhante a:
await dao.carregarLista();
const lista = dao.listar();
const encontrada = lista.find(
(pessoa) => pessoa.id === id
);
Essa abordagem consulta a lista completa para localizar um único registro.
Entretanto, como já implementamos buscarPorId(id) na Parte 3, podemos utilizar esse método diretamente.
const encontrada = await dao.buscarPorId(id);
Essa implementação evita a necessidade de consultar todos os registros quando desejamos visualizar apenas uma pessoa.
Uma vantagem da organização em camadas é permitir que os componentes utilizem operações disponibilizadas pelos DAOs.
Assim, a tela de visualização não precisa implementar sua própria lógica de busca.
9. Apresentando os dados com Descriptions
A apresentação dos dados continuará utilizando o componente Descriptions do Ant Design.
Observe:
<Descriptions bordered column={1}>
<Descriptions.Item label="Nome">
{pessoa.nome}
</Descriptions.Item>
<Descriptions.Item label="E-mail">
{pessoa.email}
</Descriptions.Item>
</Descriptions>
O atributo column={1} organiza os dados em uma única coluna de informações.
Cada Descriptions.Item apresenta um rótulo e o valor correspondente.
Essa organização é adequada para a visualização detalhada de um cadastro.
9.1. Apresentando atributos específicos de PF e PJ
O componente deverá apresentar CPF quando o registro for de Pessoa Física e CNPJ quando for de Pessoa Jurídica.
Observe:
{tipo === "PF" ? (
<Descriptions.Item label="CPF">
{pessoa.cpf}
</Descriptions.Item>
) : (
<Descriptions.Item label="CNPJ">
{pessoa.cnpj}
</Descriptions.Item>
)}
Utilizamos uma expressão condicional para decidir qual informação será apresentada.
O mesmo princípio é aplicado ao Título Eleitoral e à Inscrição Estadual.
9.2. Apresentando informações de endereço
O endereço é representado por um objeto associado à pessoa.
Por isso, acessamos seus atributos utilizando:
pessoa.endereco?.logradouro
pessoa.endereco?.bairro
pessoa.endereco?.cidade
pessoa.endereco?.uf
O operador ?., conhecido como optional chaining, permite acessar propriedades sem provocar uma exceção quando o objeto intermediário é null ou undefined.
Também utilizamos valores alternativos para informações ausentes:
{pessoa.endereco?.cep || "Não informado"}
9.3. Apresentando telefones
Uma pessoa pode possuir mais de um telefone.
Por isso, o atributo telefones é tratado como uma coleção.
Observe:
pessoa.telefones
.map(
(telefone) =>
`(${telefone.ddd}) ${telefone.numero}`
)
.join(" | ");
O método map() transforma cada telefone em uma representação textual.
O método join() combina essas representações em uma única string, separando os telefones pelo caractere |.
Por exemplo:
(61) 999999999 | (61) 33333333
Caso a pessoa não possua telefones cadastrados, o componente apresenta:
Não informado
10. Tratando as diferentes situações da consulta
A tela de visualização poderá apresentar três resultados principais.
10.1. Consulta em andamento
Enquanto loading for true, apresentaremos o indicador de carregamento.
if (loading) {
return (
<Spin
size="large"
tip="Consultando cadastro..."
>
<div style={{ minHeight: 100 }} />
</Spin>
);
}
O usuário será informado de que a aplicação está aguardando a resposta do servidor.
10.2. Registro não encontrado
Se o DAO retornar null, o componente apresentará uma mensagem informando que nenhum registro foi encontrado.
if (!pessoa) {
return (
<div>
<h3>Nenhuma pessoa encontrada.</h3>
<Button
onClick={() => navigate("/listar")}
>
Voltar à lista
</Button>
</div>
);
}
Essa situação poderá ocorrer quando o identificador informado não corresponder a um registro existente.
10.3. Falha de comunicação
Se ocorrer uma exceção durante a consulta, o componente armazenará a mensagem no estado erro.
catch (error) {
if (ativo) {
setErro(error.message);
}
}
A interface apresentará a mensagem utilizando Alert.
<Alert
type="error"
title="Erro ao consultar cadastro"
description={erro}
showIcon
/>
Não devemos tratar todas as falhas como registros inexistentes.
Uma falha de rede ou um erro do servidor não significa necessariamente que a pessoa não está cadastrada.
Por isso, a implementação diferencia o estado de erro da ausência de um registro.
11. Navegando para edição e listagem
O componente disponibiliza dois botões.
O primeiro permite acessar a edição do cadastro:
<Button
type="primary"
onClick={() =>
navigate(`/editar/${tipo}/${pessoa.id}`)
}
>
Editar
</Button>
O segundo permite retornar à listagem:
<Button
onClick={() => navigate("/listar")}
>
Voltar
</Button>
Observe que continuamos utilizando useNavigate(), apresentado nos capítulos anteriores.
A navegação permanece semelhante à versão com armazenamento local.
A diferença está no fato de que a próxima tela também precisará utilizar os DAOs de backend para recuperar e atualizar os dados.
Os caminhos /listar, /visualizar/:tipo/:id e /editar/:tipo/:id serão associados aos novos componentes na Parte 7.
Até que essa integração seja realizada, a navegação para a versão com backend ainda não estará completamente disponível.
12. Verificando a evolução dos arquivos
Após concluir esta parte, a estrutura deverá conter:
src/
├── components/
│ ├── listapessoas/
│ │ ├── ListaPessoas.jsx
│ │ └── ListaPessoasBack.jsx
│ │
│ └── visualizapessoa/
│ ├── VisualizarPessoa.jsx
│ └── VisualizarPessoaBack.jsx
│
└── objetos/
└── dao/
├── PFDAOLocal.mjs
├── PJDAOLocal.mjs
├── PFDAOBackEnd.mjs
└── PJDAOBackEnd.mjs
Os arquivos da versão local foram preservados.
O novo componente já utiliza os DAOs de backend, mas ainda não foi associado às rotas da aplicação.
A integração será realizada após a implementação do formulário de cadastro e edição.
13. Exercício de fixação — Analisando a visualização assíncrona
Considere o código desenvolvido nesta parte e responda:
1. Qual é a principal diferença entre VisualizarPessoa.jsx e VisualizarPessoaBack.jsx?
2. Qual é a finalidade do hook useParams()?
3. Por que utilizamos await antes de chamar dao.buscarPorId(id)?
4. Qual é a vantagem de utilizar buscarPorId(id) em vez de consultar todos os registros e aplicar find()?
5. Por que o estado loading foi inicializado com true?
6. Qual é a diferença entre um registro não encontrado e uma falha de comunicação?
7. Por que tipo e id foram incluídos no array de dependências do useEffect?
8. Qual é a finalidade da variável ativo?
9. Por que utilizamos o operador ?. ao apresentar os atributos de endereço?
10. Como o componente diferencia a apresentação de CPF/CNPJ e Título Eleitoral/Inscrição Estadual?
13.1. Atividade prática complementar
Após adicionar VisualizarPessoaBack.jsx, realize as seguintes atividades:
- Localize o trecho responsável por recuperar os parâmetros da rota.
- Identifique onde o DAO é selecionado.
- Localize a chamada assíncrona de
buscarPorId(id). - Identifique como o componente apresenta o indicador de carregamento.
- Explique o comportamento esperado quando o registro não existe.
- Identifique como são apresentados os atributos específicos de PF e PJ.
- Compare o código de visualização com a implementação anterior, identificando os trechos reaproveitados e os novos mecanismos de processamento assíncrono.
Nesta etapa, não é necessário modificar as rotas principais da aplicação.
O objetivo é compreender a implementação do novo componente e preparar o projeto para a integração final.
Os testes completos de navegação serão realizados na Parte 7.
14. Síntese da Parte 5
Nesta parte, implementamos o componente VisualizarPessoaBack.jsx, responsável por apresentar os dados detalhados de Pessoas Físicas e Jurídicas armazenadas no backend.
O componente preserva a estrutura visual desenvolvida anteriormente, utilizando Card, Descriptions e Button do Ant Design.
Entretanto, incorpora novos mecanismos para lidar com a comunicação assíncrona.
Compreendemos como:
- Recuperar os parâmetros da rota utilizando
useParams(); - Selecionar o DAO correspondente ao tipo de pessoa;
- Consultar um registro utilizando
buscarPorId(id); - Aguardar o resultado da consulta;
- Controlar estados de carregamento e erro;
- Diferenciar registros não encontrados de falhas de comunicação;
- Evitar atualizações de estado provenientes de efeitos encerrados;
- Apresentar os atributos específicos de PF e PJ;
- Preservar a navegação para edição e listagem.
O componente anterior foi mantido, permitindo comparar as duas estratégias de persistência.
Na Parte 6 — Evoluindo os formulários para cadastro e edição, implementaremos PessoaFormBack.jsx, reaproveitando os formulários de Pessoa Física e Jurídica e introduzindo o carregamento assíncrono dos dados, o envio de requisições POST e PUT e o controle dos estados de processamento.
Parte 6 — Evoluindo os formulários para cadastro e edição com backend
Nas partes anteriores, implementamos os DAOs de backend, a listagem de pessoas e a visualização detalhada dos registros.
Agora, vamos desenvolver o componente responsável pelo cadastro e pela edição de Pessoas Físicas e Jurídicas.
No capítulo anterior, utilizamos o componente PessoaFormOO.jsx para realizar essas operações com armazenamento no localStorage.
Nesta etapa, criaremos PessoaFormBack.jsx, preservando a estrutura do formulário e as classes de domínio, mas substituindo a persistência local pela comunicação com a API REST.
Não crie um novo projeto ReactJS e não substitua PessoaFormOO.jsx.
O novo componente será adicionado ao projeto existente e utilizará os DAOs PFDAOBackEnd.mjs e PJDAOBackEnd.mjs, implementados na Parte 3.
A integração definitiva com as rotas será realizada na Parte 7.
1. Relembrando o funcionamento do formulário anterior
Na versão com armazenamento local, desenvolvemos um formulário capaz de:
- Cadastrar Pessoas Físicas e Jurídicas;
- Alternar entre os tipos PF e PJ;
- Validar campos obrigatórios;
- Utilizar componentes específicos para endereço, telefone, PF e PJ;
- Criar instâncias das classes de domínio;
- Recuperar os dados de um registro para edição;
- Salvar e atualizar informações utilizando os DAOs locais.
Essas funcionalidades serão preservadas.
A principal diferença estará na comunicação com o mecanismo de persistência.
1.1. Comparando as implementações
| Operação | Versão local | Versão com backend |
|---|---|---|
| Cadastro | DAO local | Requisição POST |
| Edição | DAO local | Requisição PUT |
| Recuperação do cadastro | Consulta local | Requisição GET |
| Validação dos campos | Ant Design Form | Ant Design Form |
| Criação dos objetos | Classes PF, PJ e associadas | Mesmas classes |
| Tempo de resposta | Geralmente imediato | Depende do servidor |
| Feedback de processamento | Não era essencial | Necessário |
| Tratamento de falhas | Erros locais | Erros de rede e HTTP |
Observe que não precisamos reconstruir os formulários.
A evolução será concentrada nos pontos em que o componente acessa os dados.
2. Identificando os arquivos reaproveitados
Continuaremos utilizando os componentes desenvolvidos anteriormente.
src/
├── components/
│ └── cadastrapessoa/
│ ├── EnderecoFormEX.jsx
│ ├── TelefoneListOO.jsx
│ ├── PFForm.jsx
│ ├── PJForm.jsx
│ ├── PessoaFormOO.jsx
│ └── PessoaFormBack.jsx
│
└── objetos/
├── pessoas/
│ ├── PF.mjs
│ ├── PJ.mjs
│ ├── Endereco.mjs
│ ├── Telefone.mjs
│ ├── Titulo.mjs
│ └── IE.mjs
│
└── dao/
├── PFDAOBackEnd.mjs
└── PJDAOBackEnd.mjs
O único arquivo novo desta parte será PessoaFormBack.jsx.
A utilização dos mesmos componentes de formulário demonstra uma vantagem da separação de responsabilidades.
Os componentes EnderecoFormEX.jsx, TelefoneListOO.jsx, PFForm.jsx e PJForm.jsx continuam responsáveis pela entrada dos dados.
A mudança no mecanismo de persistência não exige reconstruir esses componentes.
3. Compreendendo os modos de cadastro e edição
O novo componente deverá identificar se o usuário deseja cadastrar uma nova pessoa ou editar um registro existente.
Essa identificação será realizada por meio dos parâmetros da rota.
3.1. Modo cadastro
Quando o usuário acessar:
/cadastrar
O formulário deverá:
- Apresentar os campos vazios;
- Selecionar Pessoa Física como tipo inicial;
- Permitir alternar entre PF e PJ;
- Validar os dados informados;
- Criar os objetos correspondentes;
- Enviar uma requisição POST ao backend;
- Aguardar a confirmação do cadastro.
3.2. Modo edição
Quando o usuário acessar uma rota semelhante a:
/editar/PF/identificador-do-registro
O formulário deverá:
- Recuperar os parâmetros
tipoeid; - Selecionar o DAO correspondente;
- Consultar o registro no backend;
- Aguardar o retorno dos dados;
- Preencher os campos do formulário;
- Permitir alterar as informações;
- Enviar uma requisição PUT;
- Aguardar a confirmação da atualização.
3.3. Recuperando os parâmetros
Utilizaremos novamente o hook useParams():
const { tipo: tipoParam, id } = useParams();
Observe que utilizamos tipoParam para distinguir o tipo recebido na rota do estado tipo, controlado pelo formulário.
4. Criando o componente PessoaFormBack.jsx
4.1. Arquivo que será adicionado
Na pasta:
src/components/cadastrapessoa/
Crie o arquivo:
PessoaFormBack.jsx
Situação: NOVO ARQUIVO.
Não substitua PessoaFormOO.jsx.
4.2. Código completo — PessoaFormBack.jsx
Copie o código abaixo para o arquivo criado.
// src/components/cadastrapessoa/PessoaFormBack.jsx
import { useEffect, useRef, useState } from "react";
import {
Form,
Input,
Button,
Radio,
Spin,
Alert,
message
} from "antd";
import {
useParams,
useNavigate
} from "react-router-dom";
import dayjs from "dayjs";
// Componentes reaproveitados
import EnderecoForm from "./EnderecoFormEX.jsx";
import TelefoneList from "./TelefoneListOO.jsx";
import PFForm from "./PFForm.jsx";
import PJForm from "./PJForm.jsx";
// DAOs de backend
import PFDAO from "../../objetos/dao/PFDAOBackEnd.mjs";
import PJDAO from "../../objetos/dao/PJDAOBackEnd.mjs";
// Classes de domínio
import PF from "../../objetos/pessoas/PF.mjs";
import PJ from "../../objetos/pessoas/PJ.mjs";
import Endereco from "../../objetos/pessoas/Endereco.mjs";
import Telefone from "../../objetos/pessoas/Telefone.mjs";
import Titulo from "../../objetos/pessoas/Titulo.mjs";
import IE from "../../objetos/pessoas/IE.mjs";
export default function PessoaFormBack() {
const [form] = Form.useForm();
const navigate = useNavigate();
const { tipo: tipoParam, id } = useParams();
// Estados da interface
const [tipo, setTipo] = useState("PF");
const [loading, setLoading] = useState(false);
const [salvando, setSalvando] = useState(false);
const [erro, setErro] = useState(null);
// Preserva as instâncias dos DAOs
const pfDAO = useRef(new PFDAO()).current;
const pjDAO = useRef(new PJDAO()).current;
// Identifica o modo de edição
const editando = Boolean(id);
// Seleciona o DAO correspondente
function obterDAO(tipoPessoa) {
return tipoPessoa === "PF"
? pfDAO
: pjDAO;
}
// ===================================================
// CARREGAMENTO DOS DADOS NO MODO EDIÇÃO
// ===================================================
useEffect(() => {
let ativo = true;
async function carregarEdicao() {
// Modo cadastro
if (!id) {
setTipo("PF");
setErro(null);
setLoading(false);
form.resetFields();
form.setFieldsValue({ tipo: "PF" });
return;
}
// Modo edição
setLoading(true);
setErro(null);
try {
if (tipoParam !== "PF" && tipoParam !== "PJ") {
throw new Error(
"Tipo de pessoa inválido."
);
}
setTipo(tipoParam);
const dao = obterDAO(tipoParam);
// Aguarda a consulta ao backend
const pessoa = await dao.buscarPorId(id);
if (!ativo) {
return;
}
if (!pessoa) {
throw new Error(
"Pessoa não encontrada."
);
}
// Prepara os valores para o Ant Design Form
const valores = {
tipo: tipoParam,
nome: pessoa.nome,
email: pessoa.email,
endereco: pessoa.endereco || {},
telefones: pessoa.telefones || []
};
if (tipoParam === "PF") {
valores.cpf = pessoa.cpf;
valores.titulo = pessoa.titulo || {
numero: "",
zona: "",
secao: ""
};
} else {
const ieObj = pessoa.ie || {};
valores.cnpj = pessoa.cnpj;
valores.ie = {
numero: ieObj.numero || "",
estado: ieObj.estado || "",
dataRegistro: ieObj.dataRegistro
? dayjs(ieObj.dataRegistro)
: null
};
}
form.resetFields();
form.setFieldsValue(valores);
} catch (error) {
if (ativo) {
setErro(error.message);
}
} finally {
if (ativo) {
setLoading(false);
}
}
}
carregarEdicao();
return () => {
ativo = false;
};
}, [id, tipoParam, form]);
// ===================================================
// ALTERAÇÃO DO TIPO DE PESSOA
// ===================================================
function onChangeTipo(e) {
const novoTipo = e.target.value;
setTipo(novoTipo);
// Preserva os campos comuns
const valoresAtuais = form.getFieldsValue();
form.resetFields();
form.setFieldsValue({
nome: valoresAtuais.nome,
email: valoresAtuais.email,
endereco: valoresAtuais.endereco,
telefones: valoresAtuais.telefones,
tipo: novoTipo
});
}
// ===================================================
// CONSTRUÇÃO DO OBJETO DE DOMÍNIO
// ===================================================
function construirPessoa(values) {
const endVals = values.endereco || {};
// Cria o objeto Endereco
const endereco = new Endereco();
endereco.setCep(endVals.cep);
endereco.setLogradouro(endVals.logradouro);
endereco.setBairro(endVals.bairro);
endereco.setCidade(endVals.cidade);
endereco.setUf(endVals.uf);
endereco.setRegiao(endVals.regiao);
// Cria o objeto PF ou PJ
let pessoa;
if (values.tipo === "PF") {
const pf = new PF();
pf.setNome(values.nome);
pf.setEmail(values.email);
pf.setCPF(values.cpf);
pf.setEndereco(endereco);
// Título Eleitoral
if (values.titulo) {
const titulo = new Titulo();
titulo.setNumero(values.titulo.numero);
titulo.setZona(values.titulo.zona);
titulo.setSecao(values.titulo.secao);
pf.setTitulo(titulo);
}
pessoa = pf;
} else {
const pj = new PJ();
pj.setNome(values.nome);
pj.setEmail(values.email);
pj.setCNPJ(values.cnpj);
pj.setEndereco(endereco);
// Inscrição Estadual
if (values.ie) {
const ie = new IE();
ie.setNumero(values.ie.numero);
ie.setEstado(values.ie.estado);
const data = values.ie.dataRegistro;
const dataRegistro =
data && typeof data.format === "function"
? data.format("YYYY-MM-DD")
: data || "";
ie.setDataRegistro(dataRegistro);
pj.setIE(ie);
}
pessoa = pj;
}
// Telefones
if (values.telefones?.length > 0) {
values.telefones.forEach((tel) => {
const telefone = new Telefone();
telefone.setDdd(tel.ddd);
telefone.setNumero(tel.numero);
pessoa.addTelefone(telefone);
});
}
return pessoa;
}
// ===================================================
// CADASTRO E ATUALIZAÇÃO
// ===================================================
async function onFinish(values) {
if (salvando) {
return;
}
setSalvando(true);
setErro(null);
try {
// Constrói a instância de PF ou PJ
const pessoa = construirPessoa(values);
// Seleciona o DAO
const tipoPessoa = editando
? tipoParam
: values.tipo;
const dao = obterDAO(tipoPessoa);
if (editando) {
// Requisição PUT
await dao.atualizar(id, pessoa);
message.success(
"Registro atualizado com sucesso!"
);
} else {
// Requisição POST
await dao.salvar(pessoa);
message.success(
"Registro criado com sucesso!"
);
}
// Retorna à listagem após confirmação
navigate("/listar");
} catch (error) {
console.error(
"Erro ao salvar registro:",
error
);
setErro(error.message);
message.error(
"Não foi possível salvar o registro."
);
} finally {
setSalvando(false);
}
}
// ===================================================
// INDICADOR DE CARREGAMENTO DA EDIÇÃO
// ===================================================
if (loading) {
return (
<div
style={{
minHeight: 300,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<Spin
size="large"
tip="Carregando cadastro..."
>
<div style={{ minHeight: 100, width: 240 }} />
</Spin>
</div>
);
}
// ===================================================
// FORMULÁRIO DE CADASTRO / EDIÇÃO
// ===================================================
return (
<div
className="main-scroll"
style={{
overflowY: "auto",
overflowX: "hidden",
minHeight: "100vh",
background: "#f9f9f9"
}}
>
<div
className="form-container"
style={{
maxWidth: 800,
margin: "24px auto",
background: "#fff",
padding: 24,
borderRadius: 8,
boxShadow: "0 2px 8px rgba(0,0,0,0.05)"
}}
>
<h2
style={{
textAlign: "center",
marginBottom: 20
}}
>
{editando
? `Editar ${
tipo === "PF"
? "Pessoa Física"
: "Pessoa Jurídica"
}`
: `Cadastro de ${
tipo === "PF"
? "Pessoa Física"
: "Pessoa Jurídica"
}`}
</h2>
{erro && (
<Alert
type="error"
title="Erro na operação"
description={erro}
showIcon
style={{ marginBottom: 20 }}
/>
)}
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{ tipo: "PF" }}
disabled={salvando || (editando && Boolean(erro))}
>
{/* Tipo de Pessoa */}
<Form.Item
label="Tipo de Pessoa"
name="tipo"
>
<Radio.Group
onChange={onChangeTipo}
disabled={editando}
>
<Radio value="PF">
Pessoa Física
</Radio>
<Radio value="PJ">
Pessoa Jurídica
</Radio>
</Radio.Group>
</Form.Item>
{/* Nome */}
<Form.Item
label="Nome"
name="nome"
rules={[
{
required: true,
message: "Informe o nome!"
}
]}
>
<Input
placeholder="Nome completo ou razão social"
/>
</Form.Item>
{/* E-mail */}
<Form.Item
label="E-mail"
name="email"
rules={[
{
required: true,
message: "Informe o e-mail!"
},
{
type: "email",
message: "Formato inválido!"
}
]}
>
<Input />
</Form.Item>
{/* CPF ou CNPJ */}
{tipo === "PF" ? (
<Form.Item
label="CPF"
name="cpf"
rules={[
{
required: true,
message: "Informe o CPF!"
}
]}
>
<Input
maxLength={11}
placeholder="Somente números"
/>
</Form.Item>
) : (
<Form.Item
label="CNPJ"
name="cnpj"
rules={[
{
required: true,
message: "Informe o CNPJ!"
}
]}
>
<Input
maxLength={18}
placeholder="Somente números"
/>
</Form.Item>
)}
{/* Componentes reaproveitados */}
<EnderecoForm />
<TelefoneList form={form} />
{tipo === "PF"
? <PFForm />
: <PJForm />}
{/* Botão Salvar */}
<Form.Item
style={{ marginTop: 20 }}
>
<Button
type="primary"
htmlType="submit"
block
loading={salvando}
>
{editando
? "Salvar Alterações"
: "Salvar"}
</Button>
</Form.Item>
</Form>
{/* Navegação */}
<Button
block
disabled={salvando}
onClick={() => navigate("/listar")}
>
{editando ? "Cancelar" : "Voltar"}
</Button>
</div>
</div>
);
}
A implementação mantém os componentes, as classes e o fluxo geral do formulário original.
Foram realizados os seguintes ajustes:
- A edição utiliza
dao.buscarPorId(id), em vez de consultar toda a lista e aplicarfind(). - O carregamento utiliza
try/catch/finally. - O estado
loadingcontrola a consulta inicial. - O estado
salvandocontrola as operações POST e PUT. - O formulário permanece visível durante o envio, com seus campos desabilitados.
- Os DAOs são preservados entre renderizações.
- O tipo PF/PJ não pode ser alterado durante a edição de um registro.
- A construção dos objetos de domínio foi concentrada na função
construirPessoa().
5. Compreendendo os estados do formulário
Observe:
const [tipo, setTipo] = useState("PF");
const [loading, setLoading] = useState(false);
const [salvando, setSalvando] = useState(false);
const [erro, setErro] = useState(null);
Esses estados controlam diferentes aspectos do funcionamento do componente.
| Estado | Responsabilidade |
|---|---|
tipo | Identificar PF ou PJ |
loading | Controlar o carregamento de um cadastro existente |
salvando | Controlar o envio de POST ou PUT |
erro | Armazenar mensagens de falha |
5.1. Por que separar loading e salvando?
No modo edição, precisamos consultar os dados antes de apresentar o formulário.
Nesse momento, utilizamos loading.
Entretanto, quando o usuário clica em Salvar, os campos já estão preenchidos e não precisamos ocultar toda a interface.
Podemos utilizar salvando para desabilitar os campos e apresentar um indicador no botão.
<Button
type="primary"
htmlType="submit"
loading={salvando}
>
Salvar
</Button>
Essa abordagem permite que o usuário compreenda qual operação está sendo realizada.
6. Carregando os dados para edição
Quando existe um identificador na rota, o componente precisa recuperar os dados do backend.
Observe:
const pessoa = await dao.buscarPorId(id);
O método buscarPorId() foi implementado na Parte 3.
Ele consulta o registro correspondente e retorna seus dados.
6.1. Preparando os valores para o formulário
Os dados recebidos do backend são representados por um objeto JavaScript.
Entretanto, alguns componentes do Ant Design precisam receber valores em formatos específicos.
Por isso, criamos:
const valores = {
tipo: tipoParam,
nome: pessoa.nome,
email: pessoa.email,
endereco: pessoa.endereco || {},
telefones: pessoa.telefones || []
};
Depois, acrescentamos os atributos específicos de PF ou PJ.
Para Pessoa Física:
valores.cpf = pessoa.cpf;
valores.titulo = pessoa.titulo || {
numero: "",
zona: "",
secao: ""
};
Para Pessoa Jurídica:
valores.cnpj = pessoa.cnpj;
valores.ie = {
numero: pessoa.ie?.numero || "",
estado: pessoa.ie?.estado || "",
dataRegistro: pessoa.ie?.dataRegistro
? dayjs(pessoa.ie.dataRegistro)
: null
};
Observe que a data da Inscrição Estadual é convertida utilizando dayjs.
Isso é necessário porque o componente DatePicker do Ant Design trabalha com objetos de data compatíveis com essa biblioteca.
6.2. Preenchendo os campos
Após preparar os valores, utilizamos:
form.setFieldsValue(valores);
Esse método permite preencher os campos do formulário programaticamente.
Assim, o usuário poderá visualizar os dados existentes e realizar as alterações necessárias.
Não precisamos criar uma interface diferente para cadastro e edição.
O mesmo componente poderá ser utilizado nas duas operações.
A principal diferença estará no carregamento inicial dos dados e no método utilizado para persistir as informações.
7. Construindo os objetos de domínio
Na versão anterior, já utilizávamos classes para representar Pessoas Físicas e Jurídicas.
Esse processo será mantido.
A função construirPessoa() recebe os valores do formulário e cria as instâncias correspondentes.
function construirPessoa(values) {
// Cria os objetos associados
// e retorna uma instância de PF ou PJ.
}
7.1. Criando o endereço
Observe:
const endereco = new Endereco();
endereco.setCep(endVals.cep);
endereco.setLogradouro(endVals.logradouro);
endereco.setBairro(endVals.bairro);
endereco.setCidade(endVals.cidade);
endereco.setUf(endVals.uf);
endereco.setRegiao(endVals.regiao);
Os valores recebidos do formulário são utilizados para preencher o objeto Endereco.
Esse objeto será associado à Pessoa Física ou Jurídica.
7.2. Criando uma Pessoa Física
const pf = new PF();
pf.setNome(values.nome);
pf.setEmail(values.email);
pf.setCPF(values.cpf);
pf.setEndereco(endereco);
Observe que continuamos utilizando os métodos das classes de domínio.
A comunicação com o backend não elimina a aplicação dos conceitos de orientação a objetos.
7.3. Criando uma Pessoa Jurídica
const pj = new PJ();
pj.setNome(values.nome);
pj.setEmail(values.email);
pj.setCNPJ(values.cnpj);
pj.setEndereco(endereco);
O mesmo princípio é aplicado aos objetos Titulo, IE e Telefone.
Na Parte 3, implementamos o método toPlain() nos DAOs.
Esse método recebe uma instância de PF ou PJ e converte seus atributos em um objeto JavaScript simples, adequado ao envio em JSON.
Portanto, o fluxo será:
Valores do formulário
↓
Objetos de domínio
↓
Método toPlain()
↓
Objeto JavaScript simples
↓
JSON.stringify()
↓
Requisição HTTP
8. Cadastrando uma nova pessoa
Quando o usuário acessa /cadastrar, o componente funciona no modo cadastro.
Após preencher e validar os campos, o Ant Design executa onFinish().
Observe o trecho responsável pelo cadastro:
const pessoa = construirPessoa(values);
const dao = obterDAO(values.tipo);
await dao.salvar(pessoa);
message.success(
"Registro criado com sucesso!"
);
O método salvar() foi implementado nos DAOs de backend.
Ele utiliza uma requisição HTTP POST.
8.1. Por que utilizar await?
A instrução:
await dao.salvar(pessoa);
faz com que a função aguarde a conclusão da operação.
Somente depois da confirmação do backend apresentaremos a mensagem de sucesso.
Não devemos apresentar a mensagem de sucesso antes de receber a confirmação do servidor.
Uma requisição pode falhar por problemas de conexão, indisponibilidade da API ou rejeição dos dados enviados.
9. Atualizando um cadastro existente
Quando existe um identificador na rota, o formulário funciona no modo edição.
Nesse caso, utilizaremos o método atualizar().
Observe:
await dao.atualizar(id, pessoa);
message.success(
"Registro atualizado com sucesso!"
);
O método atualizar() utiliza uma requisição HTTP PUT.
O identificador informa qual registro deverá ser modificado.
9.1. Comparando cadastro e edição
| Característica | Cadastro | Edição |
|---|---|---|
| Rota | /cadastrar | /editar/:tipo/:id |
| Carregamento inicial | Não necessário | Consulta GET |
| Objeto de domínio | Novo objeto | Objeto construído com valores editados |
| Método DAO | salvar() | atualizar() |
| Método HTTP | POST | PUT |
| Identificador na URL | Não | Sim |
| Resultado esperado | Novo registro | Registro atualizado |
O mesmo formulário atende às duas situações.
10. Tratando erros durante o envio
A função onFinish() utiliza try/catch/finally.
async function onFinish(values) {
setSalvando(true);
try {
const pessoa = construirPessoa(values);
const dao = obterDAO(values.tipo);
await dao.salvar(pessoa);
message.success(
"Registro criado com sucesso!"
);
} catch (error) {
setErro(error.message);
message.error(
"Não foi possível salvar o registro."
);
} finally {
setSalvando(false);
}
}
Esse exemplo simplificado apresenta apenas o cadastro. O código completo também trata a edição.
10.1. O bloco try
Contém as operações que poderão produzir erros.
10.2. O bloco catch
Permite tratar exceções produzidas durante a construção dos objetos ou durante a comunicação com o backend.
10.3. O bloco finally
Encerra o estado de processamento, independentemente do resultado.
setSalvando(false);
Assim, o botão não permanecerá indefinidamente em estado de carregamento quando ocorrer uma falha.
11. Preservando os dados em caso de erro
No documento original, o componente apresenta um Spin em tela cheia durante as operações assíncronas.
Essa abordagem é adequada para o carregamento inicial da edição.
Entretanto, durante o envio dos dados, optamos por manter o formulário visível.
Observe:
<Form
form={form}
layout="vertical"
onFinish={onFinish}
disabled={salvando}
>
Quando salvando estiver ativo, os campos serão desabilitados temporariamente.
O botão também apresentará um indicador de processamento.
<Button
type="primary"
htmlType="submit"
loading={salvando}
>
Salvar
</Button>
Essa estratégia evita que o usuário continue alterando os dados enquanto a requisição está em andamento.
Também permite preservar os valores preenchidos caso ocorra um erro.
Imagine que um usuário preencha um formulário extenso e ocorra uma falha de comunicação.
Se a aplicação limpar os campos imediatamente, ele precisará preencher todas as informações novamente.
Por isso, o formulário somente deverá ser encerrado ou limpo após a confirmação de sucesso.
12. Alterando o tipo de pessoa
No modo cadastro, o usuário poderá alternar entre Pessoa Física e Jurídica.
Para isso, utilizamos:
<Radio.Group onChange={onChangeTipo}>
<Radio value="PF">
Pessoa Física
</Radio>
<Radio value="PJ">
Pessoa Jurídica
</Radio>
</Radio.Group>
A função onChangeTipo() atualiza o estado correspondente.
function onChangeTipo(e) {
const novoTipo = e.target.value;
setTipo(novoTipo);
const valoresAtuais = form.getFieldsValue();
form.resetFields();
form.setFieldsValue({
nome: valoresAtuais.nome,
email: valoresAtuais.email,
endereco: valoresAtuais.endereco,
telefones: valoresAtuais.telefones,
tipo: novoTipo
});
}
Observe que preservamos os campos comuns entre PF e PJ.
Os campos específicos são reinicializados.
12.1. Por que não permitir alterar o tipo durante a edição?
Na edição, o registro já possui um tipo definido no backend.
Alterar uma Pessoa Física para Pessoa Jurídica não corresponde a uma simples atualização de atributos.
Essa operação exigiria regras específicas de transformação dos dados.
Por isso, nesta implementação, o tipo poderá ser alterado apenas durante o cadastro.
13. Verificando os arquivos do projeto
Após concluir esta parte, teremos:
src/
├── components/
│ ├── cadastrapessoa/
│ │ ├── EnderecoFormEX.jsx
│ │ ├── TelefoneListOO.jsx
│ │ ├── PFForm.jsx
│ │ ├── PJForm.jsx
│ │ ├── PessoaFormOO.jsx
│ │ └── PessoaFormBack.jsx
│ │
│ ├── listapessoas/
│ │ ├── ListaPessoas.jsx
│ │ └── ListaPessoasBack.jsx
│ │
│ └── visualizapessoa/
│ ├── VisualizarPessoa.jsx
│ └── VisualizarPessoaBack.jsx
│
└── objetos/
├── pessoas/
│ ├── PF.mjs
│ ├── PJ.mjs
│ ├── Endereco.mjs
│ ├── Telefone.mjs
│ ├── Titulo.mjs
│ └── IE.mjs
│
└── dao/
├── PFDAOLocal.mjs
├── PJDAOLocal.mjs
├── PFDAOBackEnd.mjs
└── PJDAOBackEnd.mjs
Os arquivos anteriores foram preservados.
Os três novos componentes principais já foram desenvolvidos:
ListaPessoasBack.jsx;VisualizarPessoaBack.jsx;PessoaFormBack.jsx.
Na próxima parte, realizaremos a integração com as rotas da aplicação.
O código de PessoaFormBack.jsx segue a organização do formulário original e utiliza os DAOs propostos na Parte 3.
Antes dos testes completos de cadastro e edição, será necessário conferir a correspondência entre os nomes dos métodos das classes de domínio e aqueles utilizados em toPlain().
Também deverá ser verificado o contrato de retorno da API nas operações POST e PUT.
Esses pontos serão considerados na integração final.
14. Exercício de fixação — Analisando o formulário com backend
Considere o componente PessoaFormBack.jsx implementado nesta parte.
Responda:
1. Qual é a principal diferença entre PessoaFormOO.jsx e PessoaFormBack.jsx?
2. Como o componente identifica se está no modo cadastro ou edição?
3. Por que o formulário precisa aguardar dao.buscarPorId(id) no modo edição?
4. Qual é a finalidade de utilizar form.setFieldsValue()?
5. Por que utilizamos dayjs() para preencher o campo de data da Inscrição Estadual?
6. Qual é a responsabilidade da função construirPessoa()?
7. Por que continuamos utilizando as classes PF, PJ, Endereco, Telefone, Titulo e IE?
8. Qual é a diferença entre dao.salvar() e dao.atualizar()?
9. Por que o botão Salvar deve permanecer em estado de processamento enquanto o backend não responder?
10. Qual é a vantagem de manter os campos preenchidos quando ocorre uma falha no envio?
14.1. Atividade prática complementar
Após adicionar PessoaFormBack.jsx, realize as seguintes atividades:
- Identifique o trecho que recupera os parâmetros da rota.
- Localize a função responsável pelo carregamento dos dados no modo edição.
- Identifique onde o componente utiliza
buscarPorId(). - Localize a função responsável pela construção dos objetos de domínio.
- Identifique onde o componente utiliza os métodos
salvar()eatualizar(). - Explique como o componente diferencia as operações POST e PUT.
- Identifique como o estado
salvandointerfere no funcionamento do formulário. - Compare a implementação atual com
PessoaFormOO.jsx, destacando quais elementos foram reaproveitados.
Nesta etapa, o objetivo é compreender a implementação do formulário e verificar se o arquivo foi corretamente adicionado ao projeto.
Os testes completos de cadastro e edição serão realizados após a integração das rotas.
15. Síntese da Parte 6
Nesta parte, implementamos o componente PessoaFormBack.jsx, responsável pelo cadastro e pela edição de Pessoas Físicas e Jurídicas utilizando a API REST.
O componente preserva os formulários e as classes orientadas a objetos desenvolvidos anteriormente.
Entretanto, incorpora novos mecanismos para realizar operações assíncronas.
Compreendemos como:
- Identificar os modos de cadastro e edição;
- Recuperar parâmetros da rota;
- Consultar registros existentes no backend;
- Preencher o formulário com dados recebidos;
- Construir instâncias das classes de domínio;
- Enviar cadastros utilizando POST;
- Atualizar registros utilizando PUT;
- Aguardar a confirmação das operações;
- Controlar estados de carregamento e envio;
- Tratar falhas de comunicação;
- Preservar os dados do formulário quando ocorrer um erro.
Na Parte 7 — Integração final do CRUD com backend, implementaremos as rotas da nova versão, ajustaremos os arquivos necessários e realizaremos os testes completos de cadastro, listagem, visualização, edição e exclusão.
Parte 7 — Integração final do CRUD com backend
Nas partes anteriores, evoluímos progressivamente a aplicação ReactJS para utilizar uma API REST como mecanismo de persistência.
Durante esse processo, implementamos:
PFDAOBackEnd.mjsePJDAOBackEnd.mjs, responsáveis pela comunicação com o backend;ListaPessoasBack.jsx, responsável pela consulta, filtragem e exclusão;VisualizarPessoaBack.jsx, responsável pela visualização detalhada;PessoaFormBack.jsx, responsável pelo cadastro e pela edição.
Entretanto, esses componentes ainda não foram integrados à navegação principal.
Nesta parte, concluiremos essa integração e verificaremos o funcionamento das operações CRUD.
Esta etapa não exige a criação de uma nova aplicação.
Continuaremos utilizando o projeto ReactJS desenvolvido nos capítulos anteriores.
Os arquivos da versão com localStorage serão preservados. A nova versão será ativada por meio de um arquivo de rotas específico para o backend.
1. Compreendendo a integração final
Até este momento, desenvolvemos diferentes partes da aplicação.
Os DAOs são responsáveis pela comunicação com a API REST.
Os componentes ReactJS utilizam esses DAOs para realizar as operações solicitadas pelo usuário.
Agora, precisamos conectar esses componentes às rotas da aplicação.
A arquitetura final será:
App.jsx
│
▼
BrowserRouter
│
▼
AppRoutesBack.jsx
│
▼
MainLayout.jsx
│
┌─────────┼──────────┐
│ │ │
▼ ▼ ▼
Cadastrar Listar Visualizar
Editar
│ │ │
└─────────┼──────────┘
│
▼
PFDAOBackEnd.mjs
PJDAOBackEnd.mjs
│
▼
fetch()
│
▼
API REST
│
▼
Backend
Observe que AppRoutesBack.jsx será responsável por associar cada endereço da aplicação ao componente correspondente.
O MainLayout.jsx continuará fornecendo a estrutura visual comum às diferentes telas.
2. Relembrando o funcionamento das rotas
Nos capítulos anteriores, utilizamos o React Router para permitir a navegação entre diferentes componentes sem recarregar toda a página.
Na versão com armazenamento local, utilizamos AppRoutes.jsx.
Agora, criaremos AppRoutesBack.jsx.
2.1. Rotas que serão utilizadas
| Rota | Componente | Funcionalidade |
|---|---|---|
/ | Redirecionamento | Acessar a tela inicial |
/cadastrar | PessoaFormBack | Cadastrar PF ou PJ |
/listar | ListaPessoasBack | Consultar e excluir registros |
/visualizar/:tipo/:id | VisualizarPessoaBack | Visualizar um registro |
/editar/:tipo/:id | PessoaFormBack | Editar um registro |
Observe que o componente PessoaFormBack.jsx será utilizado tanto no cadastro quanto na edição.
A diferença será identificada por meio dos parâmetros da rota.
3. Criando o arquivo AppRoutesBack.jsx
3.1. Arquivo que será adicionado
Na pasta:
src/routes/
Crie o arquivo:
AppRoutesBack.jsx
Situação: NOVO ARQUIVO.
Não substitua AppRoutes.jsx.
3.2. Código completo — AppRoutesBack.jsx
Copie o código abaixo para o arquivo criado.
// src/routes/AppRoutesBack.jsx
import { Routes, Route, Navigate } from "react-router-dom";
import MainLayout from "../layout/MainLayout.jsx";
import PessoaFormBack from
"../components/cadastrapessoa/PessoaFormBack.jsx";
import ListaPessoasBack from
"../components/listapessoas/ListaPessoasBack.jsx";
import VisualizarPessoaBack from
"../components/visualizapessoa/VisualizarPessoaBack.jsx";
export default function AppRoutesBack() {
return (
<Routes>
{/* Rotas que utilizam o layout principal */}
<Route
path="/"
element={<MainLayout />}
>
{/* Redirecionamento da página inicial */}
<Route
index
element={
<Navigate
to="cadastrar"
replace
/>
}
/>
{/* Cadastro de PF e PJ */}
<Route
path="cadastrar"
element={<PessoaFormBack />}
/>
{/* Listagem de pessoas */}
<Route
path="listar"
element={<ListaPessoasBack />}
/>
{/* Visualização detalhada */}
<Route
path="visualizar/:tipo/:id"
element={<VisualizarPessoaBack />}
/>
{/* Edição de cadastro */}
<Route
path="editar/:tipo/:id"
element={<PessoaFormBack />}
/>
</Route>
</Routes>
);
}
4. Compreendendo o arquivo de rotas
O código utiliza três componentes principais do React Router:
import {
Routes,
Route,
Navigate
} from "react-router-dom";
4.1. O componente Routes
O componente Routes organiza as definições de rotas da aplicação.
<Routes>
{/* Definições das rotas */}
</Routes>
4.2. O componente Route
Cada Route associa um caminho a um componente.
Por exemplo:
<Route
path="listar"
element={<ListaPessoasBack />}
/>
Quando o usuário acessa /listar, o React Router renderiza ListaPessoasBack.
4.3. O componente Navigate
O componente Navigate permite realizar redirecionamentos.
Observe:
<Route
index
element={
<Navigate
to="cadastrar"
replace
/>
}
/>
Nesse exemplo, quando o usuário acessa a rota principal /, será redirecionado para /cadastrar.
4.4. Reaproveitando o formulário
Observe:
<Route
path="cadastrar"
element={<PessoaFormBack />}
/>
<Route
path="editar/:tipo/:id"
element={<PessoaFormBack />}
/>
O mesmo componente será utilizado nas duas situações.
No cadastro, não haverá um identificador na rota.
Na edição, os parâmetros tipo e id estarão disponíveis.
O componente PessoaFormBack.jsx, implementado na Parte 6, utiliza esses parâmetros para identificar a operação desejada.
A utilização do mesmo componente para cadastro e edição reduz a duplicação de código.
Também facilita a manutenção, pois as alterações realizadas nos campos comuns poderão ser aproveitadas nas duas funcionalidades.
5. Reaproveitando o MainLayout.jsx
O projeto já possui o arquivo:
src/layout/MainLayout.jsx
Esse componente foi desenvolvido anteriormente para fornecer uma estrutura visual comum às diferentes telas.
Ele utiliza componentes do Ant Design, como:
Layout;Header;Content;Footer;Menu;Drawer.
Também utiliza Outlet, do React Router, para apresentar o conteúdo da rota selecionada.
5.1. O papel do Outlet
Observe:
<Content>
<Outlet />
</Content>
O componente Outlet indica onde o React Router deverá apresentar os componentes associados às rotas filhas.
Por exemplo, quando o usuário acessa /listar, o conteúdo de ListaPessoasBack.jsx será apresentado no local ocupado pelo Outlet.
Assim, o cabeçalho, o menu e o rodapé permanecem disponíveis durante a navegação.
5.2. Precisamos modificar MainLayout.jsx?
Não é necessário substituir o arquivo completo.
O MainLayout.jsx utilizado no projeto anterior já possui a estrutura necessária para receber os novos componentes.
Entretanto, podemos realizar um pequeno ajuste na identificação da rota ativa.
Localize o trecho responsável por definir currentKey.
Na implementação anterior, ele considera principalmente as rotas de cadastro, listagem e visualização.
Podemos incluir também a rota de edição.
Trecho atualizado
const currentKey =
location.pathname.startsWith("/listar") ||
location.pathname.startsWith("/visualizar") ||
location.pathname.startsWith("/editar")
? "/listar"
: "/cadastrar";
Dessa forma, quando o usuário estiver visualizando ou editando um registro, o menu continuará indicando a área de listagem.
O restante de MainLayout.jsx poderá ser mantido.
O objetivo desta etapa não é reconstruir a interface visual, mas integrar os componentes que agora utilizam a API REST.
6. Atualizando o arquivo App.jsx
Agora que todos os componentes foram implementados e o novo arquivo de rotas foi criado, podemos modificar a entrada principal da aplicação.
Esse ajuste foi deixado para o final do capítulo justamente para evitar importações de componentes que ainda não existiam.
6.1. Arquivo que será modificado
Localize:
src/App.jsx
Situação: ARQUIVO MODIFICADO.
6.2. Código completo — App.jsx
Substitua o conteúdo do arquivo pela implementação abaixo:
// src/App.jsx
import {
BrowserRouter as Router
} from "react-router-dom";
// Versão anterior com armazenamento local
// import AppRoutes from "./routes/AppRoutes.jsx";
// Nova versão com backend
import AppRoutes from "./routes/AppRoutesBack.jsx";
export default function App() {
return (
<Router>
<AppRoutes />
</Router>
);
}
Observe que mantivemos a importação da versão anterior como comentário.
Isso permite comparar as duas implementações.
6.3. Alternando entre as versões
Para utilizar a versão com backend:
// import AppRoutes from "./routes/AppRoutes.jsx";
import AppRoutes from "./routes/AppRoutesBack.jsx";
Para retornar à versão com armazenamento local:
import AppRoutes from "./routes/AppRoutes.jsx";
// import AppRoutes from "./routes/AppRoutesBack.jsx";
Apenas uma das importações deverá permanecer ativa.
O objetivo é selecionar qual conjunto de rotas será utilizado pela aplicação.
Não é necessário excluir os arquivos da versão anterior.
7. Verificando a estrutura final do projeto
Após concluir as alterações, o projeto deverá apresentar uma organização semelhante à seguinte:
src/
│
├── App.jsx
│
├── routes/
│ ├── AppRoutes.jsx
│ └── AppRoutesBack.jsx
│
├── layout/
│ └── MainLayout.jsx
│
├── components/
│ │
│ ├── cadastrapessoa/
│ │ ├── EnderecoFormEX.jsx
│ │ ├── TelefoneListOO.jsx
│ │ ├── PFForm.jsx
│ │ ├── PJForm.jsx
│ │ ├── PessoaFormOO.jsx
│ │ └── PessoaFormBack.jsx
│ │
│ ├── listapessoas/
│ │ ├── ListaPessoas.jsx
│ │ └── ListaPessoasBack.jsx
│ │
│ └── visualizapessoa/
│ ├── VisualizaPessoa.jsx
│ └── VisualizarPessoaBack.jsx
│
└── objetos/
│
├── pessoas/
│ ├── Pessoa.mjs
│ ├── PF.mjs
│ ├── PJ.mjs
│ ├── Endereco.mjs
│ ├── Telefone.mjs
│ ├── Titulo.mjs
│ └── IE.mjs
│
└── dao/
├── PFDAOLocal.mjs
├── PJDAOLocal.mjs
├── PFDAOBackEnd.mjs
└── PJDAOBackEnd.mjs
A árvore destaca os arquivos relacionados à evolução do CRUD. Outros arquivos existentes no projeto foram omitidos.
7.1. Resumo das alterações
| Arquivo | Situação | Parte |
|---|---|---|
PFDAOBackEnd.mjs | Novo | 3 |
PJDAOBackEnd.mjs | Novo | 3 |
ListaPessoasBack.jsx | Novo | 4 |
VisualizarPessoaBack.jsx | Novo | 5 |
PessoaFormBack.jsx | Novo | 6 |
AppRoutesBack.jsx | Novo | 7 |
MainLayout.jsx | Reaproveitado, com ajuste opcional | 7 |
App.jsx | Modificado | 7 |
8. Verificação de compatibilidade antes dos testes
Antes de realizar as operações de cadastro, edição e exclusão, precisamos verificar se os componentes estão utilizando corretamente as classes de domínio e os formatos de dados esperados pela API.
Essa verificação é especialmente importante porque a aplicação evoluiu a partir de implementações anteriores.
8.1. Correspondência entre as classes e os DAOs
Na Parte 6, utilizamos as classes:
PF
PJ
Endereco
Telefone
Titulo
IE
Os objetos são construídos por meio de métodos como:
pf.setNome(values.nome);
pf.setCPF(values.cpf);
pf.setEndereco(endereco);
Posteriormente, o DAO utiliza toPlain() para preparar os dados enviados ao backend.
Por exemplo:
const objeto = this.toPlain(pf);
Antes de realizar os testes de escrita, confira se os nomes dos métodos utilizados em toPlain() correspondem aos métodos existentes nas classes do projeto.
Exemplo de verificação
Se a classe Endereco disponibiliza:
getCep()
O DAO poderá utilizar:
cep: endereco.getCep()
Entretanto, se a classe utilizar outro nome para o método, será necessário ajustar a chamada.
Na Parte 3, utilizamos chamadas como:
pf.getNome?.()
O operador ?. evita uma exceção quando o método não existe, mas não resolve a incompatibilidade.
Nesse caso, o atributo poderá ficar com valor undefined e ser omitido durante a conversão para JSON.
Portanto, é importante conferir a correspondência entre os métodos das classes e os métodos utilizados nos DAOs.
8.2. Verificando o formato dos dados recebidos
A API REST utiliza JSON para transmitir os dados.
Na Parte 3, os métodos mapPF() e mapPJ() foram implementados para adaptar as respostas do backend.
Por exemplo:
mapPF(pf) {
return {
id: pf._id,
nome: pf.nome,
email: pf.email,
cpf: pf.cpf
};
}
O exemplo apresenta apenas os atributos principais.
Durante os testes, verifique se o backend retorna os atributos esperados, especialmente:
_id;nome;email;cpfoucnpj;endereco;telefones;tituloouie.
Caso a estrutura real da resposta seja diferente, os métodos de conversão deverão ser ajustados.
8.3. Verificando as respostas de POST e PUT
Os DAOs implementados na Parte 3 pressupõem que o backend retorne um objeto representando o registro cadastrado ou atualizado.
Por exemplo:
const dados = await resposta.json();
const novaPF = this.mapPF(dados);
Antes de considerar a integração concluída, confirme se as respostas reais de POST e PUT possuem esse formato.
Se o backend retornar apenas uma mensagem de sucesso ou uma estrutura diferente, será necessário adaptar o tratamento da resposta.
Os códigos desenvolvidos neste capítulo foram organizados para demonstrar a evolução do projeto e os conceitos de comunicação assíncrona.
A compatibilidade com a implementação efetiva da API e com todas as classes do projeto deve ser confirmada por meio de testes.
Não devemos presumir que uma requisição bem-sucedida garante, por si só, que todos os atributos foram persistidos corretamente.
9. Executando a aplicação
Com os arquivos adicionados e as rotas configuradas, execute o projeto ReactJS.
No terminal, dentro da pasta do projeto, utilize:
npm run dev
O comando considera que o projeto utiliza Vite, conforme a configuração adotada nas atividades anteriores.
Acesse o endereço informado no terminal.
A aplicação deverá apresentar o layout principal e a tela de cadastro.
9.1. Verificando a navegação
Antes de testar as operações de escrita, verifique:
- Se a aplicação inicia sem erros de importação;
- Se o menu apresenta as opções Cadastrar e Listar;
- Se a rota
/cadastrarapresenta o formulário; - Se a rota
/listarapresenta a listagem; - Se a seleção entre PF e PJ funciona;
- Se as requisições GET aparecem na aba Network das ferramentas de desenvolvedor.
Se ocorrer algum erro, consulte o terminal e o console do navegador para identificar o arquivo e a linha correspondente.
10. Testando as operações CRUD
Após verificar a compatibilidade dos métodos e dos formatos de dados, poderemos testar as operações CRUD.
Os testes deverão ser realizados de maneira progressiva.
A API utilizada neste capítulo é um serviço remoto.
Antes de cadastrar, alterar ou excluir registros, confirme que o ambiente disponibilizado para a atividade permite essas operações.
Utilize registros de teste e dados fictícios. Não utilize informações pessoais reais de estudantes ou terceiros.
10.1. Teste 1 — Consultar Pessoas Físicas
Acesse:
/listar
Selecione Pessoa Física.
Verifique se:
- O indicador de carregamento é apresentado;
- A aplicação realiza uma requisição GET;
- A tabela apresenta os registros recebidos;
- O filtro por nome funciona;
- O botão Atualizar executa uma nova consulta.
Resultado esperado
A tabela deverá apresentar os registros retornados pelo backend, quando existirem.
Se a API retornar uma lista vazia, a tabela deverá indicar a ausência de registros.
Se ocorrer uma falha, a interface deverá apresentar uma mensagem de erro.
10.2. Teste 2 — Consultar Pessoas Jurídicas
Na mesma tela, selecione Pessoa Jurídica.
Verifique se:
- O componente utiliza o DAO de PJ;
- A requisição é enviada ao endpoint
/pj; - A coluna CPF é substituída pela coluna CNPJ;
- Os registros apresentados correspondem a Pessoas Jurídicas.
Resultado esperado
A tabela deverá apresentar os registros de PJ recebidos da API.
10.3. Teste 3 — Visualizar um cadastro
Na listagem, selecione o botão Visualizar de um registro.
Verifique se:
- A URL contém o tipo e o identificador;
- O componente apresenta o indicador de carregamento;
- O método
buscarPorId()é executado; - Os dados do registro são apresentados;
- Os atributos específicos de PF ou PJ aparecem corretamente.
Resultado esperado
A tela deverá apresentar os dados detalhados do registro selecionado.
10.4. Teste 4 — Cadastrar uma Pessoa Física
Acesse:
/cadastrar
Selecione Pessoa Física.
Preencha os campos obrigatórios utilizando dados fictícios.
Clique em Salvar.
Verifique se:
- O formulário valida os campos;
- O botão apresenta o indicador de processamento;
- O DAO envia uma requisição POST;
- O backend confirma a operação;
- A aplicação apresenta uma mensagem de sucesso;
- O usuário é redirecionado para a listagem.
Resultado esperado
O novo registro deverá aparecer na listagem após a confirmação da operação.
10.5. Teste 5 — Cadastrar uma Pessoa Jurídica
Repita o procedimento anterior selecionando Pessoa Jurídica.
Verifique se:
- Os campos específicos de PJ são apresentados;
- O componente cria uma instância da classe
PJ; - O DAO utiliza o endpoint
/pj; - A operação é enviada utilizando POST;
- O registro aparece na listagem após a confirmação.
10.6. Teste 6 — Editar um cadastro
Na listagem, selecione o botão Editar.
Verifique se:
- O componente recupera os parâmetros da rota;
- Os dados são carregados do backend;
- Os campos são preenchidos;
- O usuário pode modificar as informações;
- O botão Salvar Alterações utiliza o método
atualizar(); - O DAO envia uma requisição PUT;
- O backend confirma a atualização.
Resultado esperado
Ao consultar novamente o registro, os dados alterados deverão estar disponíveis.
10.7. Teste 7 — Excluir um cadastro
Na listagem, selecione o botão Excluir de um registro de teste.
Verifique se:
- O sistema solicita confirmação;
- A operação somente é iniciada após a confirmação;
- O DAO envia uma requisição DELETE;
- A interface aguarda a resposta;
- O registro deixa de aparecer na listagem após a exclusão confirmada.
Resultado esperado
O registro excluído não deverá aparecer em uma nova consulta ao backend.
11. Verificando as requisições HTTP
As ferramentas de desenvolvedor do navegador permitem acompanhar as requisições realizadas pela aplicação.
Abra a aba Network e observe as operações.
| Operação | Método HTTP | Endpoint |
|---|---|---|
| Listar PF | GET | /pf |
| Listar PJ | GET | /pj |
| Visualizar PF | GET | /pf/:id |
| Visualizar PJ | GET | /pj/:id |
| Cadastrar PF | POST | /pf |
| Cadastrar PJ | POST | /pj |
| Atualizar PF | PUT | /pf/:id |
| Atualizar PJ | PUT | /pj/:id |
| Excluir PF | DELETE | /pf/:id |
| Excluir PJ | DELETE | /pj/:id |
11.1. O que observar em cada requisição?
Durante os testes, identifique:
- A URL utilizada;
- O método HTTP;
- O código de resposta;
- Os dados enviados no corpo da requisição, quando houver;
- O conteúdo da resposta;
- O tempo necessário para concluir a operação.
Essas informações ajudam a compreender a comunicação entre frontend e backend.
Também permitem identificar problemas de integração.
12. Problemas comuns durante a integração
12.1. Erro de importação
Situação: a aplicação informa que um arquivo não foi encontrado.
Possível causa: caminho de importação incorreto ou nome diferente do arquivo existente.
Verificação: confira a organização das pastas e os nomes utilizados nos imports.
12.2. A listagem não apresenta registros
Situação: a tabela permanece vazia.
Possíveis causas:
- O backend retornou uma lista vazia;
- A requisição falhou;
- O formato da resposta não corresponde ao esperado;
- O método de conversão não está adaptando corretamente os dados.
Verificação: consulte a aba Network e compare o JSON recebido com o resultado produzido pelo DAO.
12.3. O formulário apresenta sucesso, mas os dados não aparecem
Situação: o cadastro aparentemente foi concluído, mas o registro não é encontrado na listagem.
Possíveis causas:
- A resposta da API foi interpretada incorretamente;
- O cache não foi atualizado como esperado;
- Os atributos enviados não correspondem ao contrato do backend;
- A consulta posterior não está recuperando o registro.
Verificação: observe o corpo enviado na requisição POST e realize uma nova consulta GET.
12.4. A edição não preenche os campos
Situação: o componente de edição abre, mas os campos permanecem vazios.
Possíveis causas:
- O identificador não foi recuperado corretamente;
- A consulta ao backend falhou;
- O registro não existe;
- Os nomes dos atributos não correspondem aos utilizados pelo formulário.
Verificação: confira os parâmetros da rota, a resposta de buscarPorId() e os valores enviados a form.setFieldsValue().
12.5. Alguns atributos não são persistidos
Situação: determinados campos aparecem no formulário, mas não são armazenados.
Possível causa: os métodos toPlain() não incluem todos os atributos necessários ou utilizam métodos incompatíveis com as classes.
Verificação: compare os valores do formulário, os objetos de domínio e o JSON efetivamente enviado.
Quando ocorrer um problema, procure identificar em qual etapa ele aparece:
Formulário
↓
Objeto de domínio
↓
Método toPlain()
↓
Requisição HTTP
↓
Resposta do backend
↓
Método mapPF() / mapPJ()
↓
Estado ReactJS
↓
Interface
Essa análise permite localizar o problema sem modificar partes da aplicação que já estão funcionando.
13. Comparando as versões do projeto
Ao longo dos capítulos, evoluímos a aplicação de maneira progressiva.
Inicialmente, trabalhamos com componentes ReactJS e estados.
Posteriormente, incorporamos formulários, bibliotecas de componentes e classes orientadas a objetos.
Em seguida, implementamos a persistência local utilizando DAOs.
Agora, concluímos a integração com uma API REST.
| Etapa | Conhecimento aplicado |
|---|---|
| Componentes | Organização da interface |
| Props | Comunicação entre componentes |
| State | Controle de informações |
| Ant Design | Componentes visuais |
| Orientação a Objetos | Modelagem das entidades |
| DAO local | Persistência no navegador |
| React Router | Navegação entre telas |
| API REST | Comunicação HTTP |
| Async/await | Operações assíncronas |
| DAO de backend | Acesso remoto aos dados |
| Estados de carregamento | Feedback durante operações |
| Tratamento de erros | Controle de falhas |
Observe que os conhecimentos anteriores continuam sendo utilizados.
A nova implementação acrescenta responsabilidades, mas preserva a organização geral do projeto.
14. Exercício de fixação — Analisando a integração final
Considere a aplicação desenvolvida e responda:
1. Qual é a responsabilidade do arquivo AppRoutesBack.jsx?
2. Por que o componente MainLayout.jsx não precisa ser reconstruído para utilizar o backend?
3. Qual é a função do componente Outlet?
4. Como a aplicação identifica se PessoaFormBack.jsx está sendo utilizado para cadastro ou edição?
5. Qual é a diferença entre os arquivos AppRoutes.jsx e AppRoutesBack.jsx?
6. Por que os DAOs locais foram preservados?
7. Qual é a vantagem de manter a comunicação HTTP concentrada nos DAOs?
8. Por que devemos verificar o conteúdo do JSON enviado durante as operações POST e PUT?
9. Como podemos identificar, nas ferramentas de desenvolvedor, se uma operação utiliza GET, POST, PUT ou DELETE?
10. Por que uma resposta HTTP bem-sucedida não é suficiente para garantir que todos os atributos de um cadastro foram armazenados corretamente?
15. Atividade prática final — Validando o CRUD completo
Nesta atividade, você deverá verificar o funcionamento da aplicação ReactJS integrada ao backend.
Utilize exclusivamente registros de teste.
Etapa 1 — Verificação da estrutura
Confira se todos os arquivos implementados nas Partes 3 a 7 estão presentes no projeto.
Etapa 2 — Navegação
Verifique o funcionamento das rotas:
/cadastrar
/listar
/visualizar/:tipo/:id
/editar/:tipo/:id
Etapa 3 — Consulta
Consulte Pessoas Físicas e Jurídicas.
Verifique o carregamento dos dados e o funcionamento do filtro por nome.
Etapa 4 — Cadastro
Cadastre uma Pessoa Física e uma Pessoa Jurídica utilizando dados fictícios.
Confira as requisições POST na aba Network.
Etapa 5 — Visualização
Acesse os detalhes dos registros cadastrados.
Verifique se os atributos apresentados correspondem aos dados enviados.
Etapa 6 — Edição
Altere um atributo de cada registro.
Confira as requisições PUT e consulte novamente os dados.
Etapa 7 — Exclusão
Exclua os registros de teste.
Verifique se a operação foi confirmada pelo backend e se os registros não aparecem em uma nova consulta.
Etapa 8 — Análise final
Elabore uma breve comparação entre as versões com localStorage e com backend.
Considere:
- Local de armazenamento;
- Tempo de resposta;
- Operações síncronas e assíncronas;
- Tratamento de erros;
- Atualização da interface;
- Organização dos DAOs;
- Reaproveitamento de componentes.
Apresente o projeto atualizado e um breve relatório contendo:
- A estrutura dos arquivos implementados;
- A identificação dos métodos HTTP utilizados;
- Evidências das operações CRUD realizadas;
- Uma comparação entre as duas estratégias de persistência;
- As dificuldades encontradas durante a integração.
16. Síntese da Parte 7
Nesta parte, concluímos a integração dos componentes ReactJS com a API REST de Pessoas Físicas e Jurídicas.
Implementamos AppRoutesBack.jsx, atualizamos App.jsx e reaproveitamos MainLayout.jsx.
Também organizamos os procedimentos de verificação e teste das operações CRUD.
Ao longo deste capítulo, aplicamos conceitos de:
- Orientação a Objetos;
- Componentização;
- Gerenciamento de estados;
- Navegação com React Router;
- Comunicação HTTP;
- Operações assíncronas;
- Tratamento de exceções;
- Persistência de dados;
- Separação de responsabilidades.
A aplicação evoluiu de uma solução com armazenamento local para uma arquitetura em que o frontend se comunica com um backend por meio de uma API REST.
Essa evolução demonstra como os conhecimentos desenvolvidos progressivamente podem ser combinados na construção de uma aplicação mais completa.
Conclusão do capítulo: a integração estrutural está implementada. A conclusão funcional do CRUD depende da execução dos testes e da confirmação de compatibilidade entre as classes de domínio, os DAOs e o contrato da API.