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 ou reset.

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. Prefira useWatch 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.