Formulários
Guia completo para implementação de formulários na plataforma Gaio.
- Introdução
- useForm
- Field Components
- useWatch
- useFieldArray
- Validação com Zod
- Padrões Avançados
- Anti-patterns
Introdução
Seção intitulada “Introdução”| Biblioteca | Versão | Propósito |
|---|---|---|
react-hook-form | 7.x | Gerenciamento de formulários |
zod | 4.x | Validação de schemas |
@gaio/components | - | Hooks e componentes base |
@hookform/resolvers | - | Integração Zod + RHF |
Arquitetura
Seção intitulada “Arquitetura”┌─────────────────────────────────────────────────────────────┐│ useForm ││ (schema, defaultValues, masks, formatters) │└─────────────────────────┬───────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ <Form {...form}> ││ (FormProvider) │└─────────────────────────┬───────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼┌─────────────────┐ ┌───────────┐ ┌───────────────┐│ InputField │ │SelectField│ │ useFieldArray ││ (withController)│ │ │ │ │└─────────────────┘ └───────────┘ └───────────────┘Imports Padrão
Seção intitulada “Imports Padrão”// De @gaio/componentsimport { Form, useForm } from "@gaio/components";
// De react-hook-form (quando necessário)import { useWatch, useFieldArray } from "react-hook-form";
// Fields do consoleimport { InputField } from "@/components/fields/input";import { SelectField } from "@/components/fields/select";useForm
Seção intitulada “useForm”O hook useForm é um wrapper do react-hook-form com integração Zod automática.
Localização: @gaio/components
Parâmetros
Seção intitulada “Parâmetros”| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
schema | z.ZodObject | Sim | Schema Zod para validação |
defaultValues | DefaultValues<T> | Não | Valores iniciais do form |
masks | Record<string, Mask> | Não | Máscaras por campo (CPF, telefone) |
formatters | Record<string, (v) => string> | Não | Formatadores de valor |
fieldReValidateMode | Record<string, 'onChange'> | Não | Campos que revalidam onChange |
disabled | boolean | Não | Desabilita todo o form |
context | Record<string, any> | Não | Contexto adicional |
Uso Básico
Seção intitulada “Uso Básico”import { z } from "zod";import { Form, useForm } from "@gaio/components";import { InputField } from "@/components/fields/input";
const ProfileSchema = z.object({ name: z.string().min(1, "Campo obrigatório"), email: z.email("E-mail inválido"),});
function ProfileForm() { const form = useForm({ schema: ProfileSchema, defaultValues: { name: "", email: "" }, });
const onSubmit = (data: z.infer<typeof ProfileSchema>) => { console.log(data); };
return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <InputField name="name" label="Nome" control={form.control} /> <InputField name="email" label="E-mail" control={form.control} /> <button type="submit">Salvar</button> </form> </Form> );}Com Máscaras e Formatters
Seção intitulada “Com Máscaras e Formatters”import { cpfMask, phoneMask } from "@gaio/common/masks";
const form = useForm({ schema: ContactSchema, defaultValues: { cpf: "", phone: "" }, masks: { cpf: cpfMask, phone: phoneMask, }, formatters: { cpf: (value) => value.replace(/\D/g, ""), },});Com Revalidação onChange
Seção intitulada “Com Revalidação onChange”const form = useForm({ schema: LoginSchema, fieldReValidateMode: { email: "onChange", },});Retorno do useForm
Seção intitulada “Retorno do useForm”| Método | Descrição |
|---|---|
form.control | Controller para passar aos fields |
form.handleSubmit(fn) | Wrapper de submit com validação |
form.reset(values?) | Reseta o form |
form.setValue(name, value, opts) | Define valor programaticamente |
form.getValues(name?) | Obtém valores atuais |
form.watch(name?) | Observa mudanças (cuidado!) |
form.trigger(name?) | Dispara validação manual |
form.setError(name, error) | Define erro manualmente |
form.clearErrors(name?) | Limpa erros |
form.formState | Estado: isDirty, isSubmitting, errors |
Field Components
Seção intitulada “Field Components”Campos Disponíveis
Seção intitulada “Campos Disponíveis”| Campo | Arquivo | Descrição |
|---|---|---|
InputField | input.tsx | Input de texto, email, password |
TextareaField | textarea.tsx | Textarea com auto-resize |
NumberField | number.tsx | Input numérico |
PhoneField | phone-field.tsx | Input de telefone |
SelectField | select.tsx | Select dropdown |
MultiSelectField | multi-select-field.tsx | Multi-select |
ComboboxField | combobox.tsx | Combobox com busca |
CheckboxField | checkbox-field.tsx | Checkbox simples |
RadioGroupField | radio-group-field.tsx | Grupo de radio buttons |
SwitchField | switch.tsx | Toggle switch |
DateField | date.tsx | Date picker |
ListField | list.tsx | Lista dinâmica de items |
UploadField | upload.tsx | Upload de arquivos |
AutocompleteField | autocomplete-field.tsx | Autocomplete async |
Props Comuns
Seção intitulada “Props Comuns”Todos os fields aceitam estas props via BaseFieldProps:
type BaseFieldProps = { name: string; // Nome do campo no schema control: Control; // form.control label?: string; // Label acima do campo labelAddon?: ReactNode; // Conteúdo à direita da label description?: string; // Descrição abaixo da label helper?: string; // Texto auxiliar abaixo do campo tooltip?: string; // Tooltip no ícone de info placeholder?: string; // Placeholder do input disabled?: boolean; // Desabilita o campo required?: boolean; // Marca como obrigatório (auto-detectado do schema) hideErrorMessage?: boolean; // Esconde mensagem de erro mask?: Mask; // Override de máscara formatter?: (v) => string; // Override de formatter};Estrutura Visual
Seção intitulada “Estrutura Visual”┌──────────────────────────────────────────────┐│ Label * [labelAddon]││ Descrição em texto menor │├──────────────────────────────────────────────┤│ ┌──────────────────────────────────────────┐ ││ │ Input/Select/etc │ ││ └──────────────────────────────────────────┘ ││ Helper text em cinza ││ Mensagem de erro │└──────────────────────────────────────────────┘useWatch
Seção intitulada “useWatch”Hook para observar valores de campos de forma otimizada.
Localização: react-hook-form
Quando Usar
Seção intitulada “Quando Usar”| Cenário | Solução |
|---|---|
| Renderização condicional baseada em valor | useWatch |
| Side effects em useEffect | form.watch() com deps |
| Validação dependente de outro campo | Schema Zod com refine |
Sintaxe: Campo Único
Seção intitulada “Sintaxe: Campo Único”import { useWatch } from "react-hook-form";
const email = useWatch({ control: form.control, name: "email",});Sintaxe: Múltiplos Campos
Seção intitulada “Sintaxe: Múltiplos Campos”const [name, email] = useWatch({ control: form.control, name: ["name", "email"],});Exemplo: Renderização Condicional
Seção intitulada “Exemplo: Renderização Condicional”function PaymentForm() { const form = useForm({ schema: PaymentSchema });
const paymentMethod = useWatch({ control: form.control, name: "payment_method", });
return ( <Form {...form}> <SelectField name="payment_method" options={methods} control={form.control} />
{paymentMethod === "credit_card" && ( <> <InputField name="card_number" control={form.control} /> <InputField name="cvv" control={form.control} /> </> )}
{paymentMethod === "pix" && ( <InputField name="pix_key" control={form.control} /> )} </Form> );}useFieldArray
Seção intitulada “useFieldArray”Hook para gerenciar arrays dinâmicos de campos.
Localização: react-hook-form
| Propriedade | Tipo | Descrição |
|---|---|---|
fields | Array<{id, ...data}> | Items com ID único |
append(data) | Function | Adiciona item ao final |
prepend(data) | Function | Adiciona item no início |
remove(index) | Function | Remove item por índice |
insert(index, data) | Function | Insere em posição específica |
swap(from, to) | Function | Troca posição de items |
move(from, to) | Function | Move item para posição |
replace(data[]) | Function | Substitui todo o array |
Setup Básico
Seção intitulada “Setup Básico”import { useFieldArray } from "react-hook-form";
const InviteSchema = z.object({ invites: z .array( z.object({ email: z.email("E-mail inválido"), }), ) .max(10, "Máximo 10 convites"),});
function InviteForm() { const form = useForm({ schema: InviteSchema });
const { fields, append, remove } = useFieldArray({ control: form.control, name: "invites", });
return ( <Form {...form}> {fields.map((field, index) => ( <div key={field.id}> <InputField name={`invites.${index}.email`} control={form.control} /> <Button onClick={() => remove(index)}>Remover</Button> </div> ))}
<Button onClick={() => append({ email: "" })}>Adicionar Convite</Button> </Form> );}Validação com Zod
Seção intitulada “Validação com Zod”Schema Simples
Seção intitulada “Schema Simples”import { z } from "zod";
const ProfileSchema = z.object({ name: z.string().min(1, "Obrigatório").max(100, "Máximo 100 caracteres"), email: z.email("E-mail inválido"), age: z.number().min(18, "Deve ser maior de idade").optional(),});
type ProfileForm = z.infer<typeof ProfileSchema>;Custom Validators
Seção intitulada “Custom Validators”import { isValidPhoneNumber } from "libphonenumber-js";
const ContactSchema = z.object({ whatsapp: z.custom<string>( (value) => isValidPhoneNumber((value as string) || ""), { error: "Número de WhatsApp inválido" }, ),});Schema Composition
Seção intitulada “Schema Composition”Use .safeExtend() para estender schemas existentes:
import { UserSchema } from "@/services/user/types";
export const UserFormSchema = UserSchema.safeExtend({ confirmPassword: z.string(), terms: z.boolean().refine((v) => v, "Aceite os termos"),});Validação Condicional
Seção intitulada “Validação Condicional”const PaymentSchema = z .object({ method: z.enum(["pix", "card"]), pixKey: z.string().optional(), cardNumber: z.string().optional(), }) .refine( (data) => { if (data.method === "pix") return !!data.pixKey; if (data.method === "card") return !!data.cardNumber; return true; }, { message: "Preencha os dados do pagamento", path: ["method"] }, );Padrões Avançados
Seção intitulada “Padrões Avançados”withController HOC
Seção intitulada “withController HOC”O HOC withController encapsula fields com Controller automaticamente.
// @/components/fields/utils/with-controller.tsximport { withController } from "@/components/fields/utils/with-controller";
export const MyCustomField = withController<MyFieldProps>( ({ value, onChange, disabled, ...props }) => { return ( <MyComponent value={value} onChange={(newValue) => onChange(newValue)} disabled={disabled} {...props} /> ); },);
// Uso<MyCustomField name="custom" control={form.control} label="Custom" />;ListField (Alternativa ao useFieldArray)
Seção intitulada “ListField (Alternativa ao useFieldArray)”O componente ListField é uma alternativa declarativa ao useFieldArray.
// @/components/fields/list.tsximport { ListField } from "@/components/fields/list";
<ListField<{ text: string }> name="messages" label="Mensagens" control={form.control} onItemData={() => ({ text: "" })} onRenderItem={(item, index, control) => ( <InputField name={`messages.${index}.text`} control={control} placeholder="Digite a mensagem..." /> )} minItems={1} maxItems={5} addButtonPosition="bottom"/>;Form State Management
Seção intitulada “Form State Management”// Verificar se form foi modificadoconst canSubmit = form.formState.isDirty;
// Loading stateconst isSubmitting = form.formState.isSubmitting;
// Reset após submitconst onSubmit = async (data) => { await saveData(data); form.reset(data);};setValue com shouldDirty
Seção intitulada “setValue com shouldDirty”Ao usar setValue programaticamente, sempre passe shouldDirty:
// ✅ Corretoform.setValue("field", newValue, { shouldDirty: true });
// ❌ Errado - não marca form como modificadoform.setValue("field", newValue);Anti-patterns
Seção intitulada “Anti-patterns”Usar form.watch() no Render
Seção intitulada “Usar form.watch() no Render”// ❌ ERRADO - causa re-renders desnecessáriosconst value = form.watch("field");
// ✅ CORRETO - re-render apenas quando 'field' mudaconst value = useWatch({ control: form.control, name: "field" });Esquecer defaultValues
Seção intitulada “Esquecer defaultValues”// ❌ ERRADO - valores undefined podem causar problemasconst form = useForm({ schema: MySchema });
// ✅ CORRETOconst form = useForm({ schema: MySchema, defaultValues: { name: "", items: [] },});Validar Manualmente
Seção intitulada “Validar Manualmente”// ❌ ERRADO - validação manual duplicadaconst onSubmit = (data) => { if (!data.email.includes("@")) { alert("E-mail inválido"); return; }};
// ✅ CORRETO - validação no schemaconst Schema = z.object({ email: z.email("E-mail inválido"),});Key com Index em Arrays Dinâmicos
Seção intitulada “Key com Index em Arrays Dinâmicos”// ❌ ERRADO - pode causar bugs de state{fields.map((field, index) => ( <div key={index}>...</div>))}
// ✅ CORRETO - usar field.id{fields.map((field, index) => ( <div key={field.id}>...</div>))}Referências
Seção intitulada “Referências”- react-hook-form: https://react-hook-form.com
- Zod: https://zod.dev