Cheat SheetsReactPatterns

Patterns — Cheat Sheet

React · 7 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Patterns
React7 topicsQuick revision reference
1

Custom Hooks

A custom hook extracts stateful logic so it can be reused and tested. It shares behaviour, never state — every caller gets its own copy.

  • A custom hook is a function starting with "use" that calls other hooks — that prefix is what the lint rules key on
  • Hooks are matched by call order, so they must never be called conditionally or inside a loop
  • Early returns go after all hook calls, never between them
  • Hooks share logic, not state — two callers get two independent copies
  • Extract when the pattern repeats or deserves its own test; wrapping a single useState is not worth the indirection
Two hooks you will write in every project
function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value)

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(id)
  }, [value, delay])

  return debounced
}

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    try { return JSON.parse(localStorage.getItem(key)) ?? initial }
    catch { return initial }
  })

  useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(value)) } catch {}
  }, [key, value])

  return [value, setValue]
}

// Return an array for a two-value tuple people will rename,
// an object when there are several named things:
return { data, isLoading, error, refetch }
2

Context — Sharing Without Prop Drilling

Context passes a value to a whole subtree without threading props through it. It is a delivery mechanism, not a state manager, and every consumer re-renders when the value changes.

  • Context delivers a value to a subtree; it is not a state manager and has no selectors
  • Every consumer re-renders when the provider value changes, so memoise the value object
  • Split contexts by concern and by update frequency, and separate state from actions
  • Passing rendered children removes much prop drilling without any context at all
  • Use context for rarely-changing shared values; use a store for high-frequency state many components slice differently
useMemo the value; guard the hook
const ThemeContext = createContext(null)

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('system')

  // Without useMemo this object is new on EVERY render of the
  // provider, so every consumer re-renders even if theme is the same
  const value = useMemo(
    () => ({ theme, setTheme, isDark: theme === 'dark' }),
    [theme],
  )

  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}

export function useTheme() {
  const ctx = useContext(ThemeContext)
  if (!ctx) throw new Error('useTheme must be used inside <ThemeProvider>')
  return ctx      // never undefined from here on
}

// Export the hook, not the context. Consumers then cannot use it
// outside a provider, and you can change the implementation freely.
3

Choosing a State Management Approach

Most applications need far less global state than they install. Separate server data from client state first, and the remaining global state is usually small enough for a light store.

  • Classify state as server, URL, local or global before choosing a tool — most of it is not global
  • A query library handles server data, which is the majority of what ends up in stores
  • A store's selectors let components subscribe to slices, which is exactly what context cannot do
  • Zustand-style stores need no provider and can be read outside React, which suits interceptors and tests
  • Redux Toolkit is worth its weight for complex cross-cutting state or an existing codebase, not for caching responses
Most "global state" is server state
// 1. SERVER state — fetched, cached, can change without you
//    -> TanStack Query. Not a store.
//    This is 70% of what people put in Redux.

// 2. URL state — filters, page, tab, search, selected id
//    -> useSearchParams. Shareable and refresh-proof for free.

// 3. LOCAL UI state — is this dropdown open, this input's draft
//    -> useState in the component that owns it

// 4. GLOBAL client state — session, theme, cart, sidebar collapsed
//    -> context for rarely-changing values
//    -> a store when it changes often or many components slice it

// Work through 1-3 honestly and 4 is often two or three values.

// The signal you actually need a store:
//   - many components read different SLICES of one changing object
//   - context re-renders are measurably hurting
//   - state must survive route changes and be updated from anywhere
4

memo, useMemo and useCallback

Memoisation trades memory and complexity for skipped work. Applied without measuring, it usually makes code slower to read and no faster to run.

  • memo skips a re-render, useMemo caches a value, useCallback caches a function identity
  • memo is defeated by a new object or arrow function prop — both halves are needed or neither helps
  • Justified cases: genuinely expensive computation, dependency identity, memoised expensive children, and context values
  • Confirm with the React DevTools Profiler rather than memoising on suspicion
  • Moving state down or passing children usually beats memoising, and React Compiler removes most manual cases
A component, a value, a function identity
// memo — skip re-rendering a component when its props are shallow-equal
const ProblemRow = memo(function ProblemRow({ problem, onOpen }) { ... })

// useMemo — cache a computed VALUE between renders
const stats = useMemo(() => computeStats(attempts), [attempts])

// useCallback — cache a FUNCTION identity between renders
const onOpen = useCallback((slug) => navigate(`/p/${slug}`), [navigate])

// The three work together or not at all. memo compares props with
// Object.is, so a new function or object prop defeats it:
const Row = memo(RowImpl)
<Row problem={p} onOpen={() => open(p.id)} />     // new arrow each render
                                                   // -> memo never skips

// Both halves are required: memo on the child AND stable props.
// One without the other is pure cost.
5

Error Boundaries

Without a boundary, one thrown error unmounts your entire application and leaves a blank white page. A boundary contains the damage to one part of the screen.

  • An uncaught render error unmounts the entire React tree, leaving a blank page
  • Boundaries are still class components — getDerivedStateFromError renders the fallback, componentDidCatch logs
  • They do not catch event handlers, async code, timeouts or SSR errors, which need their own handling
  • Layer them: one around the app, one per route, and one around each independent widget
  • The fallback must offer a way forward, and boundaries should reset on navigation
getDerivedStateFromError and componentDidCatch
class ErrorBoundary extends Component {
  state = { error: null }

  static getDerivedStateFromError(error) {
    return { error }                       // render the fallback
  }

  componentDidCatch(error, info) {
    reportError(error, { componentStack: info.componentStack })  // log it
  }

  render() {
    if (this.state.error) {
      return this.props.fallback?.(this.state.error, () => this.setState({ error: null }))
        ?? <DefaultErrorFallback />
    }
    return this.props.children
  }
}

// react-error-boundary wraps this with a friendlier API, including
// a reset that also resets the subtree:
<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onReset={() => queryClient.resetQueries()}
  resetKeys={[location.pathname]}          // auto-reset on navigation
>
6

Portals, Modals and Overlays

A portal renders a child into a different DOM node while keeping it in the React tree. It is how modals escape overflow and stacking contexts — and it is the easy half of building one.

  • A portal changes where a child renders in the DOM while keeping it in the React tree, so context and event bubbling still work
  • Portals free overlays from ancestor overflow, transform and z-index stacking contexts
  • A usable modal needs focus move, focus trap, focus restore, Escape, scroll lock and correct ARIA — the portal is the easy part
  • Use a headless library for dialogs rather than rebuilding focus trapping in every project
  • Layer overlays with z-index tokens from a single portal root; escalating numbers indicate a stacking-context problem
DOM elsewhere, React tree unchanged
import { createPortal } from 'react-dom'

function Modal({ children, onClose }) {
  return createPortal(
    <div className="backdrop" onClick={onClose}>
      <div role="dialog" aria-modal="true" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body,          // where it renders
  )
}

// DOM position: document.body — free of any ancestor's overflow,
// transform, filter or z-index stacking context.
// React position: still a child of the component that rendered it,
// so context works and events bubble to the React parent.

// That last part surprises people: a click inside the portal fires
// the onClick of the React ancestor, even though the DOM nodes are
// nowhere near each other.

// Portals are also right for tooltips, dropdowns, popovers and
// toasts — anything that must not be clipped by its container.
7

Designing a Component API

Compound components, render props and headless hooks are three answers to the same question: how flexible should this component be, and who decides how it looks?

  • A growing list of boolean props signals that the component should be composed rather than configured
  • Compound components share state implicitly through a private context, so callers just assemble parts
  • A headless hook provides behaviour with no markup, which is why headless libraries dominate for complex widgets
  • Spread remaining props and forward the ref so callers can pass className, aria-* and measure the node
  • Support both controlled and uncontrolled usage where it makes sense, and name props for meaning not appearance
Compound components over configuration flags
// Version 1
<Card title="Two Sum" />
// Eighteen months later
<Card title="Two Sum" showIcon hideFooter compact variant="pro"
      headerRight={<Badge />} bodyClassName="p-0" noBorder collapsible />
// Nobody knows which combinations were ever tested.

// Compound components — the caller assembles the parts
<Card>
  <Card.Header>
    <Card.Title>Two Sum</Card.Title>
    <ProBadge />
  </Card.Header>
  <Card.Body><Description /></Card.Body>
  <Card.Footer><SolveButton /></Card.Footer>
</Card>

// Shared state stays implicit, via a private context
const TabsContext = createContext(null)
export function Tabs({ children, defaultValue }) {
  const [value, setValue] = useState(defaultValue)
  return <TabsContext.Provider value={{ value, setValue }}>{children}</TabsContext.Provider>
}
Tabs.List = TabsList; Tabs.Trigger = TabsTrigger; Tabs.Panel = TabsPanel
// The caller never wires value and onChange between the parts.
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/react