Top 10 Common Mistakes When Using React Hook Form

Avoid these top 10 mistake using React Hook Form to build more efficient, error-free forms. Learn best practices for validation, TypeScript integration, and performance optimization.

React Hook Form has become one of the most popular form handling libraries in the React ecosystem, offering an elegant solution for managing complex forms with minimal re-renders and excellent performance. But even experienced developers can make mistakes when implementing it. This guide identifies the top 10 common mistakes when using React Hook Form and provides actionable solutions to avoid them.

Table of Contents

  1. Forgetting to Register Form Fields
  2. Mishandling Form Validation
  3. Not Implementing Proper TypeScript Integration
  4. Ignoring Form State and Lifecycle
  5. Unnecessary Re-renders with Watch
  6. Misusing Controller Component
  7. Improper Error Handling
  8. Inefficient Reset and Default Values
  9. Inadequate Complex Field Arrays Management
  10. Poor Integration with UI Libraries

Mistake #1: Forgetting to Register Form Fields Using React Hook Form

One of the most common mistakes is not properly registering form fields with React Hook Form. This oversight can lead to fields that don’t validate correctly or values that don’t appear in your form submission data.

The Problem

// ❌ Incorrect: Input not registered with React Hook Form
const MyForm = () => {
  const { handleSubmit } = useForm();
  
  const onSubmit = (data) => console.log(data);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="email" type="email" /> {/* This field won't be in form data */}
      <button type="submit">Submit</button>
    </form>
  );
};

The Solution

// ✅ Correct: Properly registered input
const MyForm = () => {
  const { register, handleSubmit } = useForm();
  
  const onSubmit = (data) => console.log(data);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} type="email" />
      <button type="submit">Submit</button>
    </form>
  );
};

Remember that every field you want to include in your form data must be registered using the register function returned by useForm().

Mistake #2: Mishandling Form Validation Using React Hook Form

Form validation is one of the core features of React Hook Form, but many developers struggle with implementing it effectively.

The Problem

jsx// ❌ Incorrect: Complex validation logic in component
const MyForm = () => {
  const { register, handleSubmit, setError } = useForm();
  
  const onSubmit = (data) => {
    // Manual validation checks scattered throughout component
    if (data.password !== data.confirmPassword) {
      setError("confirmPassword", { 
        type: "manual", 
        message: "Passwords don't match" 
      });
      return;
    }
    // More validation logic...
    console.log(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("password")} type="password" />
      <input {...register("confirmPassword")} type="password" />
      <button type="submit">Submit</button>
    </form>
  );
};

The Solution

jsx// ✅ Correct: Using built-in validation with schema
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const schema = yup.object({
  password: yup.string().required("Password is required").min(8, "Password must be at least 8 characters"),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], 'Passwords must match')
    .required('Confirm password is required')
}).required();

const MyForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema)
  });
  
  const onSubmit = (data) => console.log(data);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("password")} type="password" />
      {errors.password && <p>{errors.password.message}</p>}
      
      <input {...register("confirmPassword")} type="password" />
      {errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
      
      <button type="submit">Submit</button>
    </form>
  );
};

Leveraging schema validation with libraries like Yup, Zod, or Joi through @hookform/resolvers keeps your validation logic declarative, consistent, and maintainable.

Mistake #3: Not Implementing Proper TypeScript Integration

TypeScript support is one of React Hook Form’s strengths, but many developers don’t take full advantage of it, leading to type safety issues.

The Problem

tsx// ❌ Incorrect: No proper type definitions
const MyForm = () => {
  const { register, handleSubmit } = useForm();
  
  // No type safety for form data
  const onSubmit = (data) => console.log(data.email); // Might not exist!
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} type="email" />
      <button type="submit">Submit</button>
    </form>
  );
};

The Solution

tsx// ✅ Correct: Proper TypeScript integration
type FormValues = {
  email: string;
  password: string;
  rememberMe?: boolean;
};

const MyForm = () => {
  const { register, handleSubmit } = useForm<FormValues>({
    defaultValues: {
      email: '',
      password: '',
      rememberMe: false
    }
  });
  
  // Full type safety for form data
  const onSubmit = (data: FormValues) => {
    console.log(data.email); // TypeScript knows this exists
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} type="email" />
      <input {...register("password")} type="password" />
      <input {...register("rememberMe")} type="checkbox" />
      <button type="submit">Submit</button>
    </form>
  );
};

Always define your form value types and pass them to the useForm<T>() generic to enjoy full TypeScript support.

Mistake #4: Ignoring Form State and Lifecycle

Not properly tracking form state can lead to poor user experience and missed opportunities for UI improvements Using React Hook Form.

The Problem

jsx// ❌ Incorrect: Not utilizing form state
const MyForm = () => {
  const { register, handleSubmit } = useForm();
  
  const onSubmit = (data) => {
    // API submission logic
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username")} />
      <input {...register("password")} type="password" />
      <button type="submit">Submit</button> {/* No loading state */}
    </form>
  );
};

The Solution

jsx// ✅ Correct: Leveraging form state
const MyForm = () => {
  const { 
    register, 
    handleSubmit, 
    formState: { isSubmitting, isDirty, isValid, errors, dirtyFields, touchedFields } 
  } = useForm();
  
  const onSubmit = async (data) => {
    // API submission logic
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("username")} />
      {touchedFields.username && errors.username && (
        <p>{errors.username.message}</p>
      )}
      
      <input {...register("password")} type="password" />
      
      <button 
        type="submit" 
        disabled={!isDirty || !isValid || isSubmitting}
      >
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
      
      {isDirty && (
        <button type="button">Form has unsaved changes</button>
      )}
    </form>
  );
};

The formState object contains valuable information about your form’s current state, including whether fields are dirty (changed), touched (interacted with), or valid, as well as the submission status.

Mistake #5: Unnecessary Re-renders with Watch

The watch function is powerful but can cause performance issues if used incorrectly.

The Problem

jsx// ❌ Incorrect: Wasteful use of watch that causes re-renders
const MyForm = () => {
  const { register, handleSubmit, watch } = useForm();
  
  // This causes the component to re-render on EVERY keystroke
  const allValues = watch();
  
  // This logs on every render
  console.log('Form re-rendered', allValues);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input {...register("email")} />
      <input {...register("address")} />
      <input {...register("city")} />
      <input {...register("state")} />
      <input {...register("zip")} />
      <button type="submit">Submit</button>
    </form>
  );
};

The Solution

jsx// ✅ Correct: Selective watching and optimized rendering
const MyForm = () => {
  const { register, handleSubmit, watch } = useForm();
  
  // Only watch specific fields that need reactive updates
  const firstName = watch("firstName");
  const lastName = watch("lastName");
  
  // Or use the watch callback for side effects without re-renders
  React.useEffect(() => {
    const subscription = watch((value, { name, type }) => {
      console.log('Field changed:', name, value);
      // Perform side effects without re-rendering
    });
    return () => subscription.unsubscribe();
  }, [watch]);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      
      {/* Conditional rendering based on watched fields */}
      {firstName && lastName && (
        <p>Hello, {firstName} {lastName}!</p>
      )}
      
      <input {...register("email")} />
      {/* Other fields that don't need watching */}
      <button type="submit">Submit</button>
    </form>
  );
};

Be selective with what you watch, and consider using the subscription pattern for side effects without re-renders.

Mistake #6: Misusing Controller Component

The Controller component is essential for integrating third-party input components, but developers often implement it incorrectly.

The Problem

jsx// ❌ Incorrect: Inefficient use of Controller
import { Controller } from "react-hook-form";
import Select from "react-select";

const MyForm = () => {
  const { control, handleSubmit } = useForm();
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Incorrect - using Controller for a regular input */}
      <Controller
        name="firstName"
        control={control}
        render={({ field }) => <input {...field} />}
      />
      
      {/* Passing props incorrectly to third-party component */}
      <Controller
        name="country"
        control={control}
        render={({ field }) => <Select {...field} options={countries} />}
      />
    </form>
  );
};

The Solution

jsx// ✅ Correct: Proper Controller usage
import { Controller } from "react-hook-form";
import Select from "react-select";

const MyForm = () => {
  const { register, control, handleSubmit } = useForm();
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Use register for native inputs */}
      <input {...register("firstName")} />
      
      {/* Correctly mapping props for third-party components */}
      <Controller
        name="country"
        control={control}
        render={({ field: { onChange, value, ref, ...rest } }) => (
          <Select
            {...rest}
            inputRef={ref}
            options={countries}
            value={countries.find(c => c.value === value) || null}
            onChange={(selectedOption) => onChange(selectedOption.value)}
          />
        )}
      />
    </form>
  );
};

Use Controller only for third-party or custom components that can’t directly use register, and make sure to properly map the component’s value/onChange interface to React Hook Form’s expectations.

Let’s Build Something Great Together!

We specialize in custom React JS Development that elevate your brand and deliver stunning results. Ready to transform your online presence?

Get Your React JS Website →

Mistake #7: Improper Error Handling

Many developers struggle with displaying error messages effectively or handling validation errors in a user-friendly way.

The Problem

jsx// ❌ Incorrect: Poor error handling
const MyForm = () => {
  const { register, handleSubmit, formState } = useForm();
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: true })} />
      {/* Generic error message provides poor feedback */}
      {formState.errors.email && <p>Error in email field</p>}
      
      <button type="submit">Submit</button>
    </form>
  );
};

The Solution

jsx// ✅ Correct: Comprehensive error handling
const MyForm = () => {
  const { 
    register, 
    handleSubmit, 
    formState: { errors, isSubmitted, isSubmitSuccessful },
    setError, 
    clearErrors 
  } = useForm();
  
  const onSubmit = async (data) => {
    try {
      await submitToAPI(data);
    } catch (error) {
      // Handle API errors
      if (error.field) {
        setError(error.field, { 
          type: "server", 
          message: error.message 
        });
      } else {
        setError("root.serverError", { 
          type: "server", 
          message: "Something went wrong. Please try again." 
        });
      }
    }
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Show top-level server errors */}
      {errors.root?.serverError && (
        <div className="error-banner">{errors.root.serverError.message}</div>
      )}
      
      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input 
          id="email"
          {...register("email", { 
            required: "Email is required", 
            pattern: {
              value: /\S+@\S+\.\S+/,
              message: "Please enter a valid email"
            }
          })} 
        />
        {/* Specific, helpful error message */}
        {errors.email && <p className="error">{errors.email.message}</p>}
      </div>
      
      <button type="submit">Submit</button>
      
      {/* Success message */}
      {isSubmitted && isSubmitSuccessful && !Object.keys(errors).length && (
        <p className="success">Form submitted successfully!</p>
      )}
    </form>
  );
};

Provide specific, actionable error messages and handle different types of errors (validation, server, etc.) appropriately for a better user experience.

Mistake #8: Inefficient Reset and Default Values

Incorrectly handling form resets and default values can lead to inconsistent UI states and poor user experience.

The Problem

jsx// ❌ Incorrect: Improper form reset and default values
const MyForm = () => {
  const { register, handleSubmit, reset } = useForm();
  const [userData, setUserData] = useState(null);
  
  // Fetching user data but not updating form
  useEffect(() => {
    fetchUserData().then(data => {
      setUserData(data);
      // No form reset with new data
    });
  }, []);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} placeholder="First Name" />
      <input {...register("lastName")} placeholder="Last Name" />
      <button type="submit">Submit</button>
      <button type="button" onClick={() => reset()}>Reset</button>
    </form>
  );
};

The Solution

jsx// ✅ Correct: Proper handling of resets and defaults
const MyForm = () => {
  const [userData, setUserData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  
  // Define default values based on current state
  const defaultValues = React.useMemo(() => ({
    firstName: userData?.firstName || '',
    lastName: userData?.lastName || '',
    email: userData?.email || ''
  }), [userData]);
  
  const { register, handleSubmit, reset, formState } = useForm({
    defaultValues
  });
  
  // Reset form when default values change
  useEffect(() => {
    reset(defaultValues);
  }, [defaultValues, reset]);
  
  // Fetch and set user data
  useEffect(() => {
    setIsLoading(true);
    fetchUserData()
      .then(data => {
        setUserData(data);
      })
      .finally(() => {
        setIsLoading(false);
      });
  }, []);
  
  const onSubmit = async (data) => {
    await updateUser(data);
    // Reset to the new values after successful submission
    setUserData(data);
  };
  
  if (isLoading) return <p>Loading...</p>;
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} placeholder="First Name" />
      <input {...register("lastName")} placeholder="Last Name" />
      <input {...register("email")} placeholder="Email" type="email" />
      
      <button type="submit" disabled={!formState.isDirty}>
        {formState.isDirty ? 'Save Changes' : 'No Changes'}
      </button>
      
      <button 
        type="button" 
        onClick={() => reset()} 
        disabled={!formState.isDirty}
      >
        Cancel
      </button>
    </form>
  );
};

Properly manage form resets and default values, especially when dealing with asynchronously loaded data or after form submissions.

Mistake #9: Inadequate Complex Field Arrays Management

Handling arrays of fields (like multiple addresses or nested forms) can be challenging, and many developers implement them inefficiently.

The Problem

jsx// ❌ Incorrect: Poor field array implementation
const MyForm = () => {
  const { register, handleSubmit } = useForm();
  const [addresses, setAddresses] = useState([{ street: '', city: '' }]);
  
  // Manual array handling outside React Hook Form
  const addAddress = () => {
    setAddresses([...addresses, { street: '', city: '' }]);
  };
  
  const removeAddress = (index) => {
    const newAddresses = [...addresses];
    newAddresses.splice(index, 1);
    setAddresses(newAddresses);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {addresses.map((_, index) => (
        <div key={index}>
          <input {...register(`addresses[${index}].street`)} placeholder="Street" />
          <input {...register(`addresses[${index}].city`)} placeholder="City" />
          <button type="button" onClick={() => removeAddress(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={addAddress}>Add Address</button>
      <button type="submit">Submit</button>
    </form>
  );
};

The Solution

jsx// ✅ Correct: Using useFieldArray for dynamic fields
import { useForm, useFieldArray } from "react-hook-form";

const MyForm = () => {
  const { register, control, handleSubmit } = useForm({
    defaultValues: {
      addresses: [{ street: '', city: '' }]
    }
  });
  
  const { fields, append, remove } = useFieldArray({
    control,
    name: "addresses"
  });
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input 
            {...register(`addresses.${index}.street`)} 
            placeholder="Street" 
          />
          <input 
            {...register(`addresses.${index}.city`)} 
            placeholder="City" 
          />
          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}
      
      <button 
        type="button" 
        onClick={() => append({ street: '', city: '' })}
      >
        Add Address
      </button>
      
      <button type="submit">Submit</button>
    </form>
  );
};

Use the useFieldArray hook for efficient management of dynamic form fields, with proper performance optimization and state management.

Mistake #10: Poor Integration with UI Libraries

Developers often struggle to properly integrate React Hook Form with UI component libraries like Material-UI, Chakra UI, or Ant Design.

The Problem

jsx// ❌ Incorrect: Poor integration with UI library
import { TextField, Button } from "@mui/material";

const MyForm = () => {
  const { register, handleSubmit } = useForm();
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* This won't work properly */}
      <TextField {...register("email")} label="Email" />
      
      <Button type="submit">Submit</Button>
    </form>
  );
};

The Solution

jsx// ✅ Correct: Proper UI library integration
import { TextField, Button } from "@mui/material";
import { Controller } from "react-hook-form";

const MyForm = () => {
  const { control, handleSubmit, formState: { errors } } = useForm({
    defaultValues: {
      email: '',
      password: ''
    }
  });
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="email"
        control={control}
        rules={{ 
          required: "Email is required",
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: "Please enter a valid email"
          }
        }}
        render={({ field }) => (
          <TextField
            {...field}
            label="Email"
            variant="outlined"
            error={!!errors.email}
            helperText={errors.email?.message}
            fullWidth
            margin="normal"
          />
        )}
      />
      
      <Controller
        name="password"
        control={control}
        rules={{ 
          required: "Password is required",
          minLength: {
            value: 8,
            message: "Password must be at least 8 characters"
          }
        }}
        render={({ field }) => (
          <TextField
            {...field}
            type="password"
            label="Password"
            variant="outlined"
            error={!!errors.password}
            helperText={errors.password?.message}
            fullWidth
            margin="normal"
          />
        )}
      />
      
      <Button 
        type="submit" 
        variant="contained" 
        color="primary"
        fullWidth
        sx={{ mt: 2 }}
      >
        Submit
      </Button>
    </form>
  );
};

Use the Controller component to bridge React Hook Form with UI component libraries, properly mapping field props and error states to the UI components.

using react hook form

Conclusion

React Hook Form is an incredibly powerful library that can streamline your form development process when used correctly. By avoiding these common mistakes, you’ll create more maintainable, performant, and user-friendly forms.

Remember these key principles:

  • Always register your fields or use Controller appropriately
  • Leverage schema validation for cleaner code
  • Use TypeScript properly for type safety
  • Track and utilize form state for better UX
  • Be thoughtful about performance with selective watching
  • Implement proper error handling
  • Manage field arrays with useFieldArray
  • Integrate carefully with UI libraries

By addressing these issues, you’ll be able to take full advantage of React Hook Form’s capabilities and deliver a better experience for both developers and end users.

Further Resources

© 2025, CodeHazel Inc - All rights reserved.