troubleshooting2025-01-30·11·214/348

Resolving React Hook Form Setup Issues and Performance Pitfalls

Solutions for common React Hook Form configuration problems including controller integration, schema validation, and form state management in large forms.

Introduction

React Hook Form is the most popular form library for React, but its configuration options can be confusing for developers coming from controlled component patterns. The library's uncontrolled approach provides excellent performance, but integrating with external component libraries and managing complex form state requires understanding the Controller component and the register API.

This post addresses the setup issues I encounter most frequently when integrating React Hook Form with Material UI, custom components, and Zod validation schemas.

Environment

node --version
# v20.11.0

npm list react react-hook-form zod @hookform/resolvers
# react@18.2.0
# react-hook-form@7.49.0
# zod@3.22.4
# @hookform/resolvers@3.3.4

The application uses React Hook Form for:

  1. Multi-step registration forms
  2. Dynamic forms with add/remove fields
  3. Forms that interact with external component libraries

Problem

The first issue was that controlled components like Material UI's TextField and Select did not work with register(). The form fields displayed but never updated when users typed:

// This does not work with Material UI components
const { register } = useForm();


// The field accepts the spread but does not update form state

The second issue was that form submission triggered unnecessary re-renders. Every keystroke in any field caused all other fields to re-render, destroying performance for forms with 20+ fields:

// This pattern causes performance issues
const { watch, handleSubmit } = useForm();

// watch() causes re-renders on every change
const watchedValues = watch();

return (
  
{fields.map((field) => ( setValue(field.name, e.target.value)} /> ))}
);

The third issue was that the useFieldArray hook did not preserve field values when reordering items. After dragging an item from position 2 to position 0, the values at positions 0 and 2 were swapped but the UI displayed the old values until the next render cycle.

Analysis

React Hook Form uses uncontrolled components by default, which means it attaches a ref to the input element and reads its value on submission. Material UI components do not forward refs to the underlying input element by default, so register() cannot attach the ref. The Controller component bridges this gap by managing the controlled component's value through React Hook Form's state.

The performance issue with watch() is that it subscribes to all field changes by default. When any field changes, the component containing watch() re-renders, which causes all child components to re-render as well. This defeats React Hook Form's uncontrolled component optimization.

The useFieldArray reordering issue is a known limitation. The hook uses the array index as the key, which causes React to reuse existing DOM nodes instead of moving them. This leads to stale values in the displayed fields.

Solution

Use Controller for all controlled components:

import { useForm, Controller } from "react-hook-form";
import TextField from "@mui/material/TextField";
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";

function RegistrationForm() {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      email: "",
      role: "user",
    },
  });

  return (
    
( )} /> ( )} /> ); }

Optimize performance by subscribing only to specific fields:

import { useForm, useWatch } from "react-hook-form";

function LargeForm() {
  const { register, handleSubmit, control } = useForm();

  // Only re-render when 'email' changes
  const EmailDisplay = () => {
    const email = useWatch({
      control,
      name: "email",
    });

    return 
Email: {email}
; }; return (
); }

Fix the useFieldArray reordering issue with unique keys:

import { useFieldArray, useForm } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";

interface ExperienceItem {
  id: string;
  company: string;
  role: string;
  startDate: string;
}

function ExperienceForm() {
  const { control, handleSubmit } = useForm<{
    experiences: ExperienceItem[];
  }>({
    defaultValues: {
      experiences: [{ id: uuidv4(), company: "", role: "", startDate: "" }],
    },
  });

  const { fields, append, remove, move } = useFieldArray({
    control,
    name: "experiences",
    keyName: "fieldId", // Use custom key instead of 'id'
  });

  return (
    
{fields.map((field, index) => (
))}
); }

Integrate with Zod validation using the resolver:

import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useForm } from "react-hook-form";

const registerSchema = z
  .object({
    name: z.string().min(1, "Name is required").max(100),
    email: z.string().email("Invalid email address"),
    password: z
      .string()
      .min(8, "Password must be at least 8 characters")
      .regex(/[A-Z]/, "Must contain at least one uppercase letter"),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords do not match",
    path: ["confirmPassword"],
  });

type RegisterInput = z.infer;

function RegisterForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(registerSchema),
    mode: "onBlur", // Validate on blur for better UX
  });

  return (
    
{errors.name && {errors.name.message}}
{errors.email && {errors.email.message}}
{errors.password && {errors.password.message}}
{errors.confirmPassword && ( {errors.confirmPassword.message} )}
); }

Lessons Learned

  1. Use Controller for any component that manages its own state (Material UI, Ant Design, React Select). Use register only for native HTML inputs.

  2. Subscribe to specific fields with useWatch instead of watch() to prevent unnecessary re-renders in large forms.

  3. Set mode: "onBlur" for better user experience. Real-time validation on every keystroke is distracting for most form types.

  4. Provide unique keys to useFieldArray using keyName to avoid the reordering value swap issue.

  5. Combine React Hook Form with Zod using @hookform/resolvers to get type-safe validation that shares schemas with your backend.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.