Nesta semana, vi três desenvolvedores diferentes enfrentarem o mesmo problema: usar react-hook-form como se fosse apenas um useState
glorificado. A biblioteca é uma das formas mais eficientes de lidar com formulários em SPAs React — mas apenas se adotarmos seus padrões idiomáticos. Usá-la de forma incorreta pode transformar um formulário simples em um caos frágil e difícil de manter.
Vamos passar pelas falhas mais comuns que já observei e como resolvê-las.
Erro #1 — Ignorar register
e Controller
Alguns desenvolvedores evitam register
ou Controller
por completo. Em vez disso, fazem algo assim:
1const { watch, setValue } = useForm();
2
3<input
4 value={watch("email")}
5 onChange={(e) => setValue("email", e.target.value)}
6/>
Isso é basicamente reimplementar componentes controlados com useState
. Vai contra o propósito do react-hook-form, que foi projetado para manter os inputs não controlados e leves, reduzindo re-renders.
✅ O jeito idiomático:
1const { register } = useForm();
2
3<input type="email" {...register("email")} />
Ou, ao trabalhar com componentes mais complexos (como Select
ou DatePicker
):
1const { control } = useForm();
2
3<Controller
4 name="date"
5 control={control}
6 render={({ field }) => (
7 <DatePicker
8 ref={field.ref}
9 selected={field.value}
10 onChange={(date: Date, strDate: string) => field.onChange(strDate)}
11 />
12 )}
13/>
Erro #2 — Passar estado do formulário manualmente pela árvore
Outro anti-padrão comum: usar watch
no nível superior e passar props manualmente, ou até duplicar chamadas de register
em componentes filhos.
Isso torna os formulários verbosos e cheios de pontos de falha. Também gera mais re-renders desnecessários. Em vez disso, use FormContext:
1const methods = useForm();
2
3<FormProvider {...methods}>
4 <FormChild />
5</FormProvider>
E no componente filho:
1const { register } = useFormContext();
2
3<input {...register("username")} />
Assim, a lógica do formulário fica centralizada e cada componente pode acessar o mesmo estado sem prop drilling.
Erro #3 — Complicar o carregamento de valores iniciais
Muita gente tem dificuldade em pré-carregar dados no formulário. Já vi implementações com vários useEffect
apenas para popular os campos:
1useEffect(() => {
2 setValue("name", user.name);
3 setValue("email", user.email);
4}, [user]);
Embora funcione, é verboso e mistura responsabilidades. A abordagem mais idiomática é manter o formulário focado apenas em lidar com inputs, não com carregamento de dados ou estado de loading.
✅ Padrão mais limpo: crie um wrapper que trate dos dados assíncronos:
1// UserFormWrapper.tsx
2export function UserFormWrapper() {
3 const { data: user, isLoading } = useUserQuery();
4
5 if (isLoading) return <div>Carregando...</div>;
6 if (!user) return <div>Nenhum dado encontrado</div>;
7
8 return <UserForm initialData={user} />;
9}
No formulário em si, podemos usar os props para inicializar defaultValues
:
1// UserForm.tsx
2export function UserForm({ initialData }) {
3 const { register } = useForm({
4 defaultValues: {
5 name: initialData.name,
6 email: initialData.email,
7 },
8 });
9
10 return (
11 <form>
12 <input {...register("name")} />
13 <input {...register("email")} />
14 </form>
15 );
16}
Dessa forma:
- O wrapper lida com busca de dados, loading e erros.
- O formulário sempre recebe dados completos e já inicializados, sem precisar de
useEffect
oureset
.
A árvore de componentes fica mais declarativa: “só renderize o form quando houver dados.”
Outras armadilhas comuns
- Misturar inputs controlados e não controlados: escolha
register
/Controller
de forma consistente ou terá valores inconsistentes. - Esquecer regras de validação: o RHF integra naturalmente com validadores de schema como Zod. Evite recriar lógica de validação no
onSubmit
. - Re-renders desnecessários: usar
watch()
no nível superior em formulários grandes sem seletores gera problemas de performance. PrefirauseWatch
para assinaturas granulares.
Conclusão
React Hook Form é uma abstração de baixo custo e baixo boilerplate quando usado corretamente. Mas tratá-lo como se fosse apenas um useState
glorificado anula seus benefícios. Ao adotar seus padrões idiomáticos — register
, Controller
, FormProvider
, defaultValues
e validação por schema — seus formulários ficam mais simples, performáticos e fáceis de manter.