Cheat sheet · Senior Frontend Depth: React Internals, TypeScript & Testing

Senior Frontend Depth cheat sheet

The ideas senior frontend interviews test: how React renders, hooks and stale closures, concurrent React and Server Components, where state lives, advanced TypeScript, testing strategy and accessibility.

Read the full series →

blogs · jp-blogs.vercel.app/cheatsheets/series/frontend-depth

Render, commit, reconcileLesson 0 →

  • Render calls components (must be pure: same props, state, context gives same output, no side effects). Commit changes the DOM and runs effects.
  • Different element type at a position: subtree is destroyed, state lost. Same type, same position: updated, state kept.
  • State belongs to the position in the tree, not to the function. Use key to reset it on purpose.
  • Never define a component inside another component: new type every render, so it remounts.
Commit orderWhat runs
1DOM mutations, refs attached
2useLayoutEffect (sync, before paint)
3Browser paints
4useEffect (after paint)

State snapshots, batching, keysLesson 0 →

setCount(count + 1);
setCount(count + 1);   // +1 in total

setCount((c) => c + 1);
setCount((c) => c + 1); // +2 in total

State is a snapshot per render. Use the updater when the next value depends on the old one.

  • Batching (React 18+): updates in events, timeouts and promises give one render.
  • Keys must be stable and unique among siblings. Never Math.random().
  • Index keys tie state to a position: after a delete, input text jumps to the wrong row.

Rules of Hooks and effectsLesson 1 →

  • Hooks are matched by call order in a list. So call them at the top level only, never in if or loops.
  • useEffect syncs with something outside React (subscription, timer, widget, fetch). Return a cleanup.
  • Dependency array lists every reactive value the effect reads. [] runs on mount only. No array runs every render.
  • Objects, arrays and functions made in render are new each time, so they re-run the effect. Move them out or memoise.

Watch out: Do not switch off react-hooks/exhaustive-deps. A lie in the deps array is a stale closure.

Stale closuresLesson 1 →

useEffect(() => {
  const id = setInterval(() => {
    setCount((c) => c + 1); // updater
  }, 1000);
  return () => clearInterval(id);
}, []);

With [] and count + 1 the callback keeps seeing count from render 1.

  1. Functional update so you need not read the value.
  2. Honest dependencies: the effect re-runs when the value changes.
  3. Ref holding the latest value, or useEffectEvent (React 19.2): reads latest props and state, is not a dependency, call it only inside effects.

The other hooksLesson 1 →

useRef
Box { current }. Changing it does not render. DOM nodes, timer IDs
useLayoutEffect
Before paint. Measure DOM, avoid flicker. Blocks paint
useMemo
Remember a value until deps change. Speed hint only
useCallback
Remember a function. Needs React.memo child or effect dep
useReducer
Many related changes by rules. dispatch is stable
useId
Unique ID, same on server and client (labels, aria)
useSyncExternalStore
Read a store React does not own (3 args, incl. server value)
use (React 19)
Read a promise or context in render. Needs Suspense; callable in if
useActionState
[state, formAction, isPending] from an async action

You might not need an effectLesson 1 →

Instead of an effect for…Do this
Copying props to stateCompute it in render (useMemo if slow)
Reacting to a clickPut the work in the event handler
Resetting state on id changekey={id} on the component
Telling the parentCall onChange in the same handler
FetchingTanStack Query, SWR or Server Components; else clean up races

Tip: Effects are for code that runs because the component was shown, not because the user did something.

Concurrent React and SuspenseLesson 2 →

  • Urgent updates (typing, clicks) beat transitions. React can interrupt a transition render. Commit cannot be interrupted.
  • useTransition gives [isPending, startTransition]. It wraps the update. useDeferredValue wraps a value you receive.
  • Only updates before the first await inside startTransition are marked.
  • Suspense shows a fallback while children wait (lazy, use, async Server Components). Use several boundaries, pair with error boundaries.
AspectDebouncestartTransition
Fixed delayYesNo
InterruptibleNoYes
Use forTypeahead fetchesHeavy re-renders

Server Components and streamingLesson 2 →

Server ComponentClient Component
async, DB, files, secretsuseState, effects, onClick
Sends no component JSBundle ships; also server-rendered once
No state, effects, browser APIsBrowser APIs allowed
Can render Client ComponentsReceives Server Components as children
  • "use client" marks the boundary. The file and its imports join the bundle. Put it on interactive leaves.
  • Props across the boundary must be serialisable. No functions or class instances. Server Actions are the exception.
  • A Server Action ("use server") is a public endpoint: check auth and validate input.
  • Streaming sends the shell first, then Suspense parts. Selective hydration hydrates each boundary by priority.

Hydration mismatchesLesson 2 →

CauseFix
Date.now(), Math.random() in renderCompute on server and pass a prop, or set after mount
window, localStorage in renderRead in useEffect, or useSyncExternalStore
Bad HTML nesting (div in p)Fix the markup
Browser extensionssuppressHydrationWarning, sparingly

Watch out: A heavy library in a Client Component grows the bundle, hydration cost and server render time. Do heavy work in a Server Component, move the boundary down, lazy-load.

Where state livesLesson 3 →

KindHome
Local UI (open, tab)useState / useReducer
Server dataTanStack Query, SWR, RTK Query, Server Components
Where the user is (filter, page)The URL (searchParams)
FormReact Hook Form or TanStack Form + Zod
Shared, rare changeContext
Shared, frequent changeStore with selectors (Zustand, Redux Toolkit, Jotai)
  • Server state is a cache of something you do not own: key, dedupe, staleTime, invalidate. Do not copy it into Redux.
  • Model status as a union, not booleans. Colocate. One source of truth.

TypeScript essentialsLesson 4 →

unknown vs any
unknown must be narrowed. any switches checks off
T extends { id: string }
Constraint: needs id, keeps the exact type
K extends keyof T
Key must exist. Returns T[K]
satisfies
Checks a value, keeps its exact type
as const
Literal types and readonly
infer
Name a part inside a conditional type
UtilityEffect
Partial / RequiredAll optional / all required
Pick / OmitKeep / drop keys
RecordObject type with chosen keys
ReturnType / AwaitedFunction result / unwrap a Promise

Narrowing and DeepPartialLesson 4 →

switch (s.kind) {
  case "circle":
    return Math.PI * s.radius ** 2;
  case "rect": return s.width * s.height;
  default: {
    const _x: never = s; // forces a case
    return _x;
  }
}

Discriminated union with an exhaustive check.

type DeepPartial<T> =
  T extends (...a: never[]) => unknown ? T
  : T extends readonly (infer U)[]
    ? DeepPartial<U>[]
  : T extends object
    ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

Tip: Keep API types honest: one schema (Zod) for types and validation, or generate from OpenAPI. Never trust res.json().

Testing strategyLesson 5 →

  • Trophy: static checks, many integration tests, some unit tests, few E2E (login, checkout).
  • Query order: getByRole (with name), getByLabelText, getByText, getByTestId last. Use user-event.
  • getBy must exist now, queryBy may be absent (returns null), findBy waits.
  • Mock the network with MSW, not your own modules. Set onUnhandledRequest: "error". Use a custom render with providers.
  • Playwright locators and web-first assertions auto-wait. Log in once, reuse storageState.
  • Snapshots only for small output. Tests that break on every refactor test implementation, not behaviour.

Accessibility checklistLesson 6 →

  • Semantic HTML first: button for actions, a href for navigation, label for inputs. A div onClick has no focus, role or keys.
  • Every control needs an accessible name (label, text, aria-label). Decorative images use alt="". A placeholder is not a label.
  • Keyboard: logical Tab order, visible :focus-visible, no traps, Escape closes popups. Roving tabIndex in tabs.
  • Contrast (AA): 4.5:1 text, 3:1 large text and UI parts. Never colour alone. Respect prefers-reduced-motion.
  • Test with jsx-a11y, axe, a keyboard pass and a screen reader. Automation finds only part of the issues.
ARIAUse
aria-expanded / aria-controlsDisclosure, menu state
aria-describedby + aria-invalidField hint and error
role="status" / role="alert"Polite / urgent live announce

Modals and focusLesson 6 →

  1. Move focus into the dialog on open.
  2. Keep it inside (focus trap) and make the background inert.
  3. Close on Escape and with a visible button.
  4. Return focus to the trigger on close.
  5. Give it a name (aria-labelledby) and aria-modal.

Tip: Use native dialog with showModal() or Radix, React Aria, Headless UI. After a route change move focus to the new heading. After a failed submit, to the first error.

Interview one-linersLesson 0 →

QuestionShort answer
Why are index keys risky?State sticks to the position, not the item.
Why must hooks keep order?React finds state by call order.
Stale count in an interval?Closure over render 1. Updater, deps or ref.
Context wrong when?Fast-changing or large values: all consumers re-render.
unknown vs any?Must narrow vs checks off.
Hydration error cause?First client render differs from server HTML.
How is ARIA harmful?Announces what the element cannot do.
Why mock the network?Real code runs, tests survive refactors.

A cheat sheet is a reminder, not a teacher. Each card links to the lesson that explains it step by step.