<h2>Entendendo o Problema: Estado e Validação em Formulários</h2>
<p>Quando começamos a trabalhar com formulários em aplicações React, logo percebemos que gerenciar estado e validação se torna uma tarefa repetitiva e propensa a erros. Cada campo de um formulário precisa rastrear seu valor, validar conforme regras de negócio, exibir mensagens de erro e manter um estado de "foi tocado" para não mostrar erros prematuramente. Multiplicar isso por dez campos diferentes em vários formulários resulta em código duplicado e difícil de manter.</p>
<p>A solução tradicional — usando <code>useState</code> diretamente para cada campo — funciona, mas gera verbosidade desnecessária. É aqui que entram os Hooks customizados: abstrações reutilizáveis que encapsulam a lógica complexa de um campo de formulário em uma interface simples e testável. Um Hook customizado bem construído não apenas reduz linhas de código, mas também padroniza comportamentos e torna a lógica de validação independente da renderização visual.</p>
<h2>Construindo um Hook Customizado para Campos de Formulário</h2>
<h3>A Estrutura Básica</h3>
<p>Um Hook para campo de formulário deve gerenciar pelo menos quatro responsabilidades: o valor atual do campo, seu estado de validação, se foi tocado pelo usuário e métodos para alterar esses estados. Vamos começar com uma implementação simples e incremental.</p>
<pre><code class="language-javascript">import { useState, useCallback } from 'react';
export const useField = (initialValue = '', validator = null) => {
const [value, setValue] = useState(initialValue);
const [touched, setTouched] = useState(false);
const [error, setError] = useState('');
const validate = useCallback(() => {
if (!validator) {
setError('');
return true;
}
const validationResult = validator(value);
const isValid = validationResult === true;
setError(isValid ? '' : validationResult);
return isValid;
}, [value, validator]);
const handleChange = useCallback((e) => {
const newValue = e.target.value;
setValue(newValue);
}, []);
const handleBlur = useCallback(() => {
setTouched(true);
validate();
}, [validate]);
const reset = useCallback(() => {
setValue(initialValue);
setTouched(false);
setError('');
}, [initialValue]);
return {
value,
setValue,
error,
touched,
handleChange,
handleBlur,
validate,
reset,
bind: {
value,
onChange: handleChange,
onBlur: handleBlur,
},
};
};</code></pre>
<p>Este Hook retorna tanto métodos individuais quanto um objeto <code>bind</code> que pode ser distribuído diretamente com spread em um input. O parâmetro <code>validator</code> é uma função que recebe o valor e retorna <code>true</code> se válido ou uma string com a mensagem de erro. Dessa forma, a lógica de validação é completamente desacoplada do Hook.</p>
<h3>Usando o Hook em um Componente</h3>
<p>A beleza dessa abstração é vista imediatamente ao usar em um componente. Veja como fica simples:</p>
<pre><code class="language-javascript">import React from 'react';
import { useField } from './useField';
const emailValidator = (value) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value) ? true : 'Email inválido';
};
const passwordValidator = (value) => {
if (value.length < 8) return 'Senha deve ter no mínimo 8 caracteres';
if (!/[A-Z]/.test(value)) return 'Senha deve conter letra maiúscula';
if (!/[0-9]/.test(value)) return 'Senha deve conter número';
return true;
};
export const LoginForm = () => {
const email = useField('', emailValidator);
const password = useField('', passwordValidator);
const handleSubmit = (e) => {
e.preventDefault();
const emailValid = email.validate();
const passwordValid = password.validate();
if (emailValid && passwordValid) {
console.log('Formulário válido:', {
email: email.value,
password: password.value,
});
// Enviar para API
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
{...email.bind}
placeholder="seu@email.com"
/>
{email.touched && email.error && (
<span className="error">{email.error}</span>
)}
</div>
<div>
<label htmlFor="password">Senha:</label>
<input
id="password"
type="password"
{...password.bind}
placeholder="Sua senha"
/>
{password.touched && password.error && (
<span className="error">{password.error}</span>
)}
</div>
<button type="submit">Entrar</button>
</form>
);
};</code></pre>
<p>Perceba que em apenas três linhas por campo (inicialização do Hook, renderização do input com spread e exibição condicional de erro), temos toda a funcionalidade. O mesmo Hook pode ser reutilizado em qualquer outro formulário com validadores diferentes.</p>
<h2>Evoluindo: Um Hook para Formulários Completos</h2>
<h3>Gerenciando Múltiplos Campos</h3>
<p>Para formulários mais complexos, é interessante ter um Hook que gerencie múltiplos campos simultaneamente. Isso permite validar o formulário inteiro, verificar se algum campo foi alterado e resetar todos os campos de uma vez.</p>
<pre><code class="language-javascript">import { useCallback, useState } from 'react';
export const useForm = (initialValues, onSubmit, validators = {}) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = useCallback((name, value) => {
const validator = validators[name];
if (!validator) return '';
const result = validator(value);
return result === true ? '' : result;
}, [validators]);
const validateAllFields = useCallback(() => {
const newErrors = {};
let isValid = true;
Object.keys(initialValues).forEach((fieldName) => {
const error = validateField(fieldName, values[fieldName]);
if (error) {
newErrors[fieldName] = error;
isValid = false;
}
});
setErrors(newErrors);
return isValid;
}, [initialValues, values, validateField]);
const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
const fieldValue = type === 'checkbox' ? checked : value;
setValues((prev) => ({
...prev,
[name]: fieldValue,
}));
// Validação em tempo real apenas se o campo foi tocado
if (touched[name]) {
const error = validateField(name, fieldValue);
setErrors((prev) => ({
...prev,
[name]: error,
}));
}
}, [touched, validateField]);
const handleBlur = useCallback((e) => {
const { name } = e.target;
setTouched((prev) => ({
...prev,
[name]: true,
}));
const error = validateField(name, values[name]);
setErrors((prev) => ({
...prev,
[name]: error,
}));
}, [values, validateField]);
const handleSubmit = useCallback(
async (e) => {
e.preventDefault();
setTouched(
Object.keys(initialValues).reduce((acc, field) => {
acc[field] = true;
return acc;
}, {})
);
if (validateAllFields()) {
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
}
},
[initialValues, validateAllFields, values, onSubmit]
);
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
resetForm,
setFieldValue: (name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));
},
};
};</code></pre>
<h3>Aplicando em um Formulário Realista</h3>
<p>Agora temos um Hook poderoso que gerencia o estado completo do formulário. Veja como fica um formulário de cadastro:</p>
<pre><code class="language-javascript">import React from 'react';
import { useForm } from './useForm';
const signupValidators = {
name: (value) => {
if (!value.trim()) return 'Nome é obrigatório';
if (value.length < 3) return 'Nome deve ter pelo menos 3 caracteres';
return true;
},
email: (value) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value) ? true : 'Email inválido';
},
phone: (value) => {
const regex = /^\(\d{2}\)\s\d{4,5}-\d{4}$/;
return regex.test(value) ? true : 'Telefone deve estar no formato (XX) XXXXX-XXXX';
},
password: (value) => {
if (value.length < 8) return 'Senha deve ter no mínimo 8 caracteres';
if (!/[A-Z]/.test(value)) return 'Senha deve conter letra maiúscula';
if (!/[0-9]/.test(value)) return 'Senha deve conter número';
if (!/[!@#$%^&*]/.test(value)) return 'Senha deve conter caractere especial';
return true;
},
terms: (value) => {
return value === true ? true : 'Você deve aceitar os termos de serviço';
},
};
export const SignupForm = () => {
const form = useForm(
{
name: '',
email: '',
phone: '',
password: '',
terms: false,
},
async (values) => {
// Simular chamada à API
console.log('Enviando dados:', values);
// const response = await fetch('/api/signup', { method: 'POST', body: JSON.stringify(values) });
},
signupValidators
);
return (
<form onSubmit={form.handleSubmit}>
<div className="field-group">
<label htmlFor="name">Nome Completo:</label>
<input
id="name"
name="name"
type="text"
value={form.values.name}
onChange={form.handleChange}
onBlur={form.handleBlur}
aria-invalid={form.touched.name && !!form.errors.name}
/>
{form.touched.name && form.errors.name && (
<span className="error">{form.errors.name}</span>
)}
</div>
<div className="field-group">
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={form.values.email}
onChange={form.handleChange}
onBlur={form.handleBlur}
aria-invalid={form.touched.email && !!form.errors.email}
/>
{form.touched.email && form.errors.email && (
<span className="error">{form.errors.email}</span>
)}
</div>
<div className="field-group">
<label htmlFor="phone">Telefone:</label>
<input
id="phone"
name="phone"
type="tel"
value={form.values.phone}
onChange={form.handleChange}
onBlur={form.handleBlur}
aria-invalid={form.touched.phone && !!form.errors.phone}
/>
{form.touched.phone && form.errors.phone && (
<span className="error">{form.errors.phone}</span>
)}
</div>
<div className="field-group">
<label htmlFor="password">Senha:</label>
<input
id="password"
name="password"
type="password"
value={form.values.password}
onChange={form.handleChange}
onBlur={form.handleBlur}
aria-invalid={form.touched.password && !!form.errors.password}
/>
{form.touched.password && form.errors.password && (
<span className="error">{form.errors.password}</span>
)}
</div>
<div className="field-group checkbox">
<label htmlFor="terms">
<input
id="terms"
name="terms"
type="checkbox"
checked={form.values.terms}
onChange={form.handleChange}
onBlur={form.handleBlur}
/>
Aceito os termos de serviço
</label>
{form.touched.terms && form.errors.terms && (
<span className="error">{form.errors.terms}</span>
)}
</div>
<button type="submit" disabled={form.isSubmitting}>
{form.isSubmitting ? 'Cadastrando...' : 'Criar Conta'}
</button>
<button type="reset" onClick={form.resetForm}>
Limpar
</button>
</form>
);
};</code></pre>
<h2>Padrões Avançados e Otimizações</h2>
<h3>Validação Assíncrona</h3>
<p>Em muitos casos, você precisa validar dados no servidor — por exemplo, verificar se um email já existe. O Hook <code>useForm</code> pode ser estendido para suportar validadores assíncronos com debounce:</p>
<pre><code class="language-javascript">import { useCallback, useState, useRef } from 'react';
export const useFormWithAsync = (initialValues, onSubmit, validators = {}) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isValidating, setIsValidating] = useState(false);
const debounceTimers = useRef({});
const validateField = useCallback(async (name, value) => {
const validator = validators[name];
if (!validator) return '';
setIsValidating(true);
try {
const result = await Promise.resolve(validator(value));
return result === true ? '' : result;
} finally {
setIsValidating(false);
}
}, [validators]);
const handleChange = useCallback(
(e) => {
const { name, value, type, checked } = e.target;
const fieldValue = type === 'checkbox' ? checked : value;
setValues((prev) => ({
...prev,
[name]: fieldValue,
}));
// Debounce validação assíncrona
if (touched[name]) {
clearTimeout(debounceTimers.current[name]);
debounceTimers.current[name] = setTimeout(async () => {
const error = await validateField(name, fieldValue);
setErrors((prev) => ({
...prev,
[name]: error,
}));
}, 500);
}
},
[touched, validateField]
);
const handleBlur = useCallback(
async (e) => {
const { name } = e.target;
setTouched((prev) => ({
...prev,
[name]: true,
}));
const error = await validateField(name, values[name]);
setErrors((prev) => ({
...prev,
[name]: error,
}));
},
[values, validateField]
);
const handleSubmit = useCallback(
async (e) => {
e.preventDefault();
// Aguardar todas as validações
const allTouched = Object.keys(initialValues).reduce((acc, field) => {
acc[field] = true;
return acc;
}, {});
setTouched(allTouched);
setIsSubmitting(true);
try {
const newErrors = {};
for (const fieldName of Object.keys(initialValues)) {
const error = await validateField(fieldName, values[fieldName]);
if (error) newErrors[fieldName] = error;
}
if (Object.keys(newErrors).length === 0) {
await onSubmit(values);
} else {
setErrors(newErrors);
}
} finally {
setIsSubmitting(false);
}
},
[initialValues, validateField, values, onSubmit]
);
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
Object.values(debounceTimers.current).forEach(clearTimeout);
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
isValidating,
handleChange,
handleBlur,
handleSubmit,
resetForm,
setFieldValue: (name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));
},
};
};</code></pre>
<p>Aqui usamos <code>debounceTimers.current</code> para armazenar timeouts de validação e evitar múltiplas chamadas enquanto o usuário está digitando. A validação só ocorre 500ms após a última mudança.</p>
<h3>Exemplo de Validador Assíncrono</h3>
<pre><code class="language-javascript">// Simular uma chamada à API
const checkEmailExists = async (email) => {
const response = await fetch(/api/check-email?email=${email});
const { exists } = await response.json();
return exists ? 'Este email já está registrado' : true;
};
const advancedValidators = {
email: async (value) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(value)) return 'Email inválido';
return await checkEmailExists(value);
},
};</code></pre>
<h2>Conclusão</h2>
<p>Ao dominar Hooks customizados para formulários, você ganha três vantagens fundamentais. Primeira: <strong>eliminação de código duplicado</strong> — a lógica complexa fica encapsulada e reutilizável entre projetos. Segunda: <strong>separação de responsabilidades</strong> — a validação é independente da renderização, facilitando testes unitários e manutenção. Terceira: <strong>escalabilidade</strong> — seus Hooks evoluem para suportar casos complexos como validação assíncrona, campos dependentes e integrações com APIs sem quebrar componentes que já os usam.</p>
<p>O investimento inicial em construir Hooks bem estruturados se paga rapidamente quando você precisa manter cinco formulários em vez de um, e todas as suas validações estão centralizadas, testáveis e compreensíveis.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react.dev/reference/react/hooks" target="_blank" rel="noopener noreferrer">React Hooks - Documentação Oficial</a></li>
<li><a href="https://react.dev/learn/reusing-logic-with-custom-hooks" target="_blank" rel="noopener noreferrer">Building Your Own Hooks - React Documentation</a></li>
<li><a href="https://overreacted.io/a-complete-guide-to-useeffect/" target="_blank" rel="noopener noreferrer">A Complete Guide to useEffect - Dan Abramov</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation" target="_blank" rel="noopener noreferrer">Form Validation in React - MDN Web Docs</a></li>
<li><a href="https://react-hook-form.com/" target="_blank" rel="noopener noreferrer">React Hook Form - Open Source Library</a></li>
</ul>
<p><!-- FIM --></p>