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

ControlledUncontrolled
Value sourceReact stateDOM
Read valuevalue propref.current.value
ResetsetState('')ref.current.value = ''
ValidationOn every changeOn submit / blur
Use whenMost casesFile 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>
))}

Radio Buttons

const [size, setSize] = useState('md');

{['sm', 'md', 'lg'].map(s => (
  <label key={s}>
    <input
      type="radio"
      name="size"                       // same name groups them
      value={s}
      checked={size === s}
      onChange={e => setSize(e.target.value)}
    />
    {s}
  </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>}