React · cheat sheet

React

React 19 with function components: rendering and reconciliation, state, effects, forms and Actions, memoization, Suspense, Server Components and traps.

The React facts interviewers keep coming back to, for React 19 and function components; every hook in detail lives in the React Hooks sheet.

Mental model & JSX

  • UI = f(state): a component maps props and state to JSX. You declare what the screen should show; React works out the DOM operations.
  • Rendering must be pure: same inputs, same JSX, no side effects. React may call a component more than once, pause it or discard the result.
  • Side effects belong in event handlers first, and in effects only to sync with something outside React.
  • JSX compiles to jsx() calls (the automatic runtime, required since React 19). Capitalized tags are components; lowercase tags are DOM elements.
JSX rule Example
One root per return wrap siblings in <>…</>
Close every tag <img src={src} alt="" />
camelCase props className, htmlFor, onClick; aria-* and data-* keep dashes
style is an object style={{ fontSize: 14 }}
{} takes expressions ternary, &&, .map(); no if/for inside
Renders nothing null, undefined, true, false (but 0 and NaN do render)
Text is escaped {userInput} can’t inject HTML

Components, props & state

  • Props are read-only inputs: function Avatar({ user, size = 40 }). React 19 ignores propTypes and drops defaultProps for function components (use default parameters).
  • children is what sits between the tags; slot props (<Layout sidebar={<Nav />}>) generalize it. Children talk to parents through callbacks (onSelect); siblings share state by lifting it up to their closest common parent.
  • key isn’t a prop (the child can’t read it). ref is a plain prop in React 19, so forwardRef is no longer needed.
  • State is a per-instance snapshot: the setter schedules a re-render, and the variable keeps its value for the rest of the current render.
  • Automatic batching (React 18+): updates in one event, timeout or promise callback cause a single re-render (flushSync from react-dom opts out).
  • Use the updater form when the next value depends on the previous one; useState(() => build()) runs the initializer once.
  • An Object.is-equal value bails out. Update immutably ({ ...user, name }, [...items, item], filter, map): mutating and passing the same reference doesn’t re-render.
  • Derive what you can during render (full names, filtered lists), and don’t copy props into state: useState(props.x) reads x only once.
function Counter() {
  const [n, setN] = useState(0);
  function handleClick() {
    setN(n + 1);         // replace with 0 + 1
    setN(n + 1);         // replace with 0 + 1 again
    setN((c) => c + 1);  // queued value (1) + 1
  }                      // one re-render, n becomes 2; inside the handler n is still 0
  return <button onClick={handleClick}>{n}</button>;
}
JavaScript

Rendering & reconciliation

  • Trigger → render (React calls components and diffs the output) → commit (applies only the DOM changes, attaches refs, runs layout effects) → browser paint → useEffect callbacks.
  • A component re-renders when its state changes, its parent re-renders (props changed or not) or a context it reads changes. New props are a consequence of the parent rendering, not a separate trigger.
  • Re-rendering isn’t a DOM update: identical output leaves the DOM untouched.
  • Reconciliation: a different element type at the same position unmounts the old subtree (state lost); the same type keeps its state and gets new props; list children are matched by key (keys note).
  • State belongs to a position in the tree. <Form key={userId} /> gives each user a fresh instance: the clean way to reset state.
  • Keys must be stable and unique among siblings (data ids). key={Math.random()} remounts everything on every render.
  • Index-key bug: prepend, sort or filter a list keyed by index and the state (input text, checkbox, focus) stays at its position while the data moves, so it appears on the wrong item. Index keys are safe only for static lists.
  • Fiber (React 16+) splits rendering into units of work, which lets React 18+ (createRoot) interrupt a non-urgent render for something urgent.

Hooks at a glance

Hook Use it for
useState / useReducer local state / many actions in a pure reducer
useContext / use read context; use also reads a promise and may be called conditionally
useRef a mutable box or DOM node that doesn’t re-render
useEffect / useLayoutEffect sync with outside systems / measure the DOM before paint
useMemo / useCallback cache a value / a function between renders
useTransition / useDeferredValue mark an update non-urgent / render a lagging copy
useActionState / useOptimistic Action result + pending flag / temporary optimistic UI
useFormStatus (react-dom) pending state of the parent <form>
useId ids for htmlFor and aria-* (not keys)
useSyncExternalStore subscribe to a store outside React
useEffectEvent (19.2) read the latest values in an effect without adding deps
  • Rules of Hooks: call them only at the top level of components and custom hooks; never in conditions, loops, nested functions or after an early return. React matches hook state by call order.
  • A custom hook (useSomething) shares logic, not state: each call has its own. Lint with eslint-plugin-react-hooks.

Effects & refs

  • An effect synchronizes with an external system: network, subscriptions, timers, browser APIs, non-React widgets.
  • Deps: none → after every render; [] → after mount; [a, b] → when either changes (Object.is). List every reactive value the effect reads.
  • Cleanup runs before the next run and on unmount. The setup can’t be async: declare an async function inside and call it.
  • Effects run only on the client, after commit; when not caused by an interaction, React generally lets the browser paint first. useLayoutEffect runs before paint (and blocks it).
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser)
    .catch((err) => { if (err.name !== 'AbortError') setError(err); });
  return () => controller.abort(); // a stale response can't overwrite a newer one
}, [id]);
JavaScript
You might not need an effect to… Instead
compute a value from props/state calculate it during render (useMemo if slow)
reset state when a prop changes render with a key
react to a click do it in the event handler
notify the parent call its callback in the same handler
subscribe to a store useSyncExternalStore
fetch page data framework loaders, Server Components, TanStack Query
  • Refs: useRef(null) returns { current }, which persists across renders; changing it never re-renders. Use it for DOM access (ref.current.focus()), timer ids, previous values.
  • Don’t read or write ref.current during render; DOM refs are null until commit. React 19 ref callbacks may return a cleanup.

Forms & Actions

Controlled Uncontrolled
Value lives in state (value + onChange) the DOM (defaultValue, defaultChecked)
Read it any time, every keystroke on submit, via FormData or a ref
Good for live validation, formatting, dependent fields simple forms; file inputs are always uncontrolled
  • value without onChange gives a read-only field. A value going from undefined to a string flips uncontrolled to controlled (warning): start with ''.
  • Form Actions (19): <form action={fn}> calls fn(formData) inside a Transition, always as POST, and resets uncontrolled fields after success.
  • useActionState(action, initial) returns [state, formAction, isPending]; the action gets (prevState, formData). useFormStatus() works only in a component rendered inside the form.
  • With Server Components, action can be a Server Function ('use server'), so the form can work before JavaScript loads.
async function subscribe(prev, formData) {
  const res = await api.subscribe(formData.get('email'));
  return res.ok ? { error: null } : { error: res.message };
}
function Newsletter() {
  const [state, formAction, isPending] = useActionState(subscribe, { error: null });
  return <form action={formAction}>
    <input name="email" type="email" required />
    <button disabled={isPending}>Subscribe</button> {state.error}
  </form>;
}
JavaScript

Context & memoization

  • createContext(defaultValue), provide with <ThemeContext value={v}> (19; .Provider still works), read with useContext or use. The default applies only when no provider is above.
  • Every consumer re-renders when the value changes (Object.is), even inside memo. A provider value like { user, setUser } is a new object each render: useMemo it, split state and dispatch into two contexts, or move the provider down.
  • Context is dependency injection, not a state manager: fine for theme, locale, current user; poor for fast-changing values.
Tool Caches Pays off when
memo(Comp) render output; skips if every prop is Object.is-equal a slow child re-renders with the same props
useMemo(fn, deps) a computed value the work is expensive, or the object feeds a memo child or deps
useCallback(fn, deps) a function it feeds a memo child or a dependency array
  • Wasted if the child isn’t memoized or another prop (inline object, children JSX) changes every render; a memo child still re-renders for its own state and context (memo note).
  • Cheaper fixes first: move state down, or lift content up by passing JSX as children.
  • React Compiler (v1.0 stable since October 2025): a build-time plugin that memoizes automatically when code follows the Rules of React, making most manual memoization unnecessary.

Suspense, errors & portals

  • <Suspense fallback={…}> shows the fallback while a child suspends: a lazy component, use(promise), or a Suspense-enabled framework or library. Fetching in an effect doesn’t suspend.
  • lazy(() => import('./Settings.js')): declare at module top level; the module must default-export a component. Split by route first.
  • A Transition keeps visible content instead of flashing a fallback; nested boundaries reveal progressively, and streaming SSR sends HTML per boundary.
  • Error boundaries are still classes (static getDerivedStateFromError for the fallback, componentDidCatch to log), or use react-error-boundary. Change the key to reset one.
  • They miss errors in event handlers, async code (setTimeout), SSR and the boundary itself; errors thrown in a startTransition function are caught. React 19 adds onCaughtError/onUncaughtError to createRoot.
const Settings = lazy(() => import('./Settings.js'));
function App() {
  return (
    <ErrorBoundary fallback={<p>Something went wrong</p>}>
      <Suspense fallback={<Spinner />}>
        <Settings />
      </Suspense>
    </ErrorBoundary>
  );
}
JavaScript
  • Portals: createPortal(children, domNode) (react-dom) renders elsewhere in the DOM (modals, tooltips) but stays in the React tree: context works and events bubble to React ancestors.
  • Fragments: lists need <Fragment key={id}>, as <> takes no key. React 19.3 adds <Fragment ref>.
  • StrictMode (development only): renders twice, adds a setup+cleanup cycle to effects on mount, re-runs ref callbacks and flags deprecated APIs. An effect “running twice” exposes a missing cleanup.

Concurrent & Server Components

  • const [isPending, startTransition] = useTransition(): startTransition(() => setTab('posts')) makes the update interruptible, so typing stays responsive. Since 19 the function may be async (an Action); updates after an await need another startTransition. Not for controlled text inputs.
  • useDeferredValue(value) renders a lagging copy, for when you don’t own the setter. Unlike debouncing there’s no fixed delay and the background render is interruptible.
  • <Activity mode="hidden"> (19.2) hides a subtree but keeps its state; its effects are cleaned up until it’s shown again.
function Search({ items }) {
  const [query, setQuery] = useState('');
  const deferred = useDeferredValue(query);
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      {/* SlowList is memo()ized, so it only re-renders when `deferred` changes */}
      <SlowList items={items} query={deferred} stale={query !== deferred} />
    </>
  );
}
JavaScript
  • Server Components (in an RSC framework such as the Next.js App Router) run only on the server or at build time: they can be async and read the database directly, add no client JavaScript, and can’t use state, effects or event handlers.
  • 'use client' at the top of a file marks a module boundary: that module and all its imports become client code. Client Components are still server-rendered to HTML, then hydrated.
  • Server-to-client props must be serializable (primitives, plain objects, arrays, Date, Map, promises, JSX, Server Functions); no plain functions or class instances. Pass Server Components into Client Components as children.
  • 'use server' marks Server Functions that the client can call, e.g. as a form action.
  • SSR vs RSC: SSR renders HTML for the first load; RSC is a component type whose code never ships.

State management choices

State Reach for Why
One component’s UI useState simplest; keep it close
Many actions, complex transitions useReducer pure, testable reducer
Shared by nearby components lift state up one source of truth
App-wide, rarely changing Context theme, locale, auth user
Large client state, many writers Redux Toolkit middleware, time-travel devtools
Global store, little boilerplate Zustand (or Jotai) selector subscriptions, no provider
Server data TanStack Query, SWR, framework loaders caching, dedupe, refetch, invalidation
Filters, tabs, pages URL search params shareable, survives reload

Patterns

  • Composition over inheritance: children and slot props; React never needs component inheritance.
  • Custom hooks extract stateful logic and replaced most HOCs and render props.
  • Compound components: <Tabs> owns the state and shares it via context with <Tabs.List>, <Tabs.Tab>, <Tabs.Panel>; the caller controls the markup.
  • HOC (withAuth(Page)) wraps to add props: prop collisions, wrapper hell. Render props (<Mouse render={(pos) => …} />) share state via a function prop: flexible but nests deeply.
function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id); // each change cancels the pending timer
  }, [value, delay]);
  return debounced;
}
JavaScript

Testing with Testing Library

  • Test behavior the user sees, not state or internals. Query priority: getByRole (with { name }) → getByLabelText → getByPlaceholderText → getByText → getByDisplayValue → getByAltText → getByTitle → getByTestId (last resort).
Variant No match One Several Async
getBy… throws returns throws no
queryBy… null returns throws no
findBy… rejects resolves rejects yes, retries (1 s default)
…AllBy… throws / [] / rejects array array as above
  • queryBy… asserts absence (.not.toBeInTheDocument()); findBy…/waitFor wait for things that appear later.
  • Prefer userEvent (real sequences: focus, keydown, input) over fireEvent; mock HTTP with MSW; test hooks with renderHook.
test('greets the user after submit', async () => {
  const user = userEvent.setup();
  render(<Greeting />);
  await user.type(screen.getByLabelText(/name/i), 'Ada');
  await user.click(screen.getByRole('button', { name: /submit/i }));
  expect(await screen.findByText('Hello, Ada')).toBeInTheDocument();
});
JavaScript

Performance & security

  • Measure first: React DevTools Profiler, a production build, Chrome performance tracks (19.2+).
  • Keep state local; pass JSX as children; stable keys; split contexts and memoize provider values.
  • memo what the profiler flags (or adopt the Compiler); virtualize long lists; lazy routes.
  • Heavy updates go in useTransition/useDeferredValue; avoid parent → child fetch waterfalls.
  • XSS: JSX escapes text and attributes. dangerouslySetInnerHTML={{ __html: html }} bypasses that: pass only trusted or sanitized HTML (DOMPurify.sanitize(html)).
  • React 19 blocks javascript: URLs in href/src; still allow-list protocols for user-supplied links.
  • Server Functions are public HTTP endpoints: validate input, check authorization. Keep React patched (CVE-2025-55182, a critical Server Components RCE, was fixed in 19.0.1, 19.1.2 and 19.2.1).
  • Everything a 'use client' module imports ships to the browser: keep secrets server-side.

Quick answers

  • Virtual DOM? The element tree React keeps in memory; it diffs new against old and applies minimal DOM changes.
  • Props vs state? Props come from the parent, read-only; state is the component’s own memory.
  • Why isn’t state updated right after setX? The setter schedules a re-render; this render keeps its snapshot.
  • What triggers a re-render? Own state change, parent re-render, or a change in a context it reads.
  • Why keys? To match list items between renders so each keeps its state and DOM node.
  • Controlled vs uncontrolled? State holds the value (value + onChange) vs the DOM does (defaultValue).
  • useEffect vs useLayoutEffect? Layout effects run before paint and block it; useEffect generally after paint.
  • memo vs useMemo vs useCallback? Skip a re-render / cache a value / cache a function.
  • useRef vs useState? Both persist; a ref change doesn’t re-render.
  • Context vs Redux? Context passes a value down and re-renders all consumers; stores add selectors, middleware, devtools.
  • Function error boundary? Not possible: one class, or react-error-boundary.
  • Hydration? hydrateRoot attaches React to server HTML; the first client render must match it.
  • SSR vs Server Components? SSR renders HTML then hydrates; Server Components never send their code.
  • Effect runs twice? StrictMode (dev only) re-mounts to expose missing cleanup.

Gotchas & traps

  • {count && <List />} renders 0: write {count > 0 && <List />}.
  • onClick={save()} calls save during render; pass save or () => save(id).
  • items.push(x); setItems(items) keeps the same reference: no re-render.
  • Stale closure: setInterval(() => setCount(count + 1), 1000) in a [] effect stops at 1; use setCount((c) => c + 1).
  • Object, array or function literals in deps re-run the effect every render.
  • A component declared inside another is a new type each render: it remounts and inputs lose focus.
  • A setter called unconditionally during render loops (“Too many re-renders”).
  • Date.now(), Math.random() or typeof window in render cause hydration mismatches.
  • key belongs on the outermost element returned from map.

Interview tip

“How would you speed this up?”: profile, move state down or lift content up, fix keys, split context, and only then add memo, useMemo and useCallback (or the React Compiler).

Practice next: React questions and the React quiz.

esc