<h2>Entendendo o Problema: Por Que React Hook Form Avançado?</h2>
<p>Quando começamos a trabalhar com formulários em React, muitas vezes enfrentamos situações onde um simples <code>useState</code> ou até mesmo bibliotecas básicas se tornam insuficientes. React Hook Form é uma biblioteca que resolve este problema de forma elegante, mas suas capacidades vão muito além de formulários simples. Trabalhar com arrays dinâmicos, campos aninhados e fluxos de múltiplas etapas (wizards) requer uma compreensão profunda de como a biblioteca gerencia estado e validação.</p>
<p>A razão pela qual este tópico é crucial está no fato de que muitos aplicativos reais precisam de formulários sofisticados: cadastros de múltiplos endereços, listas de produtos com variações, ou processos de inscrição em etapas. Se você tentar implementar isso sem entender os conceitos avançados da biblioteca, acabará com código desorganizado, difícil de manter e propenso a bugs. Vamos resolver isso de forma prática e estruturada.</p>
<h2>Trabalhando com Arrays e Campos Dinâmicos</h2>
<h3>O Hook <code>useFieldArray</code>: Sua Ferramenta Principal</h3>
<p>O <code>useFieldArray</code> é o coração para trabalhar com listas de campos no React Hook Form. Ele permite adicionar, remover e manipular campos de forma reativa, mantendo o estado sincronizado com o formulário. Diferentemente de gerenciar um array com <code>useState</code>, o <code>useFieldArray</code> integra-se perfeitamente com a validação e o sistema de erros da biblioteca.</p>
<p>Vejamos um exemplo prático: um formulário onde o usuário pode adicionar múltiplos números de telefone. Começamos configurando o <code>useForm</code> com um valor padrão que inclui um array:</p>
<pre><code class="language-jsx">import React from 'react';
import { useForm, useFieldArray, Controller } from 'react-hook-form';
export function MultiPhoneForm() {
const { control, handleSubmit, formState: { errors } } = useForm({
defaultValues: {
phones: [
{ number: '', type: 'celular' }
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'phones'
});
const onSubmit = (data) => {
console.log('Dados do formulário:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<Controller
name={phones.${index}.number}
control={control}
rules={{
required: 'Telefone é obrigatório',
pattern: {
value: /^[0-9\-\(\) ]+$/,
message: 'Telefone inválido'
}
}}
render={({ field }) => (
<input
{...field}
placeholder="(11) 99999-9999"
type="tel"
/>
)}
/>
{errors.phones?.[index]?.number && (
<span>{errors.phones[index].number.message}</span>
)}
<Controller
name={phones.${index}.type}
control={control}
render={({ field }) => (
<select {...field}>
<option value="celular">Celular</option>
<option value="residencial">Residencial</option>
<option value="comercial">Comercial</option>
</select>
)}
/>
{fields.length > 1 && (
<button type="button" onClick={() => remove(index)}>
Remover
</button>
)}
</div>
))}
<button
type="button"
onClick={() => append({ number: '', type: 'celular' })}
>
Adicionar Telefone
</button>
<button type="submit">Enviar</button>
</form>
);
}</code></pre>
<h3>Validação e Comportamento Avançado</h3>
<p>Um detalhe importante que muitos iniciantes ignoram: quando você remove um campo com <code>remove()</code>, o React Hook Form automaticamente reorganiza os índices. Isso significa que você não precisa se preocupar com buracos no array ou IDs desalinhados. Porém, é crucial usar a propriedade <code>id</code> do <code>field</code> como chave do React, não o índice, para evitar problemas de re-render.</p>
<p>Quando precisamos validar o array como um todo (por exemplo, garantir que pelo menos um telefone existe), fazemos isso ao nível do formulário, não do campo individual:</p>
<pre><code class="language-jsx">const { control, handleSubmit, formState: { errors } } = useForm({
defaultValues: {
phones: [{ number: '', type: 'celular' }]
},
resolver: async (data) => {
const errors = {};
if (!data.phones || data.phones.length === 0) {
errors.phones = {
message: 'Adicione pelo menos um telefone'
};
}
if (data.phones && data.phones.every(p => !p.number)) {
errors.phones = {
message: 'Adicione pelo menos um número válido'
};
}
return { values: data, errors };
}
});</code></pre>
<h2>Campos Aninhados e Estruturas Complexas</h2>
<h3>Construindo Estruturas Hierárquicas</h3>
<p>Quando seus formulários precisam de múltiplos níveis de aninhamento, a coisa fica mais interessante. Um exemplo clássico é um cadastro de usuário com múltiplos endereços, e cada endereço contém coordenadas GPS e histórico de mudanças. A estratégia aqui é usar <code>watch</code> para monitorar valores e o padrão de nomenclatura de pontos do React Hook Form.</p>
<pre><code class="language-jsx">import React from 'react';
import { useForm, useFieldArray, Controller, watch } from 'react-hook-form';
export function UserAddressForm() {
const { control, handleSubmit, watch: watchForm } = useForm({
defaultValues: {
user: {
name: '',
email: ''
},
addresses: [
{
street: '',
number: '',
complement: '',
geolocation: {
latitude: '',
longitude: ''
},
isDefault: false
}
]
}
});
const { fields: addressFields, append: appendAddress, remove: removeAddress } = useFieldArray({
control,
name: 'addresses'
});
const watchedAddresses = watchForm('addresses');
const onSubmit = (data) => {
console.log('Dados completos:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset>
<legend>Dados Pessoais</legend>
<Controller
name="user.name"
control={control}
rules={{ required: 'Nome é obrigatório' }}
render={({ field }) => (
<input {...field} placeholder="Nome completo" />
)}
/>
<Controller
name="user.email"
control={control}
rules={{
required: 'E-mail é obrigatório',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'E-mail inválido'
}
}}
render={({ field }) => (
<input {...field} type="email" placeholder="seu@email.com" />
)}
/>
</fieldset>
<fieldset>
<legend>Endereços</legend>
{addressFields.map((addressField, addressIndex) => (
<div key={addressField.id} style={{ border: '1px solid #ccc', padding: '1rem', marginBottom: '1rem' }}>
<h4>Endereço {addressIndex + 1}</h4>
<Controller
name={addresses.${addressIndex}.street}
control={control}
rules={{ required: 'Rua é obrigatória' }}
render={({ field }) => (
<input {...field} placeholder="Rua" />
)}
/>
<Controller
name={addresses.${addressIndex}.number}
control={control}
rules={{ required: 'Número é obrigatório' }}
render={({ field }) => (
<input {...field} placeholder="Número" type="text" />
)}
/>
<Controller
name={addresses.${addressIndex}.complement}
control={control}
render={({ field }) => (
<input {...field} placeholder="Complemento (opcional)" />
)}
/>
<fieldset>
<legend>Geolocalização</legend>
<Controller
name={addresses.${addressIndex}.geolocation.latitude}
control={control}
rules={{
pattern: {
value: /^-?\d+(\.\d+)?$/,
message: 'Latitude inválida'
}
}}
render={({ field }) => (
<input {...field} placeholder="Latitude" />
)}
/>
<Controller
name={addresses.${addressIndex}.geolocation.longitude}
control={control}
rules={{
pattern: {
value: /^-?\d+(\.\d+)?$/,
message: 'Longitude inválida'
}
}}
render={({ field }) => (
<input {...field} placeholder="Longitude" />
)}
/>
</fieldset>
<Controller
name={addresses.${addressIndex}.isDefault}
control={control}
render={({ field }) => (
<label>
<input {...field} type="checkbox" />
Endereço padrão
</label>
)}
/>
{addressFields.length > 1 && (
<button
type="button"
onClick={() => removeAddress(addressIndex)}
>
Remover endereço
</button>
)}
</div>
))}
<button
type="button"
onClick={() => appendAddress({
street: '',
number: '',
complement: '',
geolocation: { latitude: '', longitude: '' },
isDefault: false
})}
>
Adicionar Endereço
</button>
</fieldset>
<button type="submit">Enviar</button>
</form>
);
}</code></pre>
<h3>Validação Condicional em Estruturas Aninhadas</h3>
<p>A parte desafiadora aqui é validar campos baseado em outros campos do mesmo nível ou de níveis diferentes. Por exemplo: se <code>isDefault</code> é true, os campos de geolocalização devem ser obrigatórios. Para isso, usamos a função de resolver do Yup ou Zod, ou validação personalizada:</p>
<pre><code class="language-jsx">import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
const validationSchema = yup.object({
user: yup.object({
name: yup.string().required('Nome obrigatório'),
email: yup.string().email().required('E-mail obrigatório')
}),
addresses: yup.array().of(
yup.object({
street: yup.string().required('Rua obrigatória'),
number: yup.string().required('Número obrigatório'),
geolocation: yup.object().when('isDefault', {
is: true,
then: (schema) => schema.shape({
latitude: yup.string().required('Latitude obrigatória quando endereço é padrão'),
longitude: yup.string().required('Longitude obrigatória quando endereço é padrão')
}),
otherwise: (schema) => schema
})
})
)
});
export function UserAddressFormWithValidation() {
const { control, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(validationSchema),
defaultValues: {
user: { name: '', email: '' },
addresses: [{ street: '', number: '', complement: '', geolocation: { latitude: '', longitude: '' }, isDefault: false }]
}
});
const onSubmit = (data) => {
console.log('Dados validados:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/ Renderizar campos aqui /}
</form>
);
}</code></pre>
<h2>Implementando Wizards (Formulários em Múltiplas Etapas)</h2>
<h3>Arquitetura de um Wizard</h3>
<p>Um wizard é um formulário dividido em várias etapas, onde o usuário progride de uma para a próxima. A chave para implementar isso corretamente com React Hook Form é manter um único <code>useForm</code> para o formulário inteiro, mas controlar qual etapa está sendo exibida. Isso garante que o estado seja preservado enquanto o usuário navega entre as etapas.</p>
<pre><code class="language-jsx">import React, { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
export function RegistrationWizard() {
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 3;
const { control, handleSubmit, trigger, formState: { errors }, watch } = useForm({
mode: 'onBlur',
defaultValues: {
// Passo 1: Informações Pessoais
firstName: '',
lastName: '',
email: '',
// Passo 2: Endereço
street: '',
number: '',
city: '',
state: '',
zipCode: '',
// Passo 3: Confirmação
acceptTerms: false,
newsletter: false
}
});
const watchedValues = watch();
const handleNext = async () => {
const fieldsToValidate = getFieldsByStep(currentStep);
const isValid = await trigger(fieldsToValidate);
if (isValid) {
setCurrentStep((prev) => Math.min(prev + 1, totalSteps));
}
};
const handleBack = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1));
};
const getFieldsByStep = (step) => {
switch (step) {
case 1:
return ['firstName', 'lastName', 'email'];
case 2:
return ['street', 'number', 'city', 'state', 'zipCode'];
case 3:
return ['acceptTerms'];
default:
return [];
}
};
const onSubmit = (data) => {
console.log('Formulário completo:', data);
// Enviar dados para API
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div style={{ marginBottom: '2rem' }}>
<div style={{ display: 'flex', gap: '0.5rem' }}>
{[1, 2, 3].map((step) => (
<div
key={step}
style={{
width: '2rem',
height: '2rem',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: currentStep >= step ? '#007bff' : '#e9ecef',
color: currentStep >= step ? 'white' : '#000',
fontWeight: 'bold'
}}
>
{step}
</div>
))}
</div>
</div>
{currentStep === 1 && (
<fieldset>
<legend>Passo 1: Informações Pessoais</legend>
<div>
<label>Primeiro Nome</label>
<Controller
name="firstName"
control={control}
rules={{ required: 'Primeiro nome é obrigatório' }}
render={({ field }) => <input {...field} type="text" />}
/>
{errors.firstName && <span>{errors.firstName.message}</span>}
</div>
<div>
<label>Último Nome</label>
<Controller
name="lastName"
control={control}
rules={{ required: 'Último nome é obrigatório' }}
render={({ field }) => <input {...field} type="text" />}
/>
{errors.lastName && <span>{errors.lastName.message}</span>}
</div>
<div>
<label>E-mail</label>
<Controller
name="email"
control={control}
rules={{
required: 'E-mail é obrigatório',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'E-mail inválido'
}
}}
render={({ field }) => <input {...field} type="email" />}
/>
{errors.email && <span>{errors.email.message}</span>}
</div>
</fieldset>
)}
{currentStep === 2 && (
<fieldset>
<legend>Passo 2: Endereço</legend>
<div>
<label>Rua</label>
<Controller
name="street"
control={control}
rules={{ required: 'Rua é obrigatória' }}
render={({ field }) => <input {...field} type="text" />}
/>
{errors.street && <span>{errors.street.message}</span>}
</div>
<div>
<label>Número</label>
<Controller
name="number"
control={control}
rules={{ required: 'Número é obrigatório' }}
render={({ field }) => <input {...field} type="text" />}
/>
{errors.number && <span>{errors.number.message}</span>}
</div>
<div>
<label>Cidade</label>
<Controller
name="city"
control={control}
rules={{ required: 'Cidade é obrigatória' }}
render={({ field }) => <input {...field} type="text" />}
/>
{errors.city && <span>{errors.city.message}</span>}
</div>
<div>
<label>Estado</label>
<Controller
name="state"
control={control}
rules={{ required: 'Estado é obrigatório' }}
render={({ field }) => (
<select {...field}>
<option value="">Selecione</option>
<option value="SP">São Paulo</option>
<option value="RJ">Rio de Janeiro</option>
<option value="MG">Minas Gerais</option>
</select>
)}
/>
{errors.state && <span>{errors.state.message}</span>}
</div>
<div>
<label>CEP</label>
<Controller
name="zipCode"
control={control}
rules={{
required: 'CEP é obrigatório',
pattern: {
value: /^\d{5}-?\d{3}$/,
message: 'CEP inválido'
}
}}
render={({ field }) => <input {...field} type="text" />}
/>
{errors.zipCode && <span>{errors.zipCode.message}</span>}
</div>
</fieldset>
)}
{currentStep === 3 && (
<fieldset>
<legend>Passo 3: Confirmação</legend>
<div>
<h3>Revise seus dados:</h3>
<p><strong>Nome:</strong> {watchedValues.firstName} {watchedValues.lastName}</p>
<p><strong>E-mail:</strong> {watchedValues.email}</p>
<p><strong>Endereço:</strong> {watchedValues.street}, {watchedValues.number} - {watchedValues.city}, {watchedValues.state}</p>
</div>
<div>
<Controller
name="acceptTerms"
control={control}
rules={{ required: 'Você deve aceitar os termos' }}
render={({ field }) => (
<label>
<input {...field} type="checkbox" />
Eu aceito os termos e condições
</label>
)}
/>
{errors.acceptTerms && <span>{errors.acceptTerms.message}</span>}
</div>
<div>
<Controller
name="newsletter"
control={control}
render={({ field }) => (
<label>
<input {...field} type="checkbox" />
Receber notícias por e-mail
</label>
)}
/>
</div>
</fieldset>
)}
<div style={{ display: 'flex', gap: '1rem', marginTop: '2rem' }}>
{currentStep > 1 && (
<button type="button" onClick={handleBack}>
Voltar
</button>
)}
{currentStep < totalSteps && (
<button type="button" onClick={handleNext}>
Próximo
</button>
)}
{currentStep === totalSteps && (
<button type="submit">
Finalizar Registro
</button>
)}
</div>
</form>
);
}</code></pre>
<h3>Validação Contextual em Wizards</h3>
<p>Um aspecto crucial que muitos desenvolvedores esquecem é que em um wizard, a validação deve ser progressiva. Quando o usuário clica em "Próximo", você não valida apenas o passo atual, mas todos os passos anteriores também. Use <code>trigger()</code> com um array de campos específicos para validar somente o que é necessário naquele momento:</p>
<pre><code class="language-jsx">// Validar apenas os campos do passo atual antes de avançar
const handleNext = async () => {
const fieldsToValidate = getFieldsByStep(currentStep);
const isValid = await trigger(fieldsToValidate);
if (isValid) {
setCurrentStep((prev) => Math.min(prev + 1, totalSteps));
}
};
// Ao submeter no final, trigger() sem argumentos valida TUDO
const onSubmit = (data) => {
console.log('Dados validados completamente:', data);
};</code></pre>
<h2>Conclusão</h2>
<p>Nesta aula, você aprendeu três conceitos fundamentais que transformam sua capacidade de trabalhar com formulários complexos em React. Primeiro, o <code>useFieldArray</code> não é apenas uma forma de iterar sobre arrays, mas um sistema inteligente de sincronização entre o estado do formulário e a interface, eliminando a necessidade de gerenciar índices manualmente. Segundo, estruturas aninhadas exigem uma nomenclatura cuidadosa usando notação de ponto e uma estratégia clara de validação condicional, que você conquista com Yup, Zod ou resolvers customizados. Terceiro, wizards são implementados corretamente quando você mantém um único <code>useForm</code> para toda a jornada, validando seletivamente por etapa e preservando o estado do usuário enquanto ele navega, não deixando que o desespero de um passo 2 rejeitado apague os dados do passo 1.</p>
<p>A verdadeira maestria em React Hook Form avançado vem de entender que a biblioteca não apenas gerencia formulários, mas oferece primitivos poderosos para sincronizar estado complexo entre UI e validação. Use esses conhecimentos não apenas para resolver os problemas apresentados aqui, mas como fundação para casos ainda mais sofisticados que você encontrará em sua carreira.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react-hook-form.com/" target="_blank" rel="noopener noreferrer">React Hook Form - Documentação Oficial</a></li>
<li><a href="https://react-hook-form.com/api/usefieldarray" target="_blank" rel="noopener noreferrer">React Hook Form - useFieldArray API</a></li>
<li><a href="https://github.com/jquense/yup" target="_blank" rel="noopener noreferrer">Yup - Schema Validation</a></li>
<li><a href="https://react-hook-form.com/form-builder" target="_blank" rel="noopener noreferrer">React Hook Form com TypeScript - Exemplo Avançado</a></li>
<li><a href="https://www.smashingmagazine.com/2021/10/react-hook-form/" target="_blank" rel="noopener noreferrer">Building Complex Forms with React Hook Form - Article</a></li>
</ul>
<p><!-- FIM --></p>