React Cheatsheet

Forms

Use this React reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

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