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