React Forms
React Forms
Section titled “React Forms”React has two approaches to form inputs: controlled (React drives the value) and uncontrolled (the DOM drives the value). For most forms, use React Hook Form.
Controlled Inputs
Section titled “Controlled Inputs”The input value is bound to React state:
function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); console.log({ 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> );}Works fine for small forms. Becomes tedious with many fields — use React Hook Form for anything larger.
React Hook Form
Section titled “React Hook Form”Minimal re-renders, built-in validation, great TypeScript support:
npm install react-hook-formimport { useForm, SubmitHandler } from 'react-hook-form';
interface LoginInputs { email: string; password: string;}
function LoginForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm<LoginInputs>();
const onSubmit: SubmitHandler<LoginInputs> = async (data) => { await login(data.email, data.password); };
return ( <form onSubmit={handleSubmit(onSubmit)}> <input type="email" {...register('email', { required: 'Email is required', pattern: { value: /^\S+@\S+$/i, message: 'Invalid email' }, })} /> {errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password', { required: 'Password is required', minLength: { value: 8, message: 'Min 8 characters' }, })} /> {errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Logging in...' : 'Login'} </button> </form> );}Validation with Zod
Section titled “Validation with Zod”Use Zod for schema-based validation (integrates with React Hook Form):
npm install zod @hookform/resolversimport { z } from 'zod';import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Min 8 characters'), age: z.number().min(18, 'Must be 18+'),});
type FormData = z.infer<typeof schema>;
function RegisterForm() { const { register, handleSubmit, formState: { errors } } = useForm<FormData>({ resolver: zodResolver(schema), });
return ( <form onSubmit={handleSubmit(data => console.log(data))}> <input type="email" {...register('email')} /> {errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} /> {errors.password && <span>{errors.password.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} /> {errors.age && <span>{errors.age.message}</span>}
<button type="submit">Register</button> </form> );}Select, Checkbox, and Radio
Section titled “Select, Checkbox, and Radio”const { register, watch } = useForm();const role = watch('role');
return ( <form> {/* Select */} <select {...register('role')}> <option value="user">User</option> <option value="admin">Admin</option> </select>
{/* Checkbox */} <input type="checkbox" {...register('acceptTerms')} />
{/* Radio */} <input type="radio" value="male" {...register('gender')} /> <input type="radio" value="female" {...register('gender')} /> </form>);Dynamic Fields with useFieldArray
Section titled “Dynamic Fields with useFieldArray”import { useFieldArray } from 'react-hook-form';
function TagsForm() { const { control, register } = useForm({ defaultValues: { tags: [{ name: '' }] } }); const { fields, append, remove } = useFieldArray({ control, name: 'tags' });
return ( <form> {fields.map((field, index) => ( <div key={field.id}> <input {...register(`tags.${index}.name`)} /> <button type="button" onClick={() => remove(index)}>Remove</button> </div> ))} <button type="button" onClick={() => append({ name: '' })}>Add Tag</button> </form> );}File Uploads
Section titled “File Uploads”function FileUpload() { const { register, handleSubmit } = useForm();
const onSubmit = (data: { file: FileList }) => { const formData = new FormData(); formData.append('file', data.file[0]); fetch('/api/upload', { method: 'POST', body: formData }); };
return ( <form onSubmit={handleSubmit(onSubmit)}> <input type="file" {...register('file')} /> <button type="submit">Upload</button> </form> );}