Common Form Cases & Solutions
Uncontrolled to Controlled Input Warning
The Warning
You may encounter this warning in React:
Warning: A component is changing an uncontrolled input to be controlled.
This is likely caused by the value changing from undefined to a defined value…Why This Happens
This warning occurs when an input’s value prop changes from undefined to a defined value (or vice versa). React distinguishes between:
- Controlled inputs: Have a
valueprop that is always defined (even if it’s an empty string) - Uncontrolled inputs: Have
value={undefined}or novalueprop at all
When React Hook Form initializes, if defaultValues are not set or if a field’s value starts as undefined, the input is uncontrolled. Once the value becomes defined, React tries to convert it to controlled, which triggers the warning.
Solutions
Solution 1: Fix in Input Component (Shadcn UI)
If you have direct access to the input component (like in Shadcn UI), you can fix it at the component level by ensuring the value is always defined:
// components/ui/input.tsx
export const Input = ({ value, ...props }) => {
return <input value={value || ''} {...props} />;
};This ensures the value prop is always a string (never undefined), making the input always controlled.
Solution 2: Use defaultValues (External Libraries)
If you don’t have direct access to the input element (like when using Chakra UI’s Input component or other third-party libraries), you should always provide defaultValues in useForm:
import { useForm } from "react-hook-form";
import { Input } from "@chakra-ui/react";
const form = useForm({
defaultValues: {
username: "",
email: "",
// ... all fields should have default values
},
});Note: We’ll discuss
initmethods from utility files later, which can help standardize default values across your application.
Edit Mode: Populating Form with Server Data
When opening a form in edit mode, you need to populate the form fields with data fetched from the server. React Hook Form provides the reset() method for this purpose.
The Problem
If you try to set defaultValues with async data, it won’t work because useForm is called synchronously, but data fetching is asynchronous:
// ❌ This won't work
const { data } = useQuery(...);
const form = useForm({
defaultValues: data, // data is undefined initially
});The Solution: Use reset() in useEffect
After fetching the data, call form.reset() with the fetched data:
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
export function EditUserForm({ userId }: { userId: string }) {
const form = useForm({
defaultValues: {
username: "",
email: "",
// ... initial empty values
},
});
// Fetch data from server
const { data: userData, isLoading } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
});
// Populate form when data is fetched
useEffect(() => {
if (userData) {
form.reset(userData); // This updates all form fields with the fetched data
}
}, [userData, form]);
const onSubmit = (data: FormValues) => {
// Handle form submission
};
if (isLoading) return <div>Loading...</div>;
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Your form fields */}
</form>
);
}How reset() Works
The reset() method:
- Updates all form field values
- Resets the form’s validation state
- Marks the form as “pristine” (not dirty)
- Can optionally accept new default values for future resets
Best Practices
- Always provide initial
defaultValueseven if they’re empty, to avoid the uncontrolled input warning - Use
reset()after data fetching to populate the form with server data - Handle loading states to prevent form submission before data is loaded
- Reset on cleanup if needed when the component unmounts or the ID changes
useEffect(() => {
if (userData) {
form.reset(userData);
}
// Cleanup: reset form when userId changes
return () => {
form.reset();
};
}, [userData, userId, form]);Summary
- Uncontrolled Input Warning: Always provide
defaultValuesor ensure input components handleundefinedvalues - Edit Mode: Use
form.reset(fetchedData)in auseEffectafter data is fetched from the server - Best Practice: Initialize forms with empty default values, then populate with
reset()when data is available