React & Frontend

O que Todo Dev Deve Saber sobre Hooks para Formulários: Abstraindo Validação e Estado de Campos

15 min de leitura

O que Todo Dev Deve Saber sobre Hooks para Formulários: Abstraindo Validação e Estado de Campos

Entendendo o Problema: Estado e Validação em Formulários 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. A solução tradicional — usando 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. Construindo um Hook Customizado para Campos de Formulário A Estrutura Básica Um Hook para campo

<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 &quot;foi tocado&quot; 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 &#039;react&#039;;

export const useField = (initialValue = &#039;&#039;, validator = null) =&gt; {

const [value, setValue] = useState(initialValue);

const [touched, setTouched] = useState(false);

const [error, setError] = useState(&#039;&#039;);

const validate = useCallback(() =&gt; {

if (!validator) {

setError(&#039;&#039;);

return true;

}

const validationResult = validator(value);

const isValid = validationResult === true;

setError(isValid ? &#039;&#039; : validationResult);

return isValid;

}, [value, validator]);

const handleChange = useCallback((e) =&gt; {

const newValue = e.target.value;

setValue(newValue);

}, []);

const handleBlur = useCallback(() =&gt; {

setTouched(true);

validate();

}, [validate]);

const reset = useCallback(() =&gt; {

setValue(initialValue);

setTouched(false);

setError(&#039;&#039;);

}, [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 &#039;react&#039;;

import { useField } from &#039;./useField&#039;;

const emailValidator = (value) =&gt; {

const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

return regex.test(value) ? true : &#039;Email inválido&#039;;

};

const passwordValidator = (value) =&gt; {

if (value.length &lt; 8) return &#039;Senha deve ter no mínimo 8 caracteres&#039;;

if (!/[A-Z]/.test(value)) return &#039;Senha deve conter letra maiúscula&#039;;

if (!/[0-9]/.test(value)) return &#039;Senha deve conter número&#039;;

return true;

};

export const LoginForm = () =&gt; {

const email = useField(&#039;&#039;, emailValidator);

const password = useField(&#039;&#039;, passwordValidator);

const handleSubmit = (e) =&gt; {

e.preventDefault();

const emailValid = email.validate();

const passwordValid = password.validate();

if (emailValid &amp;&amp; passwordValid) {

console.log(&#039;Formulário válido:&#039;, {

email: email.value,

password: password.value,

});

// Enviar para API

}

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;div&gt;

&lt;label htmlFor=&quot;email&quot;&gt;Email:&lt;/label&gt;

&lt;input

id=&quot;email&quot;

type=&quot;email&quot;

{...email.bind}

placeholder=&quot;seu@email.com&quot;

/&gt;

{email.touched &amp;&amp; email.error &amp;&amp; (

&lt;span className=&quot;error&quot;&gt;{email.error}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div&gt;

&lt;label htmlFor=&quot;password&quot;&gt;Senha:&lt;/label&gt;

&lt;input

id=&quot;password&quot;

type=&quot;password&quot;

{...password.bind}

placeholder=&quot;Sua senha&quot;

/&gt;

{password.touched &amp;&amp; password.error &amp;&amp; (

&lt;span className=&quot;error&quot;&gt;{password.error}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;button type=&quot;submit&quot;&gt;Entrar&lt;/button&gt;

&lt;/form&gt;

);

};</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 &#039;react&#039;;

export const useForm = (initialValues, onSubmit, validators = {}) =&gt; {

const [values, setValues] = useState(initialValues);

const [errors, setErrors] = useState({});

const [touched, setTouched] = useState({});

const [isSubmitting, setIsSubmitting] = useState(false);

const validateField = useCallback((name, value) =&gt; {

const validator = validators[name];

if (!validator) return &#039;&#039;;

const result = validator(value);

return result === true ? &#039;&#039; : result;

}, [validators]);

const validateAllFields = useCallback(() =&gt; {

const newErrors = {};

let isValid = true;

Object.keys(initialValues).forEach((fieldName) =&gt; {

const error = validateField(fieldName, values[fieldName]);

if (error) {

newErrors[fieldName] = error;

isValid = false;

}

});

setErrors(newErrors);

return isValid;

}, [initialValues, values, validateField]);

const handleChange = useCallback((e) =&gt; {

const { name, value, type, checked } = e.target;

const fieldValue = type === &#039;checkbox&#039; ? checked : value;

setValues((prev) =&gt; ({

...prev,

[name]: fieldValue,

}));

// Validação em tempo real apenas se o campo foi tocado

if (touched[name]) {

const error = validateField(name, fieldValue);

setErrors((prev) =&gt; ({

...prev,

[name]: error,

}));

}

}, [touched, validateField]);

const handleBlur = useCallback((e) =&gt; {

const { name } = e.target;

setTouched((prev) =&gt; ({

...prev,

[name]: true,

}));

const error = validateField(name, values[name]);

setErrors((prev) =&gt; ({

...prev,

[name]: error,

}));

}, [values, validateField]);

const handleSubmit = useCallback(

async (e) =&gt; {

e.preventDefault();

setTouched(

Object.keys(initialValues).reduce((acc, field) =&gt; {

acc[field] = true;

return acc;

}, {})

);

if (validateAllFields()) {

setIsSubmitting(true);

try {

await onSubmit(values);

} finally {

setIsSubmitting(false);

}

}

},

[initialValues, validateAllFields, values, onSubmit]

);

const resetForm = useCallback(() =&gt; {

setValues(initialValues);

setErrors({});

setTouched({});

}, [initialValues]);

return {

values,

errors,

touched,

isSubmitting,

handleChange,

handleBlur,

handleSubmit,

resetForm,

setFieldValue: (name, value) =&gt; {

setValues((prev) =&gt; ({ ...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 &#039;react&#039;;

import { useForm } from &#039;./useForm&#039;;

const signupValidators = {

name: (value) =&gt; {

if (!value.trim()) return &#039;Nome é obrigatório&#039;;

if (value.length &lt; 3) return &#039;Nome deve ter pelo menos 3 caracteres&#039;;

return true;

},

email: (value) =&gt; {

const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

return regex.test(value) ? true : &#039;Email inválido&#039;;

},

phone: (value) =&gt; {

const regex = /^\(\d{2}\)\s\d{4,5}-\d{4}$/;

return regex.test(value) ? true : &#039;Telefone deve estar no formato (XX) XXXXX-XXXX&#039;;

},

password: (value) =&gt; {

if (value.length &lt; 8) return &#039;Senha deve ter no mínimo 8 caracteres&#039;;

if (!/[A-Z]/.test(value)) return &#039;Senha deve conter letra maiúscula&#039;;

if (!/[0-9]/.test(value)) return &#039;Senha deve conter número&#039;;

if (!/[!@#$%^&amp;*]/.test(value)) return &#039;Senha deve conter caractere especial&#039;;

return true;

},

terms: (value) =&gt; {

return value === true ? true : &#039;Você deve aceitar os termos de serviço&#039;;

},

};

export const SignupForm = () =&gt; {

const form = useForm(

{

name: &#039;&#039;,

email: &#039;&#039;,

phone: &#039;&#039;,

password: &#039;&#039;,

terms: false,

},

async (values) =&gt; {

// Simular chamada à API

console.log(&#039;Enviando dados:&#039;, values);

// const response = await fetch(&#039;/api/signup&#039;, { method: &#039;POST&#039;, body: JSON.stringify(values) });

},

signupValidators

);

return (

&lt;form onSubmit={form.handleSubmit}&gt;

&lt;div className=&quot;field-group&quot;&gt;

&lt;label htmlFor=&quot;name&quot;&gt;Nome Completo:&lt;/label&gt;

&lt;input

id=&quot;name&quot;

name=&quot;name&quot;

type=&quot;text&quot;

value={form.values.name}

onChange={form.handleChange}

onBlur={form.handleBlur}

aria-invalid={form.touched.name &amp;&amp; !!form.errors.name}

/&gt;

{form.touched.name &amp;&amp; form.errors.name &amp;&amp; (

&lt;span className=&quot;error&quot;&gt;{form.errors.name}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;field-group&quot;&gt;

&lt;label htmlFor=&quot;email&quot;&gt;Email:&lt;/label&gt;

&lt;input

id=&quot;email&quot;

name=&quot;email&quot;

type=&quot;email&quot;

value={form.values.email}

onChange={form.handleChange}

onBlur={form.handleBlur}

aria-invalid={form.touched.email &amp;&amp; !!form.errors.email}

/&gt;

{form.touched.email &amp;&amp; form.errors.email &amp;&amp; (

&lt;span className=&quot;error&quot;&gt;{form.errors.email}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;field-group&quot;&gt;

&lt;label htmlFor=&quot;phone&quot;&gt;Telefone:&lt;/label&gt;

&lt;input

id=&quot;phone&quot;

name=&quot;phone&quot;

type=&quot;tel&quot;

value={form.values.phone}

onChange={form.handleChange}

onBlur={form.handleBlur}

aria-invalid={form.touched.phone &amp;&amp; !!form.errors.phone}

/&gt;

{form.touched.phone &amp;&amp; form.errors.phone &amp;&amp; (

&lt;span className=&quot;error&quot;&gt;{form.errors.phone}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;field-group&quot;&gt;

&lt;label htmlFor=&quot;password&quot;&gt;Senha:&lt;/label&gt;

&lt;input

id=&quot;password&quot;

name=&quot;password&quot;

type=&quot;password&quot;

value={form.values.password}

onChange={form.handleChange}

onBlur={form.handleBlur}

aria-invalid={form.touched.password &amp;&amp; !!form.errors.password}

/&gt;

{form.touched.password &amp;&amp; form.errors.password &amp;&amp; (

&lt;span className=&quot;error&quot;&gt;{form.errors.password}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;field-group checkbox&quot;&gt;

&lt;label htmlFor=&quot;terms&quot;&gt;

&lt;input

id=&quot;terms&quot;

name=&quot;terms&quot;

type=&quot;checkbox&quot;

checked={form.values.terms}

onChange={form.handleChange}

onBlur={form.handleBlur}

/&gt;

Aceito os termos de serviço

&lt;/label&gt;

{form.touched.terms &amp;&amp; form.errors.terms &amp;&amp; (

&lt;span className=&quot;error&quot;&gt;{form.errors.terms}&lt;/span&gt;

)}

&lt;/div&gt;

&lt;button type=&quot;submit&quot; disabled={form.isSubmitting}&gt;

{form.isSubmitting ? &#039;Cadastrando...&#039; : &#039;Criar Conta&#039;}

&lt;/button&gt;

&lt;button type=&quot;reset&quot; onClick={form.resetForm}&gt;

Limpar

&lt;/button&gt;

&lt;/form&gt;

);

};</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 &#039;react&#039;;

export const useFormWithAsync = (initialValues, onSubmit, validators = {}) =&gt; {

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) =&gt; {

const validator = validators[name];

if (!validator) return &#039;&#039;;

setIsValidating(true);

try {

const result = await Promise.resolve(validator(value));

return result === true ? &#039;&#039; : result;

} finally {

setIsValidating(false);

}

}, [validators]);

const handleChange = useCallback(

(e) =&gt; {

const { name, value, type, checked } = e.target;

const fieldValue = type === &#039;checkbox&#039; ? checked : value;

setValues((prev) =&gt; ({

...prev,

[name]: fieldValue,

}));

// Debounce validação assíncrona

if (touched[name]) {

clearTimeout(debounceTimers.current[name]);

debounceTimers.current[name] = setTimeout(async () =&gt; {

const error = await validateField(name, fieldValue);

setErrors((prev) =&gt; ({

...prev,

[name]: error,

}));

}, 500);

}

},

[touched, validateField]

);

const handleBlur = useCallback(

async (e) =&gt; {

const { name } = e.target;

setTouched((prev) =&gt; ({

...prev,

[name]: true,

}));

const error = await validateField(name, values[name]);

setErrors((prev) =&gt; ({

...prev,

[name]: error,

}));

},

[values, validateField]

);

const handleSubmit = useCallback(

async (e) =&gt; {

e.preventDefault();

// Aguardar todas as validações

const allTouched = Object.keys(initialValues).reduce((acc, field) =&gt; {

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(() =&gt; {

setValues(initialValues);

setErrors({});

setTouched({});

Object.values(debounceTimers.current).forEach(clearTimeout);

}, [initialValues]);

return {

values,

errors,

touched,

isSubmitting,

isValidating,

handleChange,

handleBlur,

handleSubmit,

resetForm,

setFieldValue: (name, value) =&gt; {

setValues((prev) =&gt; ({ ...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) =&gt; {

const response = await fetch(/api/check-email?email=${email});

const { exists } = await response.json();

return exists ? &#039;Este email já está registrado&#039; : true;

};

const advancedValidators = {

email: async (value) =&gt; {

const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!regex.test(value)) return &#039;Email inválido&#039;;

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática
useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática

O Problema Real do useContext em Aplicações Escaláveis Quando iniciamos com R...

Guia Completo de useSyncExternalStore: Integrando Stores Externas com React
Guia Completo de useSyncExternalStore: Integrando Stores Externas com React

O Problema: Estado Externo e React Quando trabalhamos com React, frequentemen...

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado
React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases React Fib...