React Cheatsheet - Hooks, Props, State & Components
This reference is for React developers who write function components and Hooks. It moves from the state/effect core to advanced hooks such as useReducer, context, and useId, then component patterns like forwardRef and render props, before controlled forms, state via Context, and performance optimizations like React.memo and lazy. Each entry is a copy-ready snippet with the deps array and edge cases called out. After reading you should be able to drive async data with useEffect, share state across components with Context, and stop wasted re-renders with memo/useCallback.
Typical Use Case
For developers migrating from class components to function components, or inheriting a React codebase whose hooks usage grew organically. Code quality is decided less by how many hooks you know than by three things: where state lives, when effects run, and how re-renders converge. This sheet covers the core hook set (useState / useEffect / useRef / useMemo / useCallback / useReducer / useContext) plus the everyday patterns — conditional rendering, list keys, error boundaries, portals and lazy loading. Worth scanning before code review, asking of every component: should this state live higher up, and does this effect have a cleanup function?
Core Hooks 6
const [count, setCount] = useState(0)const [user, setUser] = useState({ name: "", age: 0 })useEffect(() => { fetchData() }, [])useEffect(() => { subscribe(id); return () => unsubscribe() }, [id])const ref = useRef(null)const memoized = useMemo(() => compute(a, b), [a, b])Advanced Hooks 6
const [state, dispatch] = useReducer(reducer, initialState)const value = useContext(MyContext)useLayoutEffect(() => { measure() }, [deps])useImperativeHandle(ref, () => ({ focus: () => {} }))const id = useId()const [isPending, startTransition] = useTransition()Component Patterns 6
function App({ name, children }) { return <div>{children}</div> }const Button = forwardRef((props, ref) => <button ref={ref} />)function withAuth(Component) { return (props) => <Component {...props} /> }function List({ render }) { return items.map(render) }const Memo = React.memo(Component)const Lazy = React.lazy(() => import("./Comp"))Events & Forms 5
<button onClick={(e) => handleClick(e)}>Click</button><input value={val} onChange={e => setVal(e.target.value)} /><form onSubmit={e => { e.preventDefault(); submit() }}>e.stopPropagation()e.preventDefault()State Management 5
setCount(prev => prev + 1)setUser(prev => ({ ...prev, name: "new" }))const ThemeContext = createContext("light")<ThemeContext.Provider value="dark">const value = useContext(ThemeContext)Performance 5
const MemoComp = React.memo(MyComponent)const value = useMemo(() => expensive(a, b), [a, b])const handler = useCallback(() => {}, [deps])<Suspense fallback={<Loading />}><Lazy /></Suspense>const [deferred] = useDeferredValue(value)Common Patterns 6
{isLoading ? <Loading /> : <Content />}{show && <Menu />}{items.map(item => <li key={item.id}>{item.name}</li>)}class ErrorBoundary extends React.Component { static getDerivedStateFromError(e) {} }ReactDOM.createPortal(<Modal />, document.body)<Comp {...defaultProps} {...props} />Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
useState(initial) | Declare component state, returns [currentValue, setter] | const [count, setCount] = useState(0) |
setX(prev => next) | Update based on the previous value, avoiding a stale closure | setCount(prev => prev + 1) |
useEffect(fn, deps) | Run a side effect after mount or when deps change; an empty deps array runs it once | useEffect(() => { fetchData() }, []) |
useRef(null) | Return a mutable object preserved across renders, for a DOM node or non-rendering data | const ref = useRef(null) |
useMemo(fn, deps) | Reuse the previous result while deps are unchanged, skipping expensive recomputation | const memoized = useMemo(() => compute(a, b), [a, b]) |
useCallback(fn, deps) | Memoize the callback itself so memoized children can skip re-rendering | const handler = useCallback(() => {}, [deps]) |
React.memo(Component) | Shallow-compare props and skip the child re-render when unchanged | const Memo = React.memo(Component) |
key={item.id} | Stable identity for list items; missing or index keys misalign state | {items.map(item => <li key={item.id}>{item.name}</li>)} |
<Suspense fallback={<Loading />}> | Fallback UI while a lazy component loads; must wrap React.lazy | <Suspense fallback={<Loading />}><Lazy /></Suspense> |
getDerivedStateFromError(e) | Static method of an error boundary that captures child render errors | class ErrorBoundary extends React.Component { static getDerivedStateFromError(e) {} } |
Common pitfalls
SymptomAfter deleting or reordering a list, items render the next item content or expand state.
CauseThe key was the array index, or missing entirely, so React reuses DOM by position and state follows position.
FixUse a stable unique identity: {items.map(item => <li key={item.id}>{item.name}</li>)}; if there is no id, add one in the data source.
SymptomA request inside useEffect fires twice and the dev log prints the request twice.
CauseStrictMode mounts, unmounts and remounts in development to surface side-effect bugs, so the effect runs twice.
FixFirst make the request idempotent or cancellable; the real fix is a cleanup path (AbortController or a returned cleanup function), not disabling StrictMode.
SymptomAn event callback always reads the state from its first render — the log shows stale data.
CauseThe callback was created during render and cached, so its closure captured that render state; later updates do not refresh the closure.
FixAdd the state to the dependency array, or use functional updates: setCount(prev => prev + 1) / setUser(prev => ({ ...prev, name: "new" })).
SymptomAfter unmount, timers or subscriptions keep running, with warnings about setting state on an unmounted component or growing memory.
CauseuseEffect registered a subscription or setInterval but returned no cleanup function.
FixReturn a cleanup function: useEffect(() => { subscribe(id); return () => unsubscribe() }, [id]), tearing down the old subscription before the new one.
SymptomMutating a field of an object or array state updates nothing on screen.
CauseThe original object was modified in place (state.count++ style), so the reference is unchanged and React skips the re-render.
FixAlways update immutably: setUser(prev => ({ ...prev, name: "new" })), or return a new array from the array method.
SymptomUsing useEffect as if it were a lifecycle hook produces several effects that undo each other.
CauseSide effects done during render instead of after commit, or one logical side effect split into overlapping registrations.
FixGroup effects by purpose (DOM read / subscription / request), give each its own cleanup, and use useLayoutEffect for DOM measurement.
Troubleshooting
1Check how many times a piece of logic actually runs
useEffect(() => { console.log("effect run") }, [])Log inside the effect; two runs in development is usually the StrictMode mount-unmount-remount cycle, harmless once cleanup is correct.
2Identify which render produced a stale closure
const handler = useCallback(() => {}, [deps])Every external value referenced by the callback belongs in deps; useCallback fixes the reference and React.memo lets children skip re-rendering.
3Narrow re-renders to what actually changed
const MemoComp = React.memo(MyComponent)Confirm wasteful re-renders with the React DevTools Profiler first, then add React.memo / useMemo / useCallback where the profile points.
4Minimal correct conditional and list rendering
{isLoading ? <Loading /> : <Content />}Use a ternary or short-circuit expression for conditional rendering; list rendering always needs a stable key.
5Make sure side effects are reclaimed on unmount
useEffect(() => { subscribe(id); return () => unsubscribe() }, [id])Every subscription, timer and request listener needs a matching cleanup; omitting it is where memory leaks begin.
Command Examples
Effect with cleanup that rebuilds the subscription when id changes
useEffect(() => {
subscribe(id);
return () => unsubscribe();
}, [id])The dependency array is a subscription manifest, not an optimization: when id changes from 1 to 2 React runs the previous cleanup before the new effect. Without return () => unsubscribe() the old subscription stays alive, so switching between ids stacks up connections — StrictMode surfaces this in development, while production just shows sporadic duplicate pushes.
Update from the previous value to avoid stale closures
setCount(prev => prev + 1)
setUser(prev => ({ ...prev, name: "new" }))Calling setCount(count + 1) three times in one handler reads the same render-time count three times and adds 1, not 3; setCount(prev => prev + 1) composes from the latest value. Object state has the same trap: setUser({ name: "new" }) replaces the whole object and drops age, so spread prev to keep the other fields.
Key list items by stable id, not by array index
{items.map(item => <li key={item.id}>{item.name}</li>)}
{items.map((item, i) => <li key={i}>{item.name}</li>)}key tells React which DOM node belongs to which piece of data. With the index as key, inserting at the head shifts every key, so React believes item 1 became item 2 and rewrites the existing node text in place — inputs keep stale text and animations go wrong. The index is only acceptable for lists that never reorder or change length and are purely presentational.
Controlled inputs: value and onChange come as a pair
const [val, setVal] = useState("")
<input value={val} onChange={e => setVal(e.target.value)} />
<form onSubmit={e => { e.preventDefault(); submit() }}>value without onChange makes the field impossible to type in — React raises "You provided a value prop to a form field without an onChange handler" — while onChange without value makes it uncontrolled. Submitting a form requires e.preventDefault(), otherwise the page reloads and the state is lost.
useMemo and useCallback only pay off together with React.memo
const memoized = useMemo(() => compute(a, b), [a, b])
const handler = useCallback(() => {}, [deps])
const Memo = React.memo(Component)
<Suspense fallback={<Loading />}><Lazy /></Suspense>When a child is wrapped in React.memo, the parent still passes a fresh props object and a fresh function on every render, so memo buys nothing. useMemo stabilizes the props and useCallback stabilizes the callbacks, and only then can memo block the re-render. Both need a complete dependency array: a missing dependency yields stale values and is far harder to diagnose than an extra one.
Common Pitfalls
- Stale closure is the hardest class of React bug: reading state inside useEffect or an event callback yields the value from that render, so the callback keeps using the old value after setState. The classic symptom is a setInterval or SSE callback stuck on the first id. Fix it by putting the value in the dependency array (useEffect([id])) or by using the functional setX(prev => ...) form.
- A missing useEffect dependency is a frequent defect source. The exhaustive-deps rule flags it, but it is often silenced with eslint-disable — and a missing dependency means the effect does not re-run when it must, subscribing to a stale callback reference, or rebuilding the timer on every render while only ever running it once. An incomplete array does more damage than an over-complete one.
- Using the array index as key (key={i}) makes React reuse the wrong DOM node when items are inserted, removed, or sorted: input contents shift between rows and component state attaches to the wrong element. Whenever data carries an id, use it; if none exists, generate one in the data layer before rendering.
- Forgetting return () => unsubscribe() / clearInterval / abort() leaves timers, subscriptions, and requests running after unmount: memory grows and the page issues duplicate requests after a few mounts. React.memo or conditional rendering that mounts and unmounts frequently amplifies the problem.
- Mutating a state object in place (user.name = "x"; setUser(user)) breaks React immutability: the reference is unchanged, React assumes nothing changed and skips the re-render, leaving stale text on screen. Always create a new reference for object and array state with prev => ({ ...prev, x }) or [...prev, item].
- Writing build-time environment variables as literals inside components causes a hydration mismatch whenever server and client read different values: the text differs during hydration, the console warns, and the page visibly flickers. Serve configuration from the server or read it once at module top level, instead of scattering env literals through components.
Tips
- An empty [] deps array runs the effect once on mount, like componentDidMount.
- useMemo/useCallback optimize by avoiding needless recompute and re-render.
- React 18 StrictMode double-invokes effects in dev to surface side-effect bugs.
- Suspense + React.lazy enables lazy loading and code splitting.
- Always give list items a stable key; avoid using array index.
FAQ
How do I choose between useRef and useState?
The test is whether the value has to drive the UI. If it must appear on screen and a change must cause a re-render, take the pair from const [count, setCount] = useState(0). If it is never displayed, or changes too often to be worth a render, use const ref = useRef(null): it hands back a mutable ref object that can either hold a DOM node so a parent can reach the real element, or hold a value that survives re-renders without triggering any. The usual split is that the controlled value of an input is useState, while a timer id, a scroll offset or a has-initialised flag belong in a ref.
What is the difference between useTransition and useDeferredValue?
Both postpone non-urgent work so that typing stays responsive, but they approach it from opposite ends. const [isPending, startTransition] = useTransition() is an explicit declaration: you wrap a state update in startTransition, isPending flips to true while the transition runs, and you can drive a spinner from it. The text input itself keeps updating at full speed. const [deferred] = useDeferredValue(value) is passive: you update value as normal and React renders an additional lagging copy of it, letting a secondary region such as a search result list settle a moment later while the input stays instant. One marks which update matters, the other derives a stale value for you.
When should I reach for forwardRef versus useImperativeHandle?
They solve the same underlying problem at different levels of control. const Button = forwardRef((props, ref) => <button ref={ref} />) is wholesale forwarding: the ref handed down by the parent is attached straight to an internal DOM element, so the parent holds the actual node and can focus it, measure it or style it. useImperativeHandle(ref, () => ({ focus: () => {} })) is a tighter contract: you decide what is exposed, and the object returned by the factory becomes the complete public surface seen through the ref, leaving the implementation free to change. Use forwardRef for plain pass-through, and useImperativeHandle when you need a curated method set or want to hide internals.
For cross-level data sharing, when should I reach for Context, a HOC, or props?
Choose by blast radius and how often the value changes. Passing children as props is the most local option — function App({ name, children }) { return <div>{children}</div> } wraps content so the data is visible only to that subtree, and a change re-renders just the direct children. A context built from const ThemeContext = createContext("light"), rendered with <ThemeContext.Provider value="dark"> and read via useContext, fits global values such as theme or language: readable at any depth, but every new Provider value re-renders every consumer. A higher-order component like function withAuth(Component) { return (props) => <Component {...props} /> } sits in between, best for injecting cross-cutting behaviour such as auth or logging rather than data alone.
Which errors does an error boundary catch, and which slip through?
The class form on this sheet — class ErrorBoundary extends React.Component { static getDerivedStateFromError(e) {} } — intercepts an error when it bubbles up during rendering and lets static getDerivedStateFromError set the error state. In scope are errors thrown by children while rendering, in their constructors, and in their lifecycle methods. Out of scope are exceptions inside event handlers, inside setTimeout or Promise callbacks, and inside the boundary itself, because none of those sit on React render stack. Server-side rendering errors and failures thrown while the boundary is rendering are likewise not caught. Async and callback failures have to be caught in the data layer and turned into state yourself.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us