React forms can be controlled (React manages state) or uncontrolled (DOM manages state). React 19 also introduces Actions for simpler form handling.
Controlled components — React controls the input: The input value is tied to React state, and every keystroke updates that state:
1import { useState } from "react";23function LoginForm() {4 const [email, setEmail] = useState("");5 const [password, setPassword] = useState("");6 const [errors, setErrors] = useState<{ email?: string; password?: string }>({});78 const validate = () => {9 const newErrors: typeof errors = {};10 if (!email.includes("@")) newErrors.email = "Invalid email";11 if (password.length < 8) newErrors.password = "Password too short";12 setErrors(newErrors);13 return Object.keys(newErrors).length === 0;14 };1516 const handleSubmit = (e: React.FormEvent) => {17 e.preventDefault();18 if (validate()) {19 console.log("Login:", { email, password });20 }21 };2223 return (24 <form onSubmit={handleSubmit}>25 <div>26 <label htmlFor="email">Email</label>27 <input28 id="email"29 type="email"30 value={email}31 onChange={e => setEmail(e.target.value)}32 />33 {errors.email && <span style={{ color: "red" }}>{errors.email}</span>}34 </div>35 <div>36 <label htmlFor="password">Password</label>37 <input38 id="password"39 type="password"40 value={password}41 onChange={e => setPassword(e.target.value)}42 />43 {errors.password && <span style={{ color: "red" }}>{errors.password}</span>}44 </div>45 <button type="submit">Login</button>46 </form>47 );48}
Uncontrolled components — DOM manages the input:
Use ref to read the value only when needed (at submission):
1function LoginForm() {2 const emailRef = useRef<HTMLInputElement>(null);3 const passwordRef = useRef<HTMLInputElement>(null);45 const handleSubmit = (e: React.FormEvent) => {6 e.preventDefault();7 console.log(emailRef.current?.value);8 console.log(passwordRef.current?.value);9 };1011 return (12 <form onSubmit={handleSubmit}>13 <input type="email" ref={emailRef} defaultValue="" />14 <input type="password" ref={passwordRef} defaultValue="" />15 <button type="submit">Login</button>16 </form>17 );18}
React 19 Actions — the modern approach:
Use useActionState and useFormStatus for form handling with built-in pending state:
1import { useActionState } from "react";2import { useFormStatus } from "react-dom";34function SubmitButton() {5 const { pending } = useFormStatus();6 return (7 <button type="submit" disabled={pending}>8 {pending ? "Logging in..." : "Login"}9 </button>10 );11}1213function LoginForm() {14 const [error, submitAction, isPending] = useActionState(15 async (prev: string | null, formData: FormData) => {16 const email = formData.get("email") as string;17 const password = formData.get("password") as string;1819 const result = await login(email, password);20 if (result.error) return result.error;21 redirect("/dashboard");22 return null;23 },24 null25 );2627 return (28 <form action={submitAction}>29 <input name="email" type="email" required />30 <input name="password" type="password" required />31 {error && <p className="error">{error}</p>}32 <SubmitButton />33 </form>34 );35}
When to use which:
Libraries for complex forms: React Hook Form and Formik handle validation, error messaging, and form state management for large forms with many fields.