★ · Part 1 · Beginner
The React cheat page
Everything worth having in front of you, on one page.
11On this page
Hooks, at a glance
| Hook | Use it for | Watch for |
|---|---|---|
useState | Value the UI renders | Updater form when the next value depends on the current |
useReducer | Several fields that move together | dispatch is stable — never needs a dependency |
useRef | DOM nodes; values that survive but do not render | Never read or write during render |
useEffect | Synchronising with something outside React | Most effects should not exist |
useLayoutEffect | Measure and adjust before paint | Blocks painting; warns during SSR |
useEffectEvent | Latest value inside an effect, without depending on it | Only callable from an effect in the same component |
useMemo | Expensive value; referential stability | Costs more than it saves for arithmetic |
useCallback | Stable function identity | Pointless unless the consumer is memoised or a dependency |
useTransition | Mark an update interruptible | Gives isPending; dim, do not blank |
useDeferredValue | Same, when the value is a prop | Renders twice by design |
useId | Linking label to input in a reusable component | Not for list keys |
useSyncExternalStore | State outside React | Snapshot must be referentially stable |
useContext | Reading ambient values | Every consumer re-renders on identity change |
useImperativeHandle | Exposing a small API instead of a node | Usually a prop would do |
The rules of hooks, and why
- Top level only. React matches hooks to state by call order, so a hook in a condition shifts every slot after it.
- From React functions only. Components or other hooks — there is no instance to attach to otherwise.
- The exception:
use()can be called conditionally, because it stores nothing in the hook list.
Rendering — the 30-second version
state changes → render (pure, interruptible) → diff → commit (sync) → paint → effects
- Each render is a snapshot.
countdoes not change mid-render. - Setting state schedules, it does not assign. Three
setCount(count + 1)add one. - Diffing is per type and per position. Different type at a position → unmount and remount, state lost.
- Children re-render by default when a parent does. Usually fine.
Keys
| What a key is | Item identity between renders — not a hint, not a perf trick |
| Index keys break when | The list reorders, or an item is removed from anywhere but the end |
| What breaks | State, focus, scroll and inputs are preserved on the wrong row |
| Deliberate use | key={userId} to reset a component's state on purpose |
| Unique | Among siblings only, not globally |
State: replace, never mutate
[...items, item] // add
items.filter((i) => i.id !== id) // remove
items.map((i) => (i.id === id ? { ...i, done: true } : i)) // update one
[...items].sort() // sort — copy first
{ ...user, address: { ...user.address, city } } // nested — spread each level
Mutating gives React the same reference back, so it concludes nothing changed and skips the render.
Which state goes where
| Kind | Home |
|---|---|
| From a server | A query cache — TanStack Query, or the framework's loader |
| Should survive refresh or a shared link | The URL |
| One component | useState, right there |
| A few nearby components | Lift to the closest common parent |
| Everywhere, changes rarely | Context |
| Everywhere, changes often | A store with selectors |
| Does not affect rendering | A ref |
Effects — the taxonomy
| You want to… | Use |
|---|---|
| Compute from props or state | Nothing. Calculate during render. |
| React to a click or submit | The event handler |
| Reset state when a prop changes | A key |
| Cache an expensive calculation | useMemo |
| Subscribe to an external store | useSyncExternalStore |
| Fetch data | A data library, or the server |
| Connect a socket, timer or browser API | an effect |
The bugs, and their one-line fixes
| Symptom | Cause | Fix |
|---|---|---|
A stray 0 on the page | {items.length && ...} | length > 0 && |
| Infinite render loop | Effect sets state, depends on an object made during render | Depend on a primitive |
| Counter stuck at 1 | Stale closure in an interval | Updater form, or useEffectEvent |
| Typed text jumps rows | Index keys | Stable ids |
| Input is read-only | value with no onChange | Add the handler |
| "changed uncontrolled to controlled" | useState() with no argument | Initialise to "" |
| Wrong data after fast navigation | Response race | AbortController in the cleanup |
| Child loses state every render | Component defined inside a component | Move it to module scope |
memo does nothing | An inline arrow or object prop | useCallback / useMemo, or move state down |
| Page reloads on submit | No preventDefault | Add it, on the <form> |
Server and client
Server Component no state, no effects, no handlers · can await · ships 0 JS
"use client" a doorway, not a label — everything it imports is client too
props across must serialise: no functions (except Server Actions)
"use server" a public HTTP endpoint — authenticate, authorise, validate
Performance, in order
- Profile with "why did this render" on. Do not guess.
- Move state down, or pass the expensive subtree as
children. - Virtualise past a few hundred rows.
- Code-split by route; find the one dependency that is a third of the bundle.
- Then memoise, exactly where the profiler pointed.
The accessibility minimum
- A real
<button>, not adivwithonClick. - Every control has an accessible name; a placeholder is not a label.
- Move focus on route change — client navigation does not reset it.
- Do not disable the submit button; explain the failure instead.
- Tab through the flow with the mouse away. Nothing else catches what that catches.
Opens in the editor: write it, run it, and check it against real tests.
All 46 for this level →Finished reading?
0 of 57 chapters read in this series