📘 Cadastro de Pessoas com Orientação a Objetos e Persistência Local
Nos capítulos anteriores estudamos diferentes recursos utilizados na construção de interfaces com ReactJS:
Componentes
↓
Props
↓
State
↓
Eventos
↓
Renderização condicional
↓
CSS
↓
Tailwind
↓
Ant Design
Agora utilizaremos esses conhecimentos na construção de uma aplicação mais completa.
Desenvolveremos um cadastro de:
Pessoa Física (PF)
e
Pessoa Jurídica (PJ)
integrando diferentes tecnologias e conceitos:
ReactJS
+
Ant Design
+
Orientação a Objetos
+
ViaCEP
+
DAO
+
localStorage
Objetivos
Ao final deste capítulo você deverá ser capaz de:
- construir um formulário composto por diferentes componentes;
- trabalhar com campos específicos para Pessoa Física e Pessoa Jurídica;
- utilizar formulários dinâmicos do Ant Design;
- consultar um serviço externo utilizando
fetch; - transformar dados do formulário em objetos;
- utilizar classes JavaScript na aplicação React;
- aplicar composição e herança;
- compreender o padrão DAO;
- persistir dados utilizando
localStorage.
Evolução da aplicação
Até agora nossos exemplos estavam concentrados principalmente na camada visual.
Neste capítulo teremos:
INTERFACE
│
▼
OBJETOS
│
▼
PERSISTÊNCIA
Isso significa que não teremos apenas componentes React.
Nossa aplicação também possuirá classes responsáveis pela representação do domínio e classes responsáveis pela persistência.
Separação de responsabilidades
Organizaremos a aplicação em três grupos principais.
Interface
Os componentes React ficarão em:
src/components/cadastrapessoa/
Por exemplo:
PessoaFormOO.jsx
EnderecoFormEX.jsx
PFForm.jsx
PJForm.jsx
TelefoneListOO.jsx
...
A pasta cadastrapessoa poderá conter outros componentes e exemplos
relacionados ao cadastro ao longo da evolução do projeto.
Domínio
As classes que representam nossos objetos ficarão em:
src/objetos/pessoas/
Teremos:
Pessoa.mjs
PF.mjs
PJ.mjs
Endereco.mjs
Telefone.mjs
Titulo.mjs
IE.mjs
Essas classes representam conceitos do problema que estamos modelando.
Persistência
Os objetos responsáveis pelo armazenamento ficarão em:
src/objetos/dao/
Neste capítulo utilizaremos:
PFDAOLocal.mjs
PJDAOLocal.mjs
Modelo de objetos
Podemos representar o domínio de forma simplificada:
Pessoa
│
┌───────┴───────┐
│ │
▼ ▼
PF PJ
│ │
▼ ▼
Titulo IE
Pessoa
│
├── Endereco
│
└── Telefone[]
Temos alguns conceitos importantes de Orientação a Objetos.
Herança
Pessoa
▲
│
┌┴─┐
PF PJ
PF e PJ especializam Pessoa.
Composição/associação
Uma pessoa possui:
Endereco
e pode possuir:
Telefone[]
Além disso:
PF → Titulo
PJ → IE
Reutilizando as classes já estudadas
Neste capítulo não reconstruiremos todas as classes de domínio.
Utilizaremos as classes já desenvolvidas anteriormente:
Pessoa
PF
PJ
Endereco
Telefone
Titulo
IE
Elas devem estar disponíveis em:
src/objetos/pessoas/
Obtendo as classes de domínio
As classes de domínio utilizadas neste capítulo estão disponíveis no repositório do projeto:
Classes de domínio — src/objetos/pessoas/
Acesse o diretório acima e verifique se os seguintes arquivos estão disponíveis em seu projeto:
src/objetos/pessoas/
├── Pessoa.mjs
├── PF.mjs
├── PJ.mjs
├── Endereco.mjs
├── Telefone.mjs
├── Titulo.mjs
└── IE.mjs
Neste capítulo essas classes não serão reconstruídas. Elas foram estudadas anteriormente e serão reutilizadas para integrar a camada de interface React à camada de persistência.
Assim conseguimos concentrar o estudo na integração entre:
React
+
Orientação a Objetos
+
Persistência
O que é localStorage?
O navegador disponibiliza um mecanismo de armazenamento denominado:
localStorage
Ele permite armazenar dados localmente no navegador.
Entretanto, o localStorage trabalha com:
chave → texto
Por isso utilizaremos:
JSON.stringify()
para transformar estruturas JavaScript em texto e:
JSON.parse()
para realizar o processo inverso.
Por que utilizar DAO?
Poderíamos fazer diretamente:
PessoaFormOO
│
▼
localStorage
Entretanto, isso faria o componente conhecer detalhes da persistência.
Utilizaremos:
PessoaFormOO
│
▼
DAO
│
▼
localStorage
DAO significa:
Data Access Object
Sua responsabilidade será concentrar as operações relacionadas ao armazenamento.
Integração com ViaCEP
Para facilitar o preenchimento do endereço utilizaremos o serviço ViaCEP.
O processo será:
Usuário informa CEP
│
▼
fetch()
│
▼
ViaCEP
│
▼
Dados do endereço
│
▼
Formulário
Arquitetura final
Nossa organização será semelhante a:
src/
│
├── components/
│ └── cadastrapessoa/
│ ├── PessoaFormOO.jsx
│ ├── EnderecoFormEX.jsx
│ ├── PFForm.jsx
│ ├── PJForm.jsx
│ ├── TelefoneListOO.jsx
│ └── ...
│
└── objetos/
├── pessoas/
│ ├── Pessoa.mjs
│ ├── PF.mjs
│ ├── PJ.mjs
│ ├── Endereco.mjs
│ ├── Telefone.mjs
│ ├── Titulo.mjs
│ └── IE.mjs
│
└── dao/
├── PFDAOLocal.mjs
└── PJDAOLocal.mjs
Nos próximos tópicos construiremos a interface progressivamente e, depois, integraremos os componentes às classes de domínio e aos DAOs.
Construindo o formulário principal
O componente central da aplicação será:
PessoaFormOO.jsx
localizado em:
src/components/cadastrapessoa/
Ele será responsável por coordenar os demais componentes do cadastro.
Importações iniciais
Começaremos com:
import React, { useState } from "react";
import {
Form,
Input,
Button,
Radio,
message
} from "antd";
import EnderecoForm from "./EnderecoFormEX.jsx";
import TelefoneList from "./TelefoneListOO.jsx";
import PFForm from "./PFForm.jsx";
import PJForm from "./PJForm.jsx";
O PessoaFormOO utiliza componentes que construiremos nas próximas partes:
EnderecoFormEX.jsx
TelefoneListOO.jsx
PFForm.jsx
PJForm.jsx
Portanto, acompanhe a construção na sequência apresentada no capítulo.
Instância do formulário
Dentro do componente:
export default function PessoaFormOO() {
const [form] = Form.useForm();
const [tipo, setTipo] = useState("PF");
// ...
}
Temos dois elementos importantes:
form
representa o formulário do Ant Design.
Já:
tipo
indica se estamos cadastrando:
PF
ou:
PJ
Alterando o tipo
Utilizamos:
function onChangeTipo(e) {
setTipo(e.target.value);
}
O fluxo será:
Radio
│
▼
onChangeTipo
│
▼
setTipo
│
▼
PF ou PJ
Estrutura inicial do formulário
Utilizamos:
<Form
layout="vertical"
form={form}
>
A propriedade:
layout="vertical"
faz com que os rótulos sejam apresentados acima dos campos.
Escolhendo PF ou PJ
Adicionamos:
<Form.Item
label="Tipo de Pessoa"
name="tipo"
initialValue="PF"
>
<Radio.Group onChange={onChangeTipo}>
<Radio value="PF">
Pessoa Física
</Radio>
<Radio value="PJ">
Pessoa Jurídica
</Radio>
</Radio.Group>
</Form.Item>
Nome
O campo nome será comum aos dois tipos:
<Form.Item
label="Nome"
name="nome"
rules={[
{
required: true,
message: "Informe o nome!"
}
]}
>
<Input
placeholder="Nome completo ou razão social"
/>
</Form.Item>
Também teremos:
<Form.Item
label="Email"
name="email"
rules={[
{
required: true,
message: "Informe o e-mail!"
},
{
type: "email",
message: "Formato de e-mail inválido!"
}
]}
>
<Input placeholder="exemplo@email.com" />
</Form.Item>
Aqui utilizamos duas validações:
campo obrigatório
+
formato de e-mail
CPF ou CNPJ
Agora utilizaremos renderização condicional:
{tipo === "PF" ? (
<Form.Item
label="CPF"
name="cpf"
rules={[
{
required: true,
message: "Informe o CPF!"
}
]}
>
<Input
placeholder="Somente números"
maxLength={11}
/>
</Form.Item>
) : (
<Form.Item
label="CNPJ"
name="cnpj"
rules={[
{
required: true,
message: "Informe o CNPJ!"
}
]}
>
<Input
placeholder="Somente números"
maxLength={18}
/>
</Form.Item>
)}
Portanto:
tipo
│
├── PF → CPF
│
└── PJ → CNPJ
Isso também explica por que CPF não precisa estar novamente dentro do
PFForm.
Adicionando os subcomponentes
Depois utilizaremos:
<EnderecoForm />
<TelefoneList />
{tipo === "PF"
? <PFForm />
: <PJForm />
}
Nossa estrutura começa a ficar:
PessoaFormOO
│
├── Nome
├── Email
├── CPF/CNPJ
├── EnderecoForm
├── TelefoneList
└── PFForm/PJForm
Botão Salvar
Finalmente:
<Form.Item>
<Button
type="primary"
htmlType="submit"
block
>
Salvar
</Button>
</Form.Item>
Ainda não adicionaremos toda a lógica de persistência.
Primeiro compreenderemos os componentes utilizados pelo formulário.
Na Parte 6 retornaremos ao PessoaFormOO para implementar:
onFinish(values)
Construindo o formulário de endereço
O próximo componente será:
EnderecoFormEX.jsx
localizado em:
src/components/cadastrapessoa/
Sua responsabilidade será:
CEP
│
▼
ViaCEP
│
▼
Endereço
│
▼
Formulário
Importações
import React, { useState } from "react";
import {
Form,
Input,
Select,
message,
Spin
} from "antd";
Também definiremos as UFs:
const UFS = [
"AC","AL","AP","AM","BA","CE","DF",
"ES","GO","MA","MT","MS","MG","PA",
"PB","PR","PE","PI","RJ","RN","RS",
"RO","RR","SC","SP","SE","TO"
];
Obtendo o formulário pai
O componente está dentro de um Form do Ant Design.
Por isso podemos utilizar:
const form = Form.useFormInstance();
Assim, não precisamos receber a instância do formulário por props.
Temos:
PessoaFormOO
│
└── Form
│
└── EnderecoForm
│
▼
useFormInstance()
Estado da consulta
Utilizamos:
const [buscandoCEP, setBuscandoCEP] =
useState(false);
Esse estado será utilizado pelo Spin.
Descobrindo a região
Criamos:
function obterRegiao(uf) {
const mapa = {
Norte:
["AC","AP","AM","PA","RO","RR","TO"],
Nordeste:
["AL","BA","CE","MA","PB","PE","PI","RN","SE"],
"Centro-Oeste":
["DF","GO","MT","MS"],
Sudeste:
["ES","MG","RJ","SP"],
Sul:
["PR","RS","SC"]
};
for (const [regiao, estados]
of Object.entries(mapa)) {
if (estados.includes(uf)) {
return regiao;
}
}
return "";
}
Por exemplo:
DF
│
▼
Centro-Oeste
Consultando o CEP
async function handleCepChange(e) {
const cep =
(e.target.value || "")
.replace(/\D/g, "");
if (cep.length === 8) {
setBuscandoCEP(true);
try {
const resp = await fetch(
`https://viacep.com.br/ws/${cep}/json/`
);
const dados = await resp.json();
if (dados?.erro) {
message.warning(
"CEP não encontrado!"
);
setBuscandoCEP(false);
return;
}
const uf = dados.uf || "";
const regiao = obterRegiao(uf);
const atual =
form.getFieldValue("endereco") || {};
form.setFieldsValue({
endereco: {
...atual,
cep,
logradouro:
dados.logradouro || "",
bairro:
dados.bairro || "",
cidade:
dados.localidade || "",
uf,
regiao
}
});
} catch (err) {
message.error(
"Erro ao buscar CEP."
);
} finally {
setBuscandoCEP(false);
}
}
}
Preservando os dados anteriores
Observe:
const atual =
form.getFieldValue("endereco") || {};
e:
endereco: {
...atual,
// novos valores
}
O operador:
...
preserva os dados existentes e acrescenta/atualiza os valores obtidos.
Utilizando Spin
Podemos envolver os campos em:
<Spin
spinning={buscandoCEP}
tip="Buscando CEP..."
size="small"
>
Quando:
buscandoCEP = true
o usuário recebe uma indicação visual de que a consulta está sendo realizada.
Código completo
import React, { useState } from "react";
import {
Form,
Input,
Select,
message,
Spin
} from "antd";
const UFS = [
"AC","AL","AP","AM","BA","CE","DF",
"ES","GO","MA","MT","MS","MG","PA",
"PB","PR","PE","PI","RJ","RN","RS",
"RO","RR","SC","SP","SE","TO"
];
export default function EnderecoFormEX() {
const form = Form.useFormInstance();
const [buscandoCEP, setBuscandoCEP] =
useState(false);
function obterRegiao(uf) {
const mapa = {
Norte:
["AC","AP","AM","PA","RO","RR","TO"],
Nordeste:
["AL","BA","CE","MA","PB","PE","PI","RN","SE"],
"Centro-Oeste":
["DF","GO","MT","MS"],
Sudeste:
["ES","MG","RJ","SP"],
Sul:
["PR","RS","SC"]
};
for (const [regiao, estados]
of Object.entries(mapa)) {
if (estados.includes(uf)) {
return regiao;
}
}
return "";
}
async function handleCepChange(e) {
const cep =
(e.target.value || "")
.replace(/\D/g, "");
if (cep.length === 8) {
setBuscandoCEP(true);
try {
const resp = await fetch(
`https://viacep.com.br/ws/${cep}/json/`
);
const dados = await resp.json();
if (dados?.erro) {
message.warning(
"CEP não encontrado!"
);
setBuscandoCEP(false);
return;
}
const uf = dados.uf || "";
const regiao = obterRegiao(uf);
const atual =
form.getFieldValue("endereco") || {};
form.setFieldsValue({
endereco: {
...atual,
cep,
logradouro:
dados.logradouro || "",
bairro:
dados.bairro || "",
cidade:
dados.localidade || "",
uf,
regiao
}
});
} catch (err) {
message.error(
"Erro ao buscar CEP."
);
} finally {
setBuscandoCEP(false);
}
}
}
return (
<div style={{ position: "relative" }}>
<h4 style={{ marginTop: 8 }}>
Endereço
</h4>
<Spin
spinning={buscandoCEP}
tip="Buscando CEP..."
size="small"
>
<Form.Item
name={["endereco", "cep"]}
label="CEP"
rules={[
{
required: true,
message: "Informe o CEP!"
}
]}
>
<Input
placeholder="Somente números"
maxLength={8}
onChange={handleCepChange}
/>
</Form.Item>
<Form.Item
name={["endereco", "logradouro"]}
label="Logradouro"
>
<Input />
</Form.Item>
<Form.Item
name={["endereco", "bairro"]}
label="Bairro"
>
<Input />
</Form.Item>
<Form.Item
name={["endereco", "cidade"]}
label="Cidade"
>
<Input />
</Form.Item>
<Form.Item
name={["endereco", "uf"]}
label="UF"
>
<Select
showSearch
placeholder="Selecione"
options={
UFS.map((uf) => ({
value: uf,
label: uf
}))
}
onChange={(uf) => {
const atual =
form.getFieldValue("endereco")
|| {};
form.setFieldsValue({
endereco: {
...atual,
uf,
regiao:
obterRegiao(uf)
}
});
}}
/>
</Form.Item>
<Form.Item
name={["endereco", "regiao"]}
label="Região"
>
<Input disabled />
</Form.Item>
</Spin>
</div>
);
}
O componente agora permite tanto preencher o endereço pelo CEP quanto selecionar manualmente uma UF, atualizando automaticamente a região.
Dados específicos de PF e PJ
Já possuímos no formulário principal:
Nome
Email
CPF/CNPJ
Agora criaremos os dados específicos.
Pessoa Física
O arquivo:
PFForm.jsx
trabalhará com:
Data de nascimento
Título eleitoral
O título possui:
Titulo
│
├── numero
├── zona
└── secao
Código
import React from "react";
import {
Form,
Input,
DatePicker
} from "antd";
function PFForm() {
return (
<>
<Form.Item
label="Data de Nascimento"
name="dataNascimento"
>
<DatePicker
format="DD/MM/YYYY"
style={{ width: "100%" }}
placeholder="Selecione data de nascimento"
/>
</Form.Item>
<Form.Item
label="Título Eleitoral - Número"
name={["titulo", "numero"]}
>
<Input
placeholder="Número do título"
/>
</Form.Item>
<Form.Item
label="Zona"
name={["titulo", "zona"]}
>
<Input
placeholder="Zona eleitoral"
/>
</Form.Item>
<Form.Item
label="Seção"
name={["titulo", "secao"]}
>
<Input
placeholder="Seção eleitoral"
/>
</Form.Item>
</>
);
}
export default PFForm;
A estrutura produzida será:
{
dataNascimento: ...,
titulo: {
numero: "...",
zona: "...",
secao: "..."
}
}
O CPF é coletado pelo PessoaFormOO.
Por isso não precisamos duplicá-lo dentro do PFForm.
Pessoa Jurídica
O arquivo:
PJForm.jsx
será responsável pelos dados da:
Inscrição Estadual
Sua estrutura será:
IE
│
├── numero
├── estado
└── dataRegistro
Código
import React from "react";
import {
Form,
Input,
Select,
DatePicker
} from "antd";
export default function PJForm() {
const estados = [
"AC","AL","AP","AM","BA","CE","DF",
"ES","GO","MA","MT","MS","MG","PA",
"PB","PR","PE","PI","RJ","RN","RS",
"RO","RR","SC","SP","SE","TO"
];
return (
<>
<h3 style={{ marginTop: 20 }}>
Dados da Inscrição Estadual (IE)
</h3>
<Form.Item
label="Número da IE"
name={["ie", "numero"]}
rules={[{ required: false }]}
>
<Input
placeholder={
"Informe o número da IE (se aplicável)"
}
/>
</Form.Item>
<Form.Item
label="Estado da IE"
name={["ie", "estado"]}
>
<Select
placeholder="Selecione o estado"
allowClear
options={
estados.map((uf) => ({
label: uf,
value: uf
}))
}
/>
</Form.Item>
<Form.Item
label="Data de Registro da IE"
name={["ie", "dataRegistro"]}
>
<DatePicker
style={{ width: "100%" }}
format="YYYY-MM-DD"
allowClear
placeholder={
"Selecione a data de registro"
}
/>
</Form.Item>
</>
);
}
O resultado será:
{
ie: {
numero: "...",
estado: "DF",
dataRegistro: ...
}
}
Organização resultante
Agora temos:
PessoaFormOO
│
├── CPF
│ └── PFForm
│ ├── dataNascimento
│ └── titulo
│
└── CNPJ
└── PJForm
└── ie
Os componentes específicos complementam os dados coletados pelo formulário principal.
Criando uma lista dinâmica de telefones
Uma pessoa poderá possuir:
0, 1 ou vários telefones
Portanto, precisamos trabalhar com uma coleção:
telefones[]
Para isso utilizaremos um recurso específico do Ant Design:
Form.List
Importações
import React from "react";
import {
Form,
Input,
Button,
Space
} from "antd";
import {
MinusCircleOutlined,
PlusOutlined
} from "@ant-design/icons";
Form.List
A estrutura principal será:
<Form.List name="telefones">
{(fields, { add, remove }) => (
// ...
)}
</Form.List>
O Ant Design fornece automaticamente:
fields
add()
remove()
Assim não precisamos criar um useState separado para manter a lista.
Percorrendo os telefones
Utilizamos:
fields.map(
({ key, name, ...rest }) => (
// campos
)
)
Para cada telefone teremos:
DDD
Número
Remover
DDD
<Form.Item
{...rest}
name={[name, "ddd"]}
rules={[
{
required: true,
message: "DDD obrigatório"
}
]}
>
<Input
placeholder="DDD"
style={{ width: 90 }}
maxLength={2}
/>
</Form.Item>
Número
<Form.Item
{...rest}
name={[name, "numero"]}
rules={[
{
required: true,
message: "Número obrigatório"
}
]}
>
<Input
placeholder="Número"
style={{ width: 200 }}
maxLength={9}
/>
</Form.Item>
Removendo
Utilizamos:
<MinusCircleOutlined
onClick={() => remove(name)}
/>
Adicionando
Para criar um novo conjunto de campos:
<Button
type="dashed"
onClick={() => add()}
icon={<PlusOutlined />}
block
>
Adicionar Telefone
</Button>
Código completo
import React from "react";
import {
Form,
Input,
Button,
Space
} from "antd";
import {
MinusCircleOutlined,
PlusOutlined
} from "@ant-design/icons";
export default function TelefoneListOO() {
return (
<>
<h4 style={{ marginTop: 8 }}>
Telefones
</h4>
<Form.List name="telefones">
{(fields, { add, remove }) => (
<>
{fields.map(
({ key, name, ...rest }) => (
<Space
key={key}
style={{
display: "flex",
marginBottom: 8
}}
align="baseline"
>
<Form.Item
{...rest}
name={[name, "ddd"]}
rules={[
{
required: true,
message: "DDD obrigatório"
}
]}
>
<Input
placeholder="DDD"
style={{ width: 90 }}
maxLength={2}
/>
</Form.Item>
<Form.Item
{...rest}
name={[name, "numero"]}
rules={[
{
required: true,
message: "Número obrigatório"
}
]}
>
<Input
placeholder="Número"
style={{ width: 200 }}
maxLength={9}
/>
</Form.Item>
<MinusCircleOutlined
onClick={() => remove(name)}
/>
</Space>
)
)}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
icon={<PlusOutlined />}
block
>
Adicionar Telefone
</Button>
</Form.Item>
</>
)}
</Form.List>
</>
);
}
Estrutura produzida
Depois da inclusão de dois telefones, o formulário poderá produzir:
{
telefones: [
{
ddd: "61",
numero: "999998888"
},
{
ddd: "61",
numero: "33334444"
}
]
}
Observe que esses ainda são:
dados do formulário
Na próxima etapa eles serão transformados em:
objetos Telefone
Transformando os dados do formulário em objetos
Agora retornaremos ao:
PessoaFormOO.jsx
Precisamos implementar:
onFinish(values)
O objetivo será transformar:
dados do Form
em:
objetos de domínio
Importações
Adicione:
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";
import PFDAO from "../../objetos/dao/PFDAOLocal.mjs";
import PJDAO from "../../objetos/dao/PJDAOLocal.mjs";
Construindo o endereço
const endVals =
values.endereco || {};
const end = new Endereco();
end.setCep(endVals.cep);
end.setLogradouro(endVals.logradouro);
end.setBairro(endVals.bairro);
end.setCidade(endVals.cidade);
end.setUf(endVals.uf);
end.setRegiao(endVals.regiao);
Temos:
values.endereco
│
▼
new Endereco()
│
▼
end
Construindo uma Pessoa Física
const pf = new PF();
pf.setNome(values.nome);
pf.setEmail(values.email);
pf.setCPF(values.cpf);
pf.setEndereco(end);
Observe que o método real da classe é:
setCPF()
Criando o título
Os dados:
values.titulo
ainda não representam uma instância de Titulo.
Precisamos fazer:
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);
}
O fluxo será:
values.titulo
│
▼
new Titulo
│
▼
pf.setTitulo()
Pessoa Jurídica
Para PJ:
const pj = new PJ();
pj.setNome(values.nome);
pj.setEmail(values.email);
pj.setCNPJ(values.cnpj);
pj.setEndereco(end);
Criando a IE
if (values.ie) {
const ie = new IE();
ie.setNumero(
values.ie.numero
);
ie.setEstado(
values.ie.estado
);
// tratamento da data
pj.setIE(ie);
}
Atenção ao DatePicker
O valor produzido pelo DatePicker não deve ser tratado simplesmente como
uma string.
Antes da persistência, podemos convertê-lo:
const dr =
values.ie.dataRegistro;
const dataRegistro =
dr &&
typeof dr === "object" &&
typeof dr.format === "function"
? dr.format("YYYY-MM-DD")
: dr || "";
ie.setDataRegistro(dataRegistro);
Assim:
DatePicker
│
▼
objeto de data
│
▼
format("YYYY-MM-DD")
│
▼
"2026-10-01"
Telefones
Os telefones também precisam ser transformados:
if (values.telefones?.length > 0) {
values.telefones.forEach((tel) => {
const fone = new Telefone();
fone.setDdd(tel.ddd);
fone.setNumero(tel.numero);
pessoa.addTelefone(fone);
});
}
Função onFinish
Podemos agora construir:
function onFinish(values) {
try {
let pessoa;
const endVals =
values.endereco || {};
const end = new Endereco();
end.setCep(endVals.cep);
end.setLogradouro(endVals.logradouro);
end.setBairro(endVals.bairro);
end.setCidade(endVals.cidade);
end.setUf(endVals.uf);
end.setRegiao(endVals.regiao);
if (values.tipo === "PF") {
const pf = new PF();
pf.setNome(values.nome);
pf.setEmail(values.email);
pf.setCPF(values.cpf);
pf.setEndereco(end);
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);
}
if (values.telefones?.length > 0) {
values.telefones.forEach((tel) => {
const fone = new Telefone();
fone.setDdd(tel.ddd);
fone.setNumero(tel.numero);
pf.addTelefone(fone);
});
}
pessoa = pf;
} else {
const pj = new PJ();
pj.setNome(values.nome);
pj.setEmail(values.email);
pj.setCNPJ(values.cnpj);
pj.setEndereco(end);
if (values.ie) {
const ie = new IE();
ie.setNumero(values.ie.numero);
ie.setEstado(values.ie.estado);
const dr =
values.ie.dataRegistro;
const dataRegistro =
dr &&
typeof dr === "object" &&
typeof dr.format === "function"
? dr.format("YYYY-MM-DD")
: dr || "";
ie.setDataRegistro(dataRegistro);
pj.setIE(ie);
}
if (values.telefones?.length > 0) {
values.telefones.forEach((tel) => {
const fone = new Telefone();
fone.setDdd(tel.ddd);
fone.setNumero(tel.numero);
pj.addTelefone(fone);
});
}
pessoa = pj;
}
const dao =
values.tipo === "PF"
? new PFDAO()
: new PJDAO();
dao.salvar(pessoa);
message.success(
"Pessoa cadastrada com sucesso!"
);
form.resetFields();
} catch (erro) {
console.error(
"Erro ao salvar:",
erro
);
message.error(
"Erro ao salvar registro: "
+ erro.message
);
}
}
Agora associe a função ao formulário:
<Form
layout="vertical"
form={form}
onFinish={onFinish}
scrollToFirstError
>
Código completo do PessoaFormOO.jsx
Depois de construir separadamente cada parte do formulário, podemos reunir todos os elementos no componente principal.
Crie ou atualize o arquivo:
src/components/cadastrapessoa/PessoaFormOO.jsx
com o código completo:
import React, { useState } from "react";
import {
Form,
Input,
Button,
Radio,
message
} from "antd";
// Componentes do formulário
import EnderecoForm from "./EnderecoFormEX.jsx";
import TelefoneList from "./TelefoneListOO.jsx";
import PFForm from "./PFForm.jsx";
import PJForm from "./PJForm.jsx";
// 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";
// DAOs
import PFDAO from "../../objetos/dao/PFDAOLocal.mjs";
import PJDAO from "../../objetos/dao/PJDAOLocal.mjs";
export default function PessoaFormOO() {
const [form] = Form.useForm();
const [tipo, setTipo] = useState("PF");
// =========================
// TROCA PF/PJ
// =========================
function onChangeTipo(e) {
const novoTipo = e.target.value;
setTipo(novoTipo);
const valoresAtuais =
form.getFieldsValue();
form.resetFields();
form.setFieldsValue({
...valoresAtuais,
tipo: novoTipo
});
}
// =========================
// SALVAR
// =========================
function onFinish(values) {
try {
let pessoa;
// =========================
// ENDEREÇO
// =========================
const endVals =
values.endereco || {};
const end = new Endereco();
end.setCep(endVals.cep);
end.setLogradouro(endVals.logradouro);
end.setBairro(endVals.bairro);
end.setCidade(endVals.cidade);
end.setUf(endVals.uf);
end.setRegiao(endVals.regiao);
// =========================
// PESSOA FÍSICA
// =========================
if (values.tipo === "PF") {
const pf = new PF();
pf.setNome(values.nome);
pf.setEmail(values.email);
pf.setCPF(values.cpf);
pf.setEndereco(end);
// 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);
}
// Telefones
if (values.telefones?.length > 0) {
values.telefones.forEach(
(tel) => {
const fone =
new Telefone();
fone.setDdd(tel.ddd);
fone.setNumero(
tel.numero
);
pf.addTelefone(fone);
}
);
}
pessoa = pf;
}
// =========================
// PESSOA JURÍDICA
// =========================
else {
const pj = new PJ();
pj.setNome(values.nome);
pj.setEmail(values.email);
pj.setCNPJ(values.cnpj);
pj.setEndereco(end);
// Inscrição Estadual
if (values.ie) {
const ie = new IE();
ie.setNumero(
values.ie.numero
);
ie.setEstado(
values.ie.estado
);
/*
* O DatePicker do Ant Design trabalha
* com um objeto de data.
*
* Antes da persistência, convertemos
* o valor para YYYY-MM-DD.
*/
const dr =
values.ie.dataRegistro;
const dataRegistro =
dr &&
typeof dr === "object" &&
typeof dr.format === "function"
? dr.format("YYYY-MM-DD")
: dr || "";
ie.setDataRegistro(
dataRegistro
);
pj.setIE(ie);
}
// Telefones
if (values.telefones?.length > 0) {
values.telefones.forEach(
(tel) => {
const fone =
new Telefone();
fone.setDdd(tel.ddd);
fone.setNumero(
tel.numero
);
pj.addTelefone(fone);
}
);
}
pessoa = pj;
}
// =========================
// PERSISTÊNCIA
// =========================
const dao =
values.tipo === "PF"
? new PFDAO()
: new PJDAO();
dao.salvar(pessoa);
message.success(
"Pessoa cadastrada com sucesso!"
);
form.resetFields();
setTipo("PF");
} catch (erro) {
console.error(
"Erro ao salvar:",
erro
);
message.error(
"Erro ao salvar registro: "
+ erro.message
);
}
}
// =========================
// INTERFACE
// =========================
return (
<div
style={{
maxWidth: 800,
margin: "24px auto",
padding: 24
}}
>
<h2>
Cadastro de Pessoa
</h2>
<Form
layout="vertical"
form={form}
onFinish={onFinish}
scrollToFirstError
>
{/* Tipo de pessoa */}
<Form.Item
label="Tipo de Pessoa"
name="tipo"
initialValue="PF"
>
<Radio.Group
onChange={onChangeTipo}
>
<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="Email"
name="email"
rules={[
{
required: true,
message:
"Informe o e-mail!"
},
{
type: "email",
message:
"Formato de e-mail inválido!"
}
]}
>
<Input
placeholder="exemplo@email.com"
/>
</Form.Item>
{/* CPF ou CNPJ */}
{tipo === "PF" ? (
<Form.Item
label="CPF"
name="cpf"
rules={[
{
required: true,
message:
"Informe o CPF!"
}
]}
>
<Input
placeholder="Somente números"
maxLength={11}
/>
</Form.Item>
) : (
<Form.Item
label="CNPJ"
name="cnpj"
rules={[
{
required: true,
message:
"Informe o CNPJ!"
}
]}
>
<Input
placeholder="Somente números"
maxLength={18}
/>
</Form.Item>
)}
{/* Endereço */}
<EnderecoForm />
{/* Telefones */}
<TelefoneList />
{/* Dados específicos */}
{tipo === "PF"
? <PFForm />
: <PJForm />
}
{/* Salvar */}
<Form.Item
style={{ marginTop: 20 }}
>
<Button
type="primary"
htmlType="submit"
block
>
Salvar
</Button>
</Form.Item>
</Form>
</div>
);
}
O PessoaFormOO é o ponto de integração entre os componentes da interface,
os objetos de domínio e a camada de persistência:
PessoaFormOO
│
├── Form Ant Design
│
├── Nome e e-mail
│
├── CPF ou CNPJ
│
├── EnderecoForm
│
├── TelefoneList
│
├── PFForm / PJForm
│
├── onFinish()
│ ├── Endereco
│ ├── Telefone[]
│ ├── PF + Titulo
│ └── PJ + IE
│
└── PFDAO / PJDAO
│
▼
localStorage
Fluxo completo
values
│
├── endereco ───────► Endereco
│
├── telefones[] ────► Telefone[]
│
└── tipo
│
├── PF ────────► PF + Titulo
│
└── PJ ────────► PJ + IE
│
▼
pessoa
│
▼
DAO
Agora os dados do formulário foram transformados em uma estrutura de objetos compatível com nosso domínio.
Persistindo os objetos com DAO
Depois da transformação temos:
Objeto PF/PJ
│
▼
DAO
│
▼
localStorage
Os arquivos estarão em:
src/objetos/dao/
PFDAOLocal.mjs
export default class PFDAO {
constructor(storageKey = "pessoasPF") {
this.storageKey = storageKey;
}
salvar(pf) {
const lista = this.listar();
const end = pf.getEndereco();
const objEndereco = end ? {
cep: end.getCep(),
logradouro: end.getLogradouro(),
bairro: end.getBairro(),
cidade: end.getCidade(),
uf: end.getUf(),
regiao: end.getRegiao()
} : null;
const telefones =
(pf.getTelefones() || []).map(
f => ({
ddd: f.getDdd(),
numero: f.getNumero()
})
);
const titulo =
pf.getTitulo() ? {
numero:
pf.getTitulo().getNumero(),
zona:
pf.getTitulo().getZona(),
secao:
pf.getTitulo().getSecao()
} : null;
const obj = {
nome: pf.getNome(),
email: pf.getEmail(),
cpf: pf.getCPF(),
endereco: objEndereco,
telefones,
titulo
};
lista.push(obj);
localStorage.setItem(
this.storageKey,
JSON.stringify(lista)
);
return obj;
}
listar() {
const dados =
localStorage.getItem(
this.storageKey
);
return dados
? JSON.parse(dados)
: [];
}
excluir(cpf) {
const lista =
this.listar().filter(
p => p.cpf !== cpf
);
localStorage.setItem(
this.storageKey,
JSON.stringify(lista)
);
}
limpar() {
localStorage.removeItem(
this.storageKey
);
}
}
PJDAOLocal.mjs
export default class PJDAO {
constructor(storageKey = "pessoasPJ") {
this.storageKey = storageKey;
}
salvar(pj) {
const lista = this.listar();
const end = pj.getEndereco();
const objEndereco = end
? {
cep: end.getCep(),
logradouro: end.getLogradouro(),
bairro: end.getBairro(),
cidade: end.getCidade(),
uf: end.getUf(),
regiao: end.getRegiao()
}
: null;
const telefones =
(pj.getTelefones() || []).map(
f => ({
ddd: f.getDdd(),
numero: f.getNumero()
})
);
const ie =
pj.getIE() ? {
numero:
pj.getIE().getNumero(),
estado:
pj.getIE().getEstado(),
dataRegistro:
pj.getIE().getDataRegistro()
} : null;
const obj = {
nome: pj.getNome(),
email: pj.getEmail(),
cnpj: pj.getCNPJ(),
endereco: objEndereco,
telefones,
ie
};
lista.push(obj);
localStorage.setItem(
this.storageKey,
JSON.stringify(lista)
);
return obj;
}
listar() {
const dados =
localStorage.getItem(
this.storageKey
);
return dados
? JSON.parse(dados)
: [];
}
excluir(cnpj) {
const lista =
this.listar().filter(
p => p.cnpj !== cnpj
);
localStorage.setItem(
this.storageKey,
JSON.stringify(lista)
);
}
limpar() {
localStorage.removeItem(
this.storageKey
);
}
}
Serialização
Os DAOs realizam uma transformação importante:
Objetos de domínio
│
▼
getters
│
▼
Objetos simples
│
▼
JSON.stringify()
│
▼
localStorage
Por exemplo:
Titulo
│
├── getNumero()
├── getZona()
└── getSecao()
│
▼
{
numero: "...",
zona: "...",
secao: "..."
}
Operações disponíveis
Os dois DAOs disponibilizam:
salvar()
listar()
excluir()
limpar()
A diferença principal é:
| PF | PJ | |
|---|---|---|
| Chave | pessoasPF | pessoasPJ |
| Identificador | CPF | CNPJ |
| Objeto específico | Título | IE |
Formato armazenado
Para PF:
{
"nome": "Maria Silva",
"email": "maria@email.com",
"cpf": "12345678901",
"endereco": {
"cep": "72300000",
"cidade": "Brasília",
"uf": "DF",
"regiao": "Centro-Oeste"
},
"telefones": [
{
"ddd": "61",
"numero": "999998888"
}
],
"titulo": {
"numero": "1234567",
"zona": "12",
"secao": "345"
}
}
Recuperação dos dados
O método:
listar()
utiliza:
JSON.parse(dados)
Porém, existe uma diferença importante:
JSON
│
▼
JSON.parse()
│
▼
Objeto simples
O resultado não será automaticamente:
new PF()
new Endereco()
new Telefone()
Caso seja necessário recuperar também os métodos das classes, será preciso realizar um processo de:
reidratação
Para simples listagem, entretanto, o JSON recuperado pode ser suficiente.
Integrando o formulário ao App.jsx
Todos os componentes do cadastro já estão construídos. Agora precisamos definir qual componente será apresentado pela aplicação.
Para isso, utilizaremos o arquivo:
src/App.jsx
Neste momento, nossa aplicação possui apenas a funcionalidade de cadastro.
Por isso, o App.jsx será propositalmente simples.
Código completo do App.jsx
import React from "react";
import PessoaForm
from "./components/cadastrapessoa/PessoaFormOO.jsx";
function App() {
return (
<PessoaForm />
);
}
export default App;
O fluxo de inicialização da interface passa a ser:
main.jsx
│
▼
App.jsx
│
▼
PessoaFormOO
│
├── EnderecoFormEX
├── TelefoneListOO
├── PFForm
└── PJForm
O App.jsx funciona, neste momento, como o ponto de entrada para a interface
de cadastro desenvolvida ao longo do capítulo.
Neste capítulo o App.jsx apresenta diretamente:
<PessoaFormOO />
Nos próximos conteúdos a aplicação será ampliada para permitir:
Cadastro
↓
Listagem
↓
Edição
↓
Exclusão
Com a inclusão dessas funcionalidades, o App.jsx também evoluirá e poderá
passar a trabalhar com diferentes páginas e rotas.
Por enquanto, manteremos sua estrutura simples para concentrar o estudo no processo de cadastro e na persistência local.
Estrutura completa neste estágio
Neste ponto, a estrutura necessária para o cadastro é:
src/
│
├── App.jsx
│
├── components/
│ └── cadastrapessoa/
│ ├── PessoaFormOO.jsx
│ ├── EnderecoFormEX.jsx
│ ├── TelefoneListOO.jsx
│ ├── PFForm.jsx
│ └── PJForm.jsx
│
└── objetos/
├── pessoas/
│ ├── Pessoa.mjs
│ ├── PF.mjs
│ ├── PJ.mjs
│ ├── Endereco.mjs
│ ├── Telefone.mjs
│ ├── Titulo.mjs
│ └── IE.mjs
│
└── dao/
├── PFDAOLocal.mjs
└── PJDAOLocal.mjs
Ao executar a aplicação, o React carregará:
App
│
▼
PessoaFormOO
│
▼
Formulário de Cadastro
Integrando a aplicação
Agora podemos visualizar o resultado completo:
USUÁRIO
│
▼
PessoaFormOO
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
EnderecoFormEX TelefoneListOO PFForm/PJForm
│ │ │
▼ ▼ ▼
endereço telefones[] Titulo / IE
│ │ │
└──────────────────┼──────────────────┘
│
▼
values
│
▼
onFinish
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
Endereco Telefone[] PF / PJ
│
┌────┴────┐
▼ ▼
Titulo IE
│ │
└────┬────┘
│
▼
pessoa
│
▼
PFDAO/PJDAO
│
▼
localStorage
Executando a aplicação
Inicie o projeto:
npm run dev
Depois acesse a aplicação pelo navegador.
Teste 1 — PF/PJ
Alterne entre:
Pessoa Física
e:
Pessoa Jurídica
Verifique:
PF → CPF + PFForm
PJ → CNPJ + PJForm
Teste 2 — Validação
Tente enviar o formulário sem preencher os campos obrigatórios.
Observe as mensagens do Ant Design.
Teste também um e-mail inválido.
Teste 3 — ViaCEP
Informe um CEP válido.
Observe:
CEP
│
▼
ViaCEP
│
▼
Logradouro
Bairro
Cidade
UF
Região
Observe também o Spin durante a consulta.
Teste 4 — UF e região
Selecione manualmente uma UF.
Verifique se o campo:
Região
é atualizado automaticamente.
Teste 5 — Telefones
Clique em:
Adicionar Telefone
Adicione dois ou mais telefones.
Depois remova um deles.
Verifique também as validações de:
DDD
Número
Teste 6 — Cadastro de PF
Cadastre uma Pessoa Física preenchendo:
Nome
Email
CPF
Endereço
Telefones
Título
Depois salve.
Teste 7 — Cadastro de PJ
Cadastre uma Pessoa Jurídica preenchendo:
Nome
Email
CNPJ
Endereço
Telefones
IE
Salve novamente.
Inspecionando o localStorage
Abra as ferramentas de desenvolvimento do navegador.
Procure pelas chaves:
pessoasPF
e:
pessoasPJ
Analise os valores armazenados.
O que aprendemos?
Neste capítulo integramos:
ReactJS
├── Componentes
├── Props
├── State
└── Renderização condicional
Ant Design
├── Form
├── Form.Item
├── Form.List
├── Input
├── Select
├── DatePicker
├── Radio
├── Button
├── Spin
└── message
JavaScript
├── Objetos
├── Arrays
├── map
├── forEach
├── async/await
├── fetch
├── JSON.stringify
└── JSON.parse
Orientação a Objetos
├── Classes
├── Objetos
├── Encapsulamento
├── Herança
└── Composição
Persistência
├── DAO
└── localStorage
Arquitetura resultante
┌──────────────────────────────┐
│ INTERFACE │
│ │
│ React + AntD │
│ │
│ components/cadastrapessoa/ │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ DOMÍNIO │
│ │
│ Classes JavaScript │
│ │
│ objetos/pessoas/ │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ PERSISTÊNCIA │
│ │
│ DAO │
│ │
│ objetos/dao/ │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ localStorage │
└──────────────────────────────┘
Exercício de Fixação
Reproduza e execute todos os códigos apresentados neste capítulo.
O objetivo não é apenas copiar o código, mas compreender o caminho percorrido pelos dados desde a interface até o armazenamento.
1. Estrutura do projeto
Verifique se estão disponíveis:
src/components/cadastrapessoa/
src/objetos/pessoas/
src/objetos/dao/
Confirme a existência dos componentes, classes e DAOs utilizados.
2. Interface
Execute e teste:
PessoaFormOO
EnderecoFormEX
TelefoneListOO
PFForm
PJForm
Verifique:
- seleção PF/PJ;
- CPF/CNPJ;
- validações;
- consulta ViaCEP;
- identificação da região;
- inclusão e remoção de telefones;
- campos específicos de PF e PJ.
3. Objetos
Utilizando o código de onFinish, identifique em que momento são criadas
instâncias de:
PF
PJ
Endereco
Telefone
Titulo
IE
Explique de quais campos do formulário vêm os dados utilizados para construir cada objeto.
4. Persistência
Realize pelo menos:
1 cadastro de PF
1 cadastro de PJ
Depois localize no navegador:
pessoasPF
pessoasPJ
Compare os dados armazenados com os objetos construídos pela aplicação.
5. Análise
Localize no projeto exemplos de:
useState
Form.useForm
Form.useFormInstance
Form.List
renderização condicional
async/await
fetch
map
forEach
JSON.stringify
JSON.parse
Explique a responsabilidade de cada recurso no contexto da aplicação.
6. Fluxo final
Certifique-se de compreender todo o processo:
Usuário
│
▼
Formulário
│
▼
values
│
▼
Objetos de domínio
│
▼
DAO
│
▼
JSON
│
▼
localStorage
Ao concluir a atividade, você deverá ser capaz de explicar não apenas como cadastrar uma pessoa, mas também como os diferentes componentes, objetos e camadas da aplicação colaboram durante o processo.