React

Complete React cheat sheet covering components, JSX, all hooks, state management, performance optimization, and common patterns.

15 sections24 cards

React is a UI library. Its one job: keep the DOM in sync with your state. You describe what the UI should look like for a given state, React figures out what changed and updates the DOM efficiently.

Core loop: state changes → React re-renders affected components → React diffs the new virtual DOM against the previous one (reconciliation) → only real DOM changes are applied (commit phase).

Everything is a component. Components are just functions that take props and return JSX. React calls these functions during render. If state or props change, the function runs again.

Rendering ≠ painting. Rendering means React calling your component function. Painting means the browser updating pixels. React can render without anything visually changing.

JSX is not HTML. It compiles to React.createElement() calls. Rules that differ from HTML:

className instead of class. htmlFor instead of for.

style takes an object: style={{ color: "red", fontSize: 16 }} — camelCase properties, numbers for px values.

All tags must be closed: <img />, <input />, <br />.

Must return a single root element. Use <Fragment> or <>...</> to group without adding a DOM node.

JS expressions in curly braces: {variable}, {fn()}, {condition ? "yes" : "no"}. Statements (if, for) don't work directly — use ternary or move logic outside JSX.

Render lists with .map(). Each item needs a unique key prop — helps React identify which items changed. Keys must be stable, unique among siblings. Don't use array index as key if list can reorder.

Comments: {/* comment */}

Conditional rendering: {condition && <Component />} — watch out: {0 && ...} renders 0, not nothing. Use {!!count && ...} or ternary.

component basics

Function component — a JS function that returns JSX. Name must start with uppercase (React uses this to distinguish components from DOM tags).

Props — read-only data passed from parent. Destructure in params: function Card({ title, onClick }).

Default props: function Btn({ size = "md" })

children prop — whatever is between the opening and closing tags of the component. <Card>content here</Card>props.children.

Props spreading: <Input {...inputProps} /> — pass all properties of an object as props.

Never mutate props — they flow down one direction only (parent → child).

component patterns

Controlled component — input value driven by state. React is the source of truth.

Uncontrolled component — DOM manages its own state. Access via ref.

Compound components — a set of components designed to work together (e.g. <Select><Option /></Select>). Share state via context.

Render props — pass a function as prop that returns JSX. Shares logic between components. Pattern mostly replaced by custom hooks.

HOC (Higher Order Component) — function that takes a component, returns enhanced component. withAuth(Component). Still common in some libraries.

Lifting state up — when siblings need shared state, move it to the closest common ancestor and pass down as props.

const [state, setState] = useState(initialValue)

setState(newValue) — replaces state. setState(prev => prev + 1) — functional update, uses current value. Always use functional form when new state depends on old state (especially in async contexts or batched updates).

State updates are asynchronous — setState doesn't immediately update the variable. The new value is available on the next render.

React batches multiple setState calls in event handlers into one render. In React 18, batching also happens in async code (timeouts, promises).

For objects/arrays: state is replaced, not merged. Spread to preserve: setState(prev => ({ ...prev, key: newVal })).

Don't mutate state directly — state.push(item) won't trigger re-render. Always create a new reference.

useState(fn) — lazy initialization. Function runs only on first render. Use for expensive initial computation.

useEffect(setup, dependencies) — run side effects after render. Fetching data, subscriptions, DOM manipulation, timers.

Dependency array controls when it runs:

useEffect(fn) — no array. Runs after every render. Rarely what you want.

useEffect(fn, []) — empty array. Runs once after first render (mount). Like componentDidMount.

useEffect(fn, [a, b]) — runs when a or b changes.

Cleanup: return a function from setup. Runs before the next effect runs and on unmount. Use to cancel subscriptions, clear timers, abort fetches.

In React 18 strict mode (dev only), effects run twice on mount — to catch cleanup bugs. Your cleanup function must properly undo the setup.

Don't put async functions directly in useEffect. Create async function inside and call it: useEffect(() => { const fn = async () => { ... }; fn(); }, [])

If something doesn't need to be in sync with render, it doesn't need useEffect. Data fetching → use a library (TanStack Query). Computed values → derive during render, no effect needed.

const ref = useRef(initialValue) — returns a mutable object { current: initialValue }. Changing ref.current does NOT trigger re-render. Persists across renders.

Two main uses:

1. DOM access — <input ref={ref} />ref.current is the DOM element. Use for focus, scroll, measuring, integrating with non-React libraries.

2. Mutable value that survives renders without causing them — storing previous state, timer IDs, abort controllers, flags like "is this the first render".

useImperativeHandle(ref, () => ({ focus, reset })) — with forwardRef, expose specific methods to parent instead of the whole DOM node.

forwardRef(fn) — wrap component to accept a ref from parent. Needed when parent needs to access a child's DOM node.

useMemo

const result = useMemo(() => expensiveCalc(a, b), [a, b])

Memoizes the return value. Recomputes only when dependencies change. Use for expensive calculations that would be wasteful to redo every render.

Also use to preserve referential equality of objects/arrays passed as props — prevents child re-renders that only happen because the parent created a new object reference.

Don't overuse. Memoization itself has a cost. Only add when you can measure a problem.

useCallback

const fn = useCallback(() => doThing(a), [a])

Memoizes the function itself. Returns the same function reference across renders unless dependencies change.

Why it matters: functions defined in a component body are recreated every render. If you pass them as props to memoized children (React.memo), a new function reference causes unnecessary re-renders.

Use with: React.memo wrapped children, dependency arrays of other hooks, event handlers passed down deep trees.

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

const [state, dispatch] = useReducer(reducer, initialState)

Alternative to useState for complex state logic. Reducer is a pure function: (state, action) => newState. Dispatch sends actions to the reducer.

When to use over useState: state has multiple sub-values that change together, next state depends on previous in complex ways, logic is complex enough to test separately, state transitions need to be explicit and named.

dispatch({ type: "INCREMENT", payload: 1 }) — action is typically an object with a type string.

Lazy init: useReducer(reducer, initialArg, initFn)initFn(initialArg) computes initial state. For expensive initialization.

useReducer + Context = lightweight Redux. Good enough for most apps without a state management library.

Context lets you pass data deep through the component tree without prop drilling.

Create: const ThemeContext = createContext(defaultValue)

Provide: <ThemeContext.Provider value={theme}>...</ThemeContext.Provider> — all descendants can access this value.

Consume: const theme = useContext(ThemeContext) — reads the nearest Provider's value above in the tree.

When the Provider's value changes, ALL consumers re-render. To prevent this, memoize the value: const value = useMemo(() => ({ user, logout }), [user])

Split contexts by how often they change — don't put everything in one context. A frequently-changing value will trigger re-renders on all consumers even if they only need the stable part.

Good for: theme, locale, auth user, feature flags. Not a replacement for all state management — local state still belongs in components.

A custom hook is just a function that starts with use and calls other hooks inside. Extracts and reuses stateful logic between components without changing component hierarchy.

Common custom hooks to know how to build: useLocalStorage, useDebounce, useFetch, useToggle, usePrevious, useMediaQuery, useClickOutside, useEventListener.

Rules of hooks apply inside custom hooks too. Custom hooks can return anything — state, functions, refs, objects.

Key insight: the logic is shared, not the state. Each component that calls the hook gets its own independent state instance.

1. Only call hooks at the top level — not inside loops, conditions, or nested functions. React depends on the call order being the same every render to associate state with the right hook.

2. Only call hooks from React function components or custom hooks — not plain JS functions, class methods, event handlers outside components.

The eslint-plugin-react-hooks enforces these automatically. Set it up and trust it.

If you feel the urge to put a hook inside a condition, the condition itself probably needs to be inside the hook, or you need to restructure your component.

React.memo

export default React.memo(MyComponent) — wraps component. Skips re-render if props haven't changed (shallow comparison).

Only helps if the parent renders frequently but the child's props are stable. If props change every render, memo does nothing.

Custom comparison: React.memo(Comp, (prevProps, nextProps) => areEqual) — return true to skip re-render.

Combine with useCallback for function props and useMemo for object props — otherwise new references break memo.

code splitting

const Comp = lazy(() => import("./Comp")) — dynamic import. Component loads only when needed.

Wrap with <Suspense fallback={<Spinner />}> — shows fallback while component loads.

Split by route — each page loads its own bundle. Most impactful split.

Split heavy components — rich text editors, charts, maps — don't load them until needed.

avoiding re-renders

State should live as close to where it's used as possible — don't lift state higher than needed.

Colocate state — if only one component uses a piece of state, keep it there, not in a parent.

Children as props — <Parent><Child /></Parent> — Child doesn't re-render when Parent's state changes because Child's reference is stable (defined by Parent's parent).

Avoid creating objects/arrays/functions inline in JSX — they're new references every render.

useTransition — mark state updates as non-urgent. UI stays responsive while expensive update processes in background.

useDeferredValue(value) — defer re-rendering with a stale value. Show previous result while computing new one.

lists performance

Keys must be stable and unique — never use Math.random() as key.

Virtualization — for very long lists (1000+ items), render only visible items. Use react-window or react-virtual.

Pagination or infinite scroll — load data in chunks instead of all at once.

Memo individual list items — React.memo(ListItem) — so only changed items re-render when list updates.

fetch in useEffect (basic)

Pattern: loading state + error state + data state. Fetch in useEffect, set all three.

Cleanup: use AbortController to cancel in-flight requests when component unmounts or deps change. Prevents setting state on unmounted component.

Race conditions: if user triggers multiple requests, only the last response should win. AbortController handles this.

Manual fetch in useEffect gets messy fast — caching, refetching, deduplication all need to be built manually.

TanStack Query (recommended)

useQuery({ queryKey: ["users"], queryFn: fetchUsers }) — handles loading, error, caching, background refetch, stale-while-revalidate automatically.

useMutation — for POST/PUT/DELETE. Gives mutate(data) function. Handle onSuccess to invalidate queries.

queryClient.invalidateQueries(["users"]) — mark cached data as stale, triggers refetch.

Handles: caching, deduplication, background updates, pagination, optimistic updates, request retry.

This is the standard in the ecosystem. Know the basics even if you haven't used it.

React uses synthetic events — a cross-browser wrapper around native events. Same API everywhere.

Pass handler reference, not a call: onClick={handleClick} not onClick={handleClick()}. The second one calls the function immediately on render.

Pass arguments: onClick={() => handleDelete(id)} — arrow function in JSX is fine for this, though it creates a new function each render.

e.preventDefault() — prevent default browser behavior (form submit, link navigate).

e.stopPropagation() — stop event bubbling up.

Controlled form: onChange updates state, value is bound to state. Every keystroke triggers a re-render.

Common events: onClick, onChange, onSubmit, onKeyDown, onFocus, onBlur, onMouseEnter, onMouseLeave, onScroll.

state gotchas

State updates are batched — multiple setState calls in one event handler cause one re-render, not multiple.

Stale closures — a callback captures the value of state at the time it was created. If state updates, old callbacks still reference the old value. Fix: use functional update form, or include value in dep array.

Setting state in render — causes infinite loop. Never call setState directly in the component body.

Object/array state — React checks reference equality. setState(state) with the same reference won't trigger re-render. Must create new reference.

useState initial value only used once — passing a new value as initial doesn't reset state on re-render. Use key prop to reset, or useEffect to sync.

useEffect gotchas

Missing dependencies — lint warning for a reason. Stale values inside effect without them in deps array.

Object/function in deps — new reference every render = effect runs every render. Memoize them first.

Don't fetch data without cleanup — unmounted component setState warning.

Infinite loop: effect sets state → re-render → effect runs again → sets state. Fix: check if value actually changed before setting state, or fix dep array.

useEffect is not for derived state — compute during render instead.

rendering gotchas

key on wrong element — putting key on a wrapper div instead of the component itself.

Changing key = destroy and remount component. Useful to reset state intentionally.

Conditional hook calls — breaks hook rules. React will throw in dev.

Direct DOM manipulation — bypasses React. Use refs carefully.

Returning null from component — valid. Component renders nothing but still mounts.

Fragments don't accept className or style — use a real element if you need those.

things to know cold

React 18: automatic batching, concurrent rendering, useTransition, useDeferredValue, Suspense for data.

Reconciliation uses the key prop to match elements across renders. Same type + same key = update. Different type or key = unmount old, mount new.

Strict Mode — double invokes render and effects in dev to surface bugs. Not a problem in production.

Portals — render component into a different DOM node: ReactDOM.createPortal(children, domNode). Used for modals, tooltips.

Error boundaries — class component with componentDidCatch. Catches render errors in children. Show fallback UI. No hook equivalent yet (use react-error-boundary).