Skip to main content
Practice Problems

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> ); }
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

FeatureControlledUncontrolled
Value stored inReact stateDOM
ValidationOn every changeOn submit
Re-rendersOn every keystrokeMinimal
Use caseComplex forms, live validationSimple forms, file inputs
LibraryReact Hook Form, FormikNative 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems