Forms and form handling in React
Form Handling in React
React provides two approaches for handling forms: controlled components (React manages form state) and uncontrolled components (DOM manages form state). React 19 also introduces form actions.
Controlled Components
React state is the single source of truth for input values:
tsx
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
login({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}Uncontrolled Components (useRef)
DOM holds the data; React reads it when needed:
tsx
function SearchForm() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(inputRef.current?.value); // Read from DOM
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} type="text" defaultValue="" />
<button type="submit">Search</button>
</form>
);
}Complex Form with useReducer
tsx
type FormState = {
name: string;
email: string;
role: string;
errors: Record<string, string>;
};
type FormAction =
| { type: "SET_FIELD"; field: string; value: string }
| { type: "SET_ERROR"; field: string; error: string }
| { type: "RESET" };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case "SET_FIELD":
return { ...state, [action.field]: action.value };
case "SET_ERROR":
return { ...state, errors: { ...state.errors, [action.field]: action.error } };
case "RESET":
return { name: "", email: "", role: "", errors: {} };
}
}
function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, {
name: "", email: "", role: "", errors: {}
});
return (
<form>
<input
value={state.name}
onChange={e => dispatch({ type: "SET_FIELD", field: "name", value: e.target.value })}
/>
{state.errors.name && <span>{state.errors.name}</span>}
{/* ... more fields */}
</form>
);
}React Hook Form (Popular Library)
tsx
import { useForm } from "react-hook-form";
interface FormData {
email: string;
password: string;
age: number;
}
function SignUpForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<FormData>();
const onSubmit = async (data: FormData) => {
await createUser(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("email", {
required: "Email is required",
pattern: { value: /^[^@]+@[^@]+$/, message: "Invalid email" }
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
{...register("password", { required: true, minLength: 8 })}
/>
{errors.password && <span>Min 8 characters</span>}
<input
type="number"
{...register("age", { min: 18, max: 120 })}
/>
<button disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Sign Up"}
</button>
</form>
);
}Controlled vs Uncontrolled
| Feature | Controlled | Uncontrolled |
|---|---|---|
| Value stored in | React state | DOM |
| Validation | On every change | On submit |
| Re-renders | On every keystroke | Minimal |
| Use case | Complex forms, live validation | Simple forms, file inputs |
| Library | React Hook Form, Formik | Native form, useRef |
Form Validation Pattern
tsx
function validateEmail(email: string): string | null {
if (!email) return "Email is required";
if (!/^[^@]+@[^@]+\.[^@]+$/.test(email)) return "Invalid email";
return null;
}
function Form() {
const [email, setEmail] = useState("");
const [error, setError] = useState<string | null>(null);
const [touched, setTouched] = useState(false);
const handleBlur = () => {
setTouched(true);
setError(validateEmail(email));
};
return (
<div>
<input
value={email}
onChange={e => { setEmail(e.target.value); if (touched) setError(validateEmail(e.target.value)); }}
onBlur={handleBlur}
/>
{touched && error && <span className="error">{error}</span>}
</div>
);
}Important:
Use controlled components when you need real-time validation, conditional logic, or synced state. Use uncontrolled components for simple forms or file inputs. For production apps, prefer React Hook Form — it minimizes re-renders and provides excellent validation. Always validate on the server too.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.