React Cheatsheet
Forms
Use this React reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Controlled vs Uncontrolled Inputs
| Controlled | Uncontrolled | |
|---|---|---|
| Value source | React state | DOM |
| Read value | value prop | ref.current.value |
| Reset | setState('') | ref.current.value = '' |
| Validation | On every change | On submit / blur |
| Use when | Most cases | File inputs, integrations |
Form Actions (React 19)
Pass a function to <form action={…}> and React calls it with the form's FormData on submit — no e.preventDefault(), no per-field state. Uncontrolled fields are reset automatically after the action resolves.
// Minimal action — works with plain uncontrolled inputs function Subscribe() { async function subscribe(formData) { await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify({ email: formData.get('email') }), }); } return ( <form action={subscribe}> <input name="email" type="email" required /> <button type="submit">Subscribe</button> </form> ); }
useActionState — result + pending flag
import { useActionState } from 'react'; function Signup() { // action receives (previousState, formData); returns the next state const [state, formAction, isPending] = useActionState( async (prev, formData) => { const res = await createAccount(formData.get('email')); if (!res.ok) return { error: res.message }; return { error: null }; }, { error: null } // initial state ); return ( <form action={formAction}> <input name="email" type="email" required /> <button type="submit" disabled={isPending}> {isPending ? 'Creating…' : 'Sign up'} </button> {state.error && <p role="alert">{state.error}</p>} </form> ); }
useFormStatus — pending state from inside the form
import { useFormStatus } from 'react-dom'; // note: react-dom, not react // Must be rendered INSIDE the <form> whose status it reads function SubmitButton() { const { pending, data, method, action } = useFormStatus(); return <button type="submit" disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>; } <form action={saveAction}> <input name="title" /> <SubmitButton /> </form>
useOptimistic — show the expected result immediately
import { useOptimistic } from 'react'; function Todos({ todos, addTodo }) { const [optimisticTodos, addOptimistic] = useOptimistic( todos, (current, newTodo) => [...current, { ...newTodo, pending: true }] ); async function action(formData) { const todo = { text: formData.get('text') }; addOptimistic(todo); // shows instantly, reverts on failure await addTodo(todo); // real mutation; todos prop updates after } return ( <> <ul>{optimisticTodos.map(t => <li key={t.text}>{t.text}{t.pending && ' …'}</li>)}</ul> <form action={action}><input name="text" /><button>Add</button></form> </> ); }
Use Actions for submit-and-mutate forms; keep controlled inputs (below) when you need per-keystroke logic like live validation or filtering.
Controlled Inputs
React state is the single source of truth.
// Text input function TextForm() { const [name, setName] = useState(''); return ( <input value={name} onChange={e => setName(e.target.value)} placeholder="Enter name" /> ); } // Number input const [age, setAge] = useState(''); <input type="number" value={age} onChange={e => setAge(e.target.value)} // value is always a string from DOM min={0} max={120} /> const numericAge = Number(age); // convert when needed // Textarea const [bio, setBio] = useState(''); <textarea value={bio} onChange={e => setBio(e.target.value)} rows={4} /> // Note: self-closing <textarea /> doesn't work — must have closing tag
Select / Dropdown
// Single select const [color, setColor] = useState('red'); <select value={color} onChange={e => setColor(e.target.value)}> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </select> // Multi-select const [selected, setSelected] = useState([]); <select multiple value={selected} onChange={e => { const vals = Array.from(e.target.selectedOptions, o => o.value); setSelected(vals); }} > <option value="a">Alpha</option> <option value="b">Beta</option> <option value="c">Gamma</option> </select>
Checkbox
// Single checkbox const [agreed, setAgreed] = useState(false); <input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} /> // Checkbox group (array of selected values) const [fruits, setFruits] = useState(['apple']); const toggle = (value) => setFruits(prev => prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value] ); {['apple', 'banana', 'cherry'].map(f => ( <label key={f}> <input type="checkbox" value={f} checked={fruits.includes(f)} onChange={() => toggle(f)} /> {f} </label> ))}
File Input (always uncontrolled)
function FileUpload() { const fileRef = useRef(null); const [preview, setPreview] = useState(null); const handleChange = (e) => { const file = e.target.files[0]; if (!file) return; setPreview(URL.createObjectURL(file)); // To upload: use FormData const form = new FormData(); form.append('file', file); fetch('/upload', { method: 'POST', body: form }); }; return ( <> <input type="file" ref={fileRef} onChange={handleChange} accept="image/*" /> {preview && <img src={preview} alt="preview" width={200} />} {/* Reset file input */} <button onClick={() => { fileRef.current.value = ''; setPreview(null); }}> Clear </button> </> ); }
Form Submission
function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); // prevent page reload setError(''); try { await login({ email, password }); } catch (err) { setError(err.message); } }; return ( <form onSubmit={handleSubmit} noValidate> <input type="email" value={email} onChange={e => setEmail(e.target.value)} required /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} required /> {error && <p className="error">{error}</p>} <button type="submit">Log in</button> </form> ); }
Generic onChange Handler (multiple fields in one object)
function SignupForm() { const [form, setForm] = useState({ name: '', email: '', password: '' }); const handleChange = (e) => { const { name, value, type, checked } = e.target; setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value, })); }; return ( <form> <input name="name" value={form.name} onChange={handleChange} /> <input name="email" value={form.email} onChange={handleChange} /> <input name="password" value={form.password} onChange={handleChange} type="password" /> </form> ); }
Uncontrolled Inputs with useRef
function SimpleForm() { const nameRef = useRef(null); const emailRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); console.log(nameRef.current.value, emailRef.current.value); }; return ( <form onSubmit={handleSubmit}> <input ref={nameRef} defaultValue="Alice" /> {/* defaultValue sets initial value */} <input ref={emailRef} type="email" /> <button type="submit">Submit</button> </form> ); }
Default Values
// Controlled — initialize state const [name, setName] = useState('Alice'); // Uncontrolled — use defaultValue / defaultChecked <input defaultValue="Alice" /> <input type="checkbox" defaultChecked /> <select defaultValue="red">...</select>
Validation
HTML5 Built-in Validation
<input required minLength={3} maxLength={50} pattern="[A-Za-z]+" />
<input type="email" required />
<input type="url" />
<input type="number" min={1} max={100} step={1} />Custom Validation
function ValidatedInput({ value, onChange }) { const [touched, setTouched] = useState(false); const error = value.length < 3 ? 'Minimum 3 characters' : ''; return ( <div> <input value={value} onChange={e => { onChange(e.target.value); }} onBlur={() => setTouched(true)} aria-invalid={touched && !!error} aria-describedby="name-error" /> {touched && error && <p id="name-error" role="alert">{error}</p>} </div> ); }
Form Libraries
// React Hook Form — minimal re-renders, ref-based import { useForm } from 'react-hook-form'; function HookForm() { const { register, handleSubmit, formState: { errors } } = useForm(); return ( <form onSubmit={handleSubmit(data => console.log(data))}> <input {...register('email', { required: 'Required', pattern: { value: /\S+@\S+/, message: 'Invalid email' } })} /> {errors.email && <p>{errors.email.message}</p>} <button type="submit">Submit</button> </form> ); } // Formik — state-based // Zod / Yup — schema validation (pair with either library)
Range, Color, and Date Inputs
const [volume, setVolume] = useState(50); <input type="range" min={0} max={100} value={volume} onChange={e => setVolume(Number(e.target.value))} /> const [color, setColor] = useState('#ff0000'); <input type="color" value={color} onChange={e => setColor(e.target.value)} /> const [date, setDate] = useState('2024-01-01'); <input type="date" value={date} onChange={e => setDate(e.target.value)} /> // value is always YYYY-MM-DD string
Accessibility Best Practices
// Always associate labels with inputs <label htmlFor="email">Email</label> <input id="email" type="email" /> // Use fieldset + legend for groups <fieldset> <legend>Notification preferences</legend> <label><input type="checkbox" name="email" /> Email</label> <label><input type="checkbox" name="sms" /> SMS</label> </fieldset> // aria attributes for custom validation <input aria-required="true" aria-invalid={!!error} aria-describedby={error ? 'err' : undefined} /> {error && <span id="err" role="alert">{error}</span>}