Skip to main content

📘 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, PUT e DELETE.

Neste capítulo, esses conhecimentos serão aplicados ao projeto ReactJS.

Continuidade do projeto

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.

Separação de responsabilidades

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ísticaVersão com localStorageVersão com backend
Local de armazenamentoNavegadorPersistência gerenciada pelo servidor
Camada de acesso a dadosPFDAOLocal e PJDAOLocalPFDAOBackEnd e PJDAOBackEnd
ConsultaLeitura localRequisição GET
CadastroGravação localRequisição POST
AtualizaçãoAlteração localRequisição PUT
ExclusãoRemoção localRequisição DELETE
Identificação dos registrosIdentificadores da implementação localIdentificadores _id retornados pela API
Tempo de respostaGeralmente imediatoDepende da comunicação e do processamento remoto
Falhas possíveisErros locaisErros 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.

Retomando conhecimentos anteriores

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:

  1. Carregando: a requisição foi iniciada e a resposta ainda não chegou.
  2. Sucesso: os dados foram recebidos e podem ser apresentados.
  3. 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.

Atenção

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:

ArquivoResponsabilidade
src/objetos/dao/PFDAOBackEnd.mjsComunicar-se com os endpoints de Pessoa Física
src/objetos/dao/PJDAOBackEnd.mjsComunicar-se com os endpoints de Pessoa Jurídica
src/components/listapessoas/ListaPessoasBack.jsxListar, filtrar e excluir pessoas utilizando a API
src/components/visualizapessoa/VisualizarPessoaBack.jsxConsultar e apresentar detalhes de um cadastro remoto
src/components/cadastrapessoa/PessoaFormBack.jsxCadastrar e editar registros utilizando o backend
AppRoutesBack.jsxDefinir 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​

ArquivoAlteração prevista
src/App.jsxSelecionar a estrutura de rotas da versão com backend
src/layout/MainLayout.jsxReaproveitar o layout, realizando ajustes apenas quando necessários à navegação
Ordem de implementaçã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.jsx deverá 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.
Uma observação sobre o cache

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:

  1. Os DAOs PFDAOLocal.mjs e PJDAOLocal.mjs;
  2. Os componentes de listagem, visualização e cadastro;
  3. O arquivo de rotas AppRoutes.jsx;
  4. O componente MainLayout.jsx;
  5. 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:

  1. Qual camada será responsável por enviar os dados do cadastro ao servidor?
  2. Qual método HTTP será utilizado para cadastrar uma nova Pessoa Física?
  3. Por que o formulário não deve informar sucesso antes de receber a confirmação do servidor?
  4. Que informação visual poderá ser apresentada enquanto a operação estiver em andamento?
  5. Quais classes e componentes do projeto anterior poderão ser reaproveitados?
  6. Por que é interessante manter PFDAOLocal.mjs e PFDAOBackEnd.mjs como arquivos distintos durante o estudo?
Orientação para a atividade

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.

Continuidade do projeto

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á:

  1. Enviar uma requisição HTTP ao backend;
  2. Aguardar o processamento da requisição;
  3. Receber a resposta do servidor;
  4. Interpretar os dados recebidos;
  5. 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:

ElementoResponsabilidade
asyncDefine uma função assíncrona
fetch()Envia uma requisição HTTP
awaitAguarda a resolução de uma Promise
resposta.okVerifica se a resposta HTTP indica sucesso
resposta.json()Interpreta o corpo JSON da resposta
throw new Error()Sinaliza uma situação de erro
Importante

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.

Relação com os DAOs

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:

EstadoFinalidade
pessoasArmazenar os registros recebidos
loadingIndicar se existe uma operação em andamento
erroArmazenar 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.

Atençã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.

Ambiente de desenvolvimento

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.

Exemplo didático

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á:

  1. Validar os campos do formulário;
  2. Iniciar o estado de processamento;
  3. Solicitar ao DAO o envio dos dados;
  4. Aguardar a confirmação do backend;
  5. Informar o resultado da operação;
  6. 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.

Atenção

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:

ArquivoAplicação dos conceitos
PFDAOBackEnd.mjsfetch, async/await, respostas HTTP e tratamento de erros
PJDAOBackEnd.mjsComunicação assíncrona com os endpoints de PJ
ListaPessoasBack.jsxuseEffect, estados de carregamento, consulta e exclusão
VisualizarPessoaBack.jsxCarregamento assíncrono de um registro específico
PessoaFormBack.jsxEnvio 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
Separação de responsabilidades

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:

  1. Execute a consulta e observe o indicador de carregamento.
  2. Utilize as ferramentas de desenvolvedor do navegador para inspecionar a requisição HTTP.
  3. Na aba Network, simule uma conexão mais lenta e observe o comportamento da interface.
  4. Altere temporariamente a URL para um endereço inválido e observe o tratamento do erro.
  5. Restaure a URL original e verifique se a consulta volta a funcionar.
Orientação para a atividade

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 localStorage são síncronas;
  • As requisições HTTP utilizam operações assíncronas;
  • async/await permite organizar o código que depende de respostas futuras;
  • try/catch/finally auxilia no tratamento de erros e no encerramento do processamento;
  • useState permite controlar os dados, o carregamento e as mensagens de erro;
  • useEffect pode ser utilizado para iniciar consultas quando um componente é montado;
  • Spin, Alert e Button permitem 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.mjs
  • PJDAOLocal.mjs

Nesta parte, adicionaremos duas novas implementações:

  • PFDAOBackEnd.mjs
  • PJDAOBackEnd.mjs

Esses arquivos permitirão que nossa aplicação realize operações CRUD utilizando a API REST apresentada no capítulo anterior.

Continuidade do projeto

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étodoResponsabilidadeMétodo HTTP
carregarLista()Consultar os registros no backendGET
listar()Obter a lista disponívelGET, quando necessário
buscarPorId(id)Consultar um registro específicoGET
salvar(pessoa)Cadastrar uma pessoaPOST
atualizar(id, pessoa)Atualizar um cadastroPUT
excluir(id)Excluir um cadastroDELETE
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.

Ajuste em relação ao documento original

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?.()
}
: {}
};
}

}
Sobre a implementação

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.

Por que utilizar cacheCarregado?

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.

ElementoPFDAOBackEndPJDAOBackEnd
Endpoint/pf/pj
Identificador recebido_id_id
Documento principalCPFCNPJ
Atributo específicoTítulo de EleitorInscrição Estadual
Conversão de respostamapPF()mapPJ()
Conversão para enviotoPlain()toPlain()
Métodos CRUDGET, POST, PUT, DELETEGET, 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.

Por que essa conversão é importante?

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.

Cuidados durante os testes

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:

  1. Execute o projeto ReactJS.
  2. Abra as ferramentas de desenvolvedor do navegador.
  3. Utilize o exemplo de importação dinâmica para consultar Pessoas Físicas.
  4. Repita a operação utilizando PJDAOBackEnd.mjs.
  5. Observe as requisições na aba Network.
  6. Identifique o método HTTP utilizado, o código de resposta e o formato dos dados recebidos.
  7. Compare os atributos retornados pela API com aqueles apresentados pelos métodos mapPF() e mapPJ().

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.

Continuidade do projeto

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çãoVersão localVersão com backend
ListagemConsulta ao DAO localConsulta assíncrona à API
Filtro por nomeFiltragem dos dados locaisFiltragem dos dados recebidos
VisualizaçãoNavegação para o registro localNavegação para consulta remota
EdiçãoUtilização dos dados locaisCarregamento remoto do cadastro
ExclusãoRemoção no localStorageRequisição HTTP DELETE
Atualização da tabelaApós operação localApós confirmação da operação remota
Indicador de carregamentoNão era essencialNecessário para informar a espera
Tratamento de errosErros locaisFalhas 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
Separação de responsabilidades

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>

);

}
Ajustes realizados em relação ao documento original

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:

  1. carregarLista() aguarda os registros retornados pelo DAO.
  2. excluirPessoa() utiliza async/await para aguardar a exclusão.
  3. try/catch/finally controla os erros e o encerramento das operações.
  4. useRef preserva as instâncias dos DAOs entre renderizações.
  5. O filtro por nome é aplicado sobre os dados já carregados.
  6. 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.

EstadoResponsabilidade
tipoIdentificar se a listagem será de PF ou PJ
filtroNomeArmazenar o texto utilizado no filtro
dadosArmazenar os registros recebidos
loadingInformar se a consulta está em andamento
excluindoIdIdentificar o registro em processo de exclusão
erroArmazenar 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.

Relembrando useRef

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:

  1. O usuário seleciona Pessoa Física.
  2. A aplicação inicia uma consulta ao backend.
  3. Antes de a resposta chegar, o usuário seleciona Pessoa Jurídica.
  4. Uma segunda consulta é iniciada.
  5. A resposta de Pessoa Jurídica chega primeiro.
  6. 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);
}
Consultas concorrentes

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
Filtro local e consulta remota

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.

Atenção

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.

Sobre os botões Visualizar e Editar

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:

  1. Localize o método carregarLista() e identifique onde a aplicação aguarda a resposta do backend.
  2. Identifique os estados responsáveis por controlar a consulta, o filtro e a exclusão.
  3. Explique por que o componente utiliza dao.carregarLista() em vez de acessar diretamente fetch().
  4. Identifique como o componente diferencia Pessoas Físicas e Jurídicas.
  5. Analise o funcionamento do filtro e explique por que ele é executado sobre o estado dados.
  6. Identifique o trecho que impede a apresentação de uma resposta antiga após uma consulta mais recente.
Orientação para a atividade

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.

Continuidade do projeto

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ísticaVersão localVersão com backend
Origem dos dadoslocalStorageAPI REST
ConsultaDAO localDAO de backend
IdentificaçãoIdentificador do registroIdentificador retornado pela API
Tempo de respostaGeralmente imediatoDepende da comunicação HTTP
CarregamentoDados disponíveis após consulta localNecessário aguardar a resposta
Tratamento de errosPrincipalmente erros locaisErros de rede e respostas HTTP
ApresentaçãoDescriptionsDescriptions

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
Reaproveitando os DAOs

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:

  • visualizar identifica a funcionalidade;
  • PF informa o tipo de pessoa;
  • 68f123456789abcdef012345 representa 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.

Sobre o identificador

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>

);

}
Ajustes em relação ao documento original

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.

EstadoResponsabilidade
pessoaArmazenar o registro recebido do backend
loadingIndicar se a consulta está em andamento
erroArmazenar 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.

Diferenciando situações

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:

  1. O usuário acessa a visualização de uma Pessoa Física.
  2. A aplicação inicia uma consulta ao backend.
  3. Antes de a resposta chegar, o usuário navega para outra página.
  4. 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.

Sobre o controle de consultas

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:

  1. Verificar se o registro já está disponível no cache do DAO;
  2. Consultar o endpoint correspondente, quando necessário;
  3. Interpretar a resposta recebida;
  4. Adaptar os dados utilizando mapPF() ou mapPJ();
  5. 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.

Reaproveitamento de métodos

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
/>
Atenção

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.

Integração das rotas

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:

  1. Localize o trecho responsável por recuperar os parâmetros da rota.
  2. Identifique onde o DAO é selecionado.
  3. Localize a chamada assíncrona de buscarPorId(id).
  4. Identifique como o componente apresenta o indicador de carregamento.
  5. Explique o comportamento esperado quando o registro não existe.
  6. Identifique como são apresentados os atributos específicos de PF e PJ.
  7. Compare o código de visualização com a implementação anterior, identificando os trechos reaproveitados e os novos mecanismos de processamento assíncrono.
Orientação para a atividade

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.

Continuidade do projeto

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çãoVersão localVersão com backend
CadastroDAO localRequisição POST
EdiçãoDAO localRequisição PUT
Recuperação do cadastroConsulta localRequisição GET
Validação dos camposAnt Design FormAnt Design Form
Criação dos objetosClasses PF, PJ e associadasMesmas classes
Tempo de respostaGeralmente imediatoDepende do servidor
Feedback de processamentoNão era essencialNecessário
Tratamento de falhasErros locaisErros 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.

Reaproveitamento de componentes

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á:

  1. Apresentar os campos vazios;
  2. Selecionar Pessoa Física como tipo inicial;
  3. Permitir alternar entre PF e PJ;
  4. Validar os dados informados;
  5. Criar os objetos correspondentes;
  6. Enviar uma requisição POST ao backend;
  7. 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á:

  1. Recuperar os parâmetros tipo e id;
  2. Selecionar o DAO correspondente;
  3. Consultar o registro no backend;
  4. Aguardar o retorno dos dados;
  5. Preencher os campos do formulário;
  6. Permitir alterar as informações;
  7. Enviar uma requisição PUT;
  8. 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>

);

}
Ajustes em relação ao documento original

A implementação mantém os componentes, as classes e o fluxo geral do formulário original.

Foram realizados os seguintes ajustes:

  1. A edição utiliza dao.buscarPorId(id), em vez de consultar toda a lista e aplicar find().
  2. O carregamento utiliza try/catch/finally.
  3. O estado loading controla a consulta inicial.
  4. O estado salvando controla as operações POST e PUT.
  5. O formulário permanece visível durante o envio, com seus campos desabilitados.
  6. Os DAOs são preservados entre renderizações.
  7. O tipo PF/PJ não pode ser alterado durante a edição de um registro.
  8. 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.

EstadoResponsabilidade
tipoIdentificar PF ou PJ
loadingControlar o carregamento de um cadastro existente
salvandoControlar o envio de POST ou PUT
erroArmazenar 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.

Reaproveitando o formulário

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.

Relação com o método toPlain()

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.

Atenção

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ísticaCadastroEdição
Rota/cadastrar/editar/:tipo/:id
Carregamento inicialNão necessárioConsulta GET
Objeto de domínioNovo objetoObjeto construído com valores editados
Método DAOsalvar()atualizar()
Método HTTPPOSTPUT
Identificador na URLNãoSim
Resultado esperadoNovo registroRegistro 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.

Experiência do usuário

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.

Verificação de compatibilidade

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:

  1. Identifique o trecho que recupera os parâmetros da rota.
  2. Localize a função responsável pelo carregamento dos dados no modo edição.
  3. Identifique onde o componente utiliza buscarPorId().
  4. Localize a função responsável pela construção dos objetos de domínio.
  5. Identifique onde o componente utiliza os métodos salvar() e atualizar().
  6. Explique como o componente diferencia as operações POST e PUT.
  7. Identifique como o estado salvando interfere no funcionamento do formulário.
  8. Compare a implementação atual com PessoaFormOO.jsx, destacando quais elementos foram reaproveitados.
Orientação para a atividade

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.mjs e PJDAOBackEnd.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.

Continuidade do projeto

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​

RotaComponenteFuncionalidade
/RedirecionamentoAcessar a tela inicial
/cadastrarPessoaFormBackCadastrar PF ou PJ
/listarListaPessoasBackConsultar e excluir registros
/visualizar/:tipo/:idVisualizarPessoaBackVisualizar um registro
/editar/:tipo/:idPessoaFormBackEditar 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.

Reaproveitamento de componentes

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.

Arquivo reaproveitado

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";
Atenção

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​

ArquivoSituaçãoParte
PFDAOBackEnd.mjsNovo3
PJDAOBackEnd.mjsNovo3
ListaPessoasBack.jsxNovo4
VisualizarPessoaBack.jsxNovo5
PessoaFormBack.jsxNovo6
AppRoutesBack.jsxNovo7
MainLayout.jsxReaproveitado, com ajuste opcional7
App.jsxModificado7

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.

Atenção aos métodos opcionais

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;
  • cpf ou cnpj;
  • endereco;
  • telefones;
  • titulo ou ie.

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.

Sobre os ajustes de compatibilidade

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:

  1. Se a aplicação inicia sem erros de importação;
  2. Se o menu apresenta as opções Cadastrar e Listar;
  3. Se a rota /cadastrar apresenta o formulário;
  4. Se a rota /listar apresenta a listagem;
  5. Se a seleção entre PF e PJ funciona;
  6. 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.

Cuidados com os dados

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çãoMétodo HTTPEndpoint
Listar PFGET/pf
Listar PJGET/pj
Visualizar PFGET/pf/:id
Visualizar PJGET/pj/:id
Cadastrar PFPOST/pf
Cadastrar PJPOST/pj
Atualizar PFPUT/pf/:id
Atualizar PJPUT/pj/:id
Excluir PFDELETE/pf/:id
Excluir PJDELETE/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.

Depuração por etapas

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.

EtapaConhecimento aplicado
ComponentesOrganização da interface
PropsComunicação entre componentes
StateControle de informações
Ant DesignComponentes visuais
Orientação a ObjetosModelagem das entidades
DAO localPersistência no navegador
React RouterNavegação entre telas
API RESTComunicação HTTP
Async/awaitOperações assíncronas
DAO de backendAcesso remoto aos dados
Estados de carregamentoFeedback durante operações
Tratamento de errosControle 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.
Entrega da atividade

Apresente o projeto atualizado e um breve relatório contendo:

  1. A estrutura dos arquivos implementados;
  2. A identificação dos métodos HTTP utilizados;
  3. Evidências das operações CRUD realizadas;
  4. Uma comparação entre as duas estratégias de persistência;
  5. 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.