React: Hooks Cheatsheet and Practice Quiz
Twelve React patterns behind most assessment questions: components, useState, immutable updates, effects, forms, memoisation and the Next.js App Router, with a 20-question practice quiz.
Syntax cheatsheet
12 patterns. Pick one on the map for its syntax, when to use it and the trap behind it.
Basics
Components and props
Functions that return UI
- Syntax
function User({ name, age = 0, children }) { return <h1>Hello {name} ({age}){children}</h1>; } <User name="David" age={33}>!</User>- When to use it
- Every piece of UI. Props flow from parent to child; a child reads its props and never edits them.
- What it does
- A component is a function of props that returns JSX. React calls it again whenever its props or state change. children is whatever sits between the opening and closing tags.
- Watch out
- Props are read-only. Component names must start with a capital letter or JSX treats them as HTML tags. Return one root element or a fragment <>…</>.
- Example
- function Card({ title, children }) { return ( <section className="card"> <h2>{title}</h2> {children} </section> ); }
Basics
Lists, keys and conditionals
Render collections
- Syntax
{users.map(u => <User key={u.id} user={u} />)} {loading ? <Spinner /> : <List />} {error && <p role="alert">{error}</p>}- When to use it
- Any array to display, and any UI that depends on state: loading, error, empty.
- What it does
- Keys let React match list items between renders so DOM nodes and state stay with the right item. Conditionals are plain JavaScript expressions inside braces.
- Watch out
- key={index} breaks when the list is reordered, filtered or has insertions: use a stable id. count && <X /> renders 0 when count is 0: write count > 0 &&.
- Example
- {items.length === 0 ? <Empty /> : <ul>{items.map(it => <li key={it.id}>{it.name}</li>)}</ul>}
State
useState and functional updates
Local state
- Syntax
const [count, setCount] = useState(0); setCount(count + 1); // uses this render's value setCount(prev => prev + 1); // uses the latest value- When to use it
- Any value that changes over time and should re-render the component: inputs, toggles, fetched data, counters.
- What it does
- useState returns the current value and a setter; calling the setter schedules a re-render with the new value. Several updates inside one event are batched into one render.
- Watch out
- Two setCount(count + 1) calls in one handler add 1, not 2, because count is a snapshot of this render: use the functional form. State belongs to the component instance, it is not shared between instances.
- Example
- function handleClick() { setCount(prev => prev + 1); setCount(prev => prev + 1); // +2 } const [form, setForm] = useState({ name: '', email: '' }); const [items, setItems] = useState(() => expensiveInitial()); // lazy initial state
State
Immutable state updates
Never mutate
- Syntax
setUser(prev => ({ ...prev, name: 'John' })); // change a field setUsers(prev => [...prev, newUser]); // add setUsers(prev => prev.filter(u => u.id !== id)); // remove setUsers(prev => prev.map(u => u.id === id ? { ...u, done: true } : u)); // edit one- When to use it
- Every object or array held in state.
- What it does
- React compares references to decide what changed; a new object or array signals an update, a mutated one looks identical and may be skipped.
- Watch out
- user.name = "x"; setUser(user) re-uses the same reference and may not re-render. push, splice, sort and reverse mutate; spread, concat, filter, map and toSorted do not.
- Example
- // wrong: same reference users.push(newUser); setUsers(users); // right: new array setUsers(prev => [...prev, newUser]); // nested field setState(prev => ({ ...prev, address: { ...prev.address, city } }));
Effects
useEffect and cleanup
Sync with the outside
- Syntax
useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); // cleanup }, []); // deps: once after mount useEffect(() => { fetchUser(userId); }, [userId]); // when userId changes- When to use it
- Subscriptions, timers, manual DOM APIs, fetching when an id changes. Not for deriving values from props or state: compute those during render.
- What it does
- The effect runs after the commit: with [] only after the first render, with [a, b] whenever a or b change, with no array after every render. The returned function runs before the next run and on unmount.
- Watch out
- A missing dependency reads stale values. Setting state unconditionally inside an effect that depends on that state loops forever. In development Strict Mode runs effects twice to test the cleanup.
- Example
- useEffect(() => { const ctrl = new AbortController(); fetch(`/api/users/${id}`, { signal: ctrl.signal }) .then(r => r.json()) .then(setUser) .catch(() => {}); return () => ctrl.abort(); }, [id]);
Effects
Fetch with loading and error
Three states
- Syntax
const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let alive = true; fetch('/api/users') .then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }) .then(data => { if (alive) setUsers(data); }) .catch(e => { if (alive) setError(e.message); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, []);- When to use it
- A component that loads its own data. In Next.js prefer fetching in a Server Component and passing the data down as props.
- What it does
- Loading, error and data are three explicit states and the component renders one of them. The alive flag ignores results that arrive after unmount or after the id has changed.
- Watch out
- fetch does not throw on 404 or 500: check r.ok. The effect callback itself cannot be async; define an async function inside it and call it.
- Example
- if (loading) return <p>Loading…</p>; if (error) return <p role="alert">{error}</p>; return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
Forms
Controlled inputs and forms
State drives the input
- Syntax
const [name, setName] = useState(''); <form onSubmit={e => { e.preventDefault(); save(name); }}> <input value={name} onChange={e => setName(e.target.value)} /> <button type="submit">Save</button> </form>- When to use it
- Validation while typing, dependent fields, resetting a form, anything where React must know the value at all times.
- What it does
- A controlled input shows the state value and reports every change, so state is the single source of truth. An uncontrolled input keeps its own value, read through a ref when needed.
- Watch out
- value without onChange makes the input read-only and React warns. Forgetting preventDefault reloads the page on submit. Checkboxes bind with checked, not value.
- Example
- const [form, setForm] = useState({ email: '', agree: false }); const onChange = e => { const { name, type, value, checked } = e.target; setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value })); }; <input name="email" value={form.email} onChange={onChange} /> <input name="agree" type="checkbox" checked={form.agree} onChange={onChange} />
State
Props vs state, lifting state up
Where data lives
- Syntax
function Parent() { const [query, setQuery] = useState(''); return ( <> <SearchBox value={query} onChange={setQuery} /> <Results query={query} /> </> ); }- When to use it
- Two siblings need the same value: move it to their closest common parent and pass it down with a setter callback.
- What it does
- Props are external input the parent owns; state is internal data the component owns. Lifting keeps one source of truth so the siblings can never disagree.
- Watch out
- Copying a prop into state (useState(props.value)) freezes it at mount. Do not store what you can compute: derive fullName from first and last during render.
- Example
- function SearchBox({ value, onChange }) { return <input value={value} onChange={e => onChange(e.target.value)} />; } const fullName = `${first} ${last}`; // derived, not state
Performance
useMemo, useCallback, React.memo
Skip repeated work
- Syntax
const sorted = useMemo(() => [...items].sort(byName), [items]); const onSelect = useCallback(id => setSelected(id), []); const Row = React.memo(function Row({ item, onSelect }) { /* … */ });- When to use it
- A calculation is measurably expensive, or a memoised child re-renders because it receives a new function or object on every render.
- What it does
- useMemo caches a value and useCallback a function until the dependencies change. React.memo skips re-rendering a component whose props are shallow-equal to last time.
- Watch out
- Memoising everything costs more than it saves: measure first. An inline object or arrow function prop defeats React.memo. The React Compiler does most of this automatically in new projects.
- Example
- // new object every render: Row re-renders anyway <Row style={{ color: 'red' }} /> // stable reference: hoist it or memoise it const style = useMemo(() => ({ color: 'red' }), []); <Row style={style} />
More hooks
useRef
A box that survives renders
- Syntax
const inputRef = useRef(null); <input ref={inputRef} /> inputRef.current.focus(); // in a handler or effect const timer = useRef(null); // mutable value, no re-render timer.current = setTimeout(fn, 500);- When to use it
- Focusing or measuring DOM nodes, holding timers, previous values or any mutable that must not trigger a render.
- What it does
- useRef returns the same { current } object on every render; changing current does not re-render, unlike state.
- Watch out
- Do not read or write ref.current during render (the React Compiler flags it): use it in handlers and effects. A ref is per instance, like state.
- Example
- useEffect(() => { inputRef.current?.focus(); }, []); const prev = useRef(value); useEffect(() => { prev.current = value; }, [value]); // previous value
More hooks
useContext and useReducer
Shared and complex state
- Syntax
const ThemeContext = createContext('light'); <ThemeContext value="dark"><App /></ThemeContext> const theme = useContext(ThemeContext); const [state, dispatch] = useReducer(reducer, initial); dispatch({ type: 'add', item });- When to use it
- Context for values many components need (theme, user, locale) without prop drilling. useReducer when several fields change together or an update depends on an action type.
- What it does
- Every consumer re-renders when the provided value changes. A reducer is a pure (state, action) => newState function, so updates are predictable and easy to test.
- Watch out
- A provider value created inline ({ user, setUser }) changes every render and re-renders all consumers: memoise it. Context is not a store; for frequent updates split contexts or use a state library.
- Example
- function reducer(state, action) { switch (action.type) { case 'add': return [...state, action.item]; case 'remove': return state.filter(i => i.id !== action.id); default: return state; } }
Next.js
Next.js App Router essentials
Server first
- Syntax
app/page.tsx // route: / app/users/[id]/page.tsx // dynamic: /users/42 app/layout.tsx // shared shell 'use client'; // first line: Client Component export function generateStaticParams() { return ids.map(id => ({ id })); } import { redirect } from 'next/navigation';- When to use it
- Server Components by default (data access, zero client bundle); "use client" only for state, effects, browser APIs and event handlers.
- What it does
- Folders under app/ define routes, [id] a dynamic segment, layout.tsx shared UI. generateStaticParams lists the dynamic pages to prerender; middleware runs request-time logic such as auth, rewrites and redirects.
- Watch out
- Secrets go in server-only env vars, never NEXT_PUBLIC_*. A Client Component cannot import a Server Component, but it can receive one as children. Use next/image for optimised images.
- Example
- // app/users/[id]/page.tsx (Server Component) export default async function Page({ params }) { const { id } = await params; const user = await db.user.find(id); // server only, never shipped return <Profile user={user} />; }
Pick a pattern on the map to see its syntax, when to use it, what it does and what to watch out for.
Practice quiz
20 multiple-choice questions drawn at random from the 38 in this track, one per screen, graded at the end. Every question points back to a pattern on the map.
Pick an answer, move freely between the questions and finish whenever you like: the score and the explanation of every question appear at the end.
- 20 questions
- No time limit
- Graded at the end
The quiz needs JavaScript. The cheatsheet above works without it.
Practice material for timed coding assessments. Behaviour described follows the React 19 and Next.js 16 App Router documentation.
