# Frontend Interview Prep — Pattern-Recognition Edition

_Built for Nachiket Salvi. The spine of this guide is **pattern recognition**: for every problem, learn the signal that tells you which technique to reach for — then see it easy → hard._

**265 questions** across JavaScript, TypeScript, React, System Design, and DSA.

---

## How to Recognise the Technique (DSA Decision Table)

When you read a problem, match its **signal** to a row — that points you at the technique before you write a line of code.

| When you see (signal) | Reach for | Why it works | Example problems |
|---|---|---|---|
| Find pairs / counts / “have I seen this before?” and you're about to write a nested loop | **Hash Map / Set** | Trades O(n) space for O(1) lookups — collapses O(n²) to O(n). | Two Sum, Group Anagrams |
| Array is SORTED and you need a pair/triplet, or to dedupe/partition in place | **Two Pointers** | Move from both ends (or slow/fast) instead of re-scanning. | 3Sum, Container With Most Water, Valid Palindrome |
| Longest / shortest / max-sum CONTIGUOUS subarray or substring | **Sliding Window** | Grow the window on the right, shrink on the left — each element enters/leaves once. | Longest Substring Without Repeat, Min Size Subarray Sum |
| Many range-sum queries, or “subarray that sums to K” | **Prefix Sum (+ hash map)** | Precompute cumulative sums so any range is one subtraction. | Running Sum, Subarray Sum Equals K |
| “Generate ALL …” subsets / permutations / combinations / valid arrangements | **Backtracking (recursion)** | Build a choice, recurse, undo the choice. Prune invalid branches early. | Subsets, Permutations, Generate Parentheses, Combination Sum |
| SORTED data and you need find / insert-position / first-or-last in O(log n) | **Binary Search** | Halve the search space each step. Watch the boundary conditions. | Search Insert, Rotated Array, First/Last Position |
| Tree/graph — need level-by-level, or the FEWEST steps / shortest path | **BFS (queue)** | Explores in rings of increasing distance, so the first time you reach a node is the shortest. | Level Order, Rotting Oranges |
| Tree/graph — explore fully, connected components, or any path exists | **DFS (recursion / stack)** | Go deep first; natural fit for recursion and 'flood fill'. | Number of Islands, Max Depth, Path Sum |
| Matching pairs, nearest-greater/smaller element, or an undo/back stack | **Stack / Monotonic Stack** | LIFO matches nesting; a monotonic stack answers 'next greater' in O(n). | Valid Parentheses, Daily Temperatures |
| Top-K / K-th largest-or-smallest / merge K sorted / streaming median | **Heap (priority queue)** | Keep only K elements ordered — O(n log k) instead of full sort. | Kth Largest, Top K Frequent |
| Overlapping ranges — merge, insert, or schedule | **Sort by start, then sweep** | After sorting, one linear pass merges/detects overlaps. | Merge Intervals, Meeting Rooms |
| Linked list — cycle, middle, or n-th from end | **Fast & Slow pointers** | Two pointers at different speeds meet/gap exactly where you need. | Detect Cycle, Middle of List |
| “Count the ways” / min-or-max where the same subproblem repeats | **Dynamic Programming (memo / tabulate)** | Cache overlapping subproblems; define state + transition + base case. | Climbing Stairs, Coin Change, LIS |

---

## Contents

- [HTML/CSS](#html/css) — 12 questions
- [Browser](#browser) — 12 questions
- [JavaScript](#javascript) — 43 questions
- [TypeScript](#typescript) — 18 questions
- [React](#react) — 25 questions
- [System Design](#system-design) — 15 questions
- [Accessibility](#accessibility) — 20 questions
- [Web Performance](#web-performance) — 20 questions
- [Testing](#testing) — 20 questions
- [Networking/Security](#networking/security) — 20 questions
- [DSA](#dsa) — 60 questions

---

## HTML/CSS

> These rounds check that you can build accessible, responsive layouts without reaching for a framework. Interviewers probe the box model, the cascade and specificity, flexbox vs grid, and semantic markup — then ask you to center a thing, build a responsive card grid, or fix a z-index bug live. The signal they want: you reason about layout from first principles (flow, containing block, stacking context) instead of guessing.

### 1. Why semantic HTML?  `Easy`

**Pattern:** Document Structure

**Problem.** Rebuild a page of <div>s using semantic elements. Which elements, and what do you gain?

**What it tests.** Whether you pick elements by the role of the content, not just for styling hooks.

**Approach & answer.** Semantic elements name the ROLE of content instead of using generic <div>/<span>: <header>, <nav>, <main> (one per page), <article> (self-contained, syndicatable), <section> (thematic group that needs a heading), <aside> (tangential), <footer>, plus <figure>/<figcaption>, <time>, <mark>. Payoffs: (1) Accessibility — screen readers expose these as landmarks so users jump straight to nav or main; a <div> announces nothing. (2) SEO — crawlers weight <main>/<article> content. (3) Maintainability — the markup documents its own intent. Rule of thumb: reach for a <div> only when no semantic element fits (a pure styling wrapper). Keep the heading hierarchy (<h1>…<h6>) logical and never skip levels. Interactive controls must be real <button>/<a> — a clickable <div> forces you to re-implement focusability, keyboard activation and the ARIA role you would have gotten for free.

**Use this technique when.** Every layout: choose the element that describes the content's role; fall back to <div> only for styling.

```html
<style>
  body { font: 15px system-ui; margin: 0; }
  header, footer { background: #1f2933; color: #fff; padding: 12px 16px; }
  nav a { color: #9fd3ff; margin-right: 12px; }
  main { display: flex; gap: 16px; padding: 16px; }
  article { flex: 1; } aside { width: 120px; color: #667; }
</style>

<header>
  <h1>Semantic Layout</h1>
  <nav><a href="#a">Home</a><a href="#b">Docs</a></nav>
</header>
<main>
  <article>
    <h2>Article title</h2>
    <p>Self-contained content lives in an &lt;article&gt;.</p>
    <p><time datetime="2026-08-13">Aug 13, 2026</time></p>
  </article>
  <aside><p>Related links (tangential).</p></aside>
</main>
<footer>© 2026 · built with landmarks, not div soup</footer>
```

**References.** [MDN · HTML elements reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) · [MDN · HTML sectioning](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section)

---

### 2. The box model & box-sizing  `Easy`

**Pattern:** Box Model

**Problem.** What are the four box-model layers? What does box-sizing: border-box change, and why is it the default people reach for?

**What it tests.** Whether you can predict an element's rendered size and know why border-box tames it.

**Approach & answer.** Every element is a box of four layers, inside out: content, padding, border, margin. With the default box-sizing: content-box, the width/height you set applies to the CONTENT box only — padding and border are ADDED on top, so a 200px + 20px padding + 2px border element actually occupies 244px. That makes 'width: 100%' plus any padding overflow its container. box-sizing: border-box makes width/height include padding and border, so the box stays the size you asked for and padding eats into the content instead. That predictability is why nearly every reset does `* { box-sizing: border-box }`. Margin is always outside the box and never counts toward width. One more gotcha: vertical margins between block siblings COLLAPSE — the larger of two adjacent margins wins rather than summing; padding and border never collapse.

**Use this technique when.** Sizing bugs where 'width: 100%' overflows: switch to border-box. Reset it globally at the top of every stylesheet.

```html
<style>
  .box { width: 200px; padding: 20px; border: 4px solid #333; margin: 8px 0; background: #e3f2fd; }
  .content { box-sizing: content-box; }  /* renders 200 + 40 + 8 = 248px wide */
  .border  { box-sizing: border-box;  }  /* renders exactly 200px wide */
</style>

<div class="box content">content-box → actual width 248px</div>
<div class="box border">border-box → actual width 200px</div>
```

**References.** [MDN · box-sizing](https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing) · [MDN · Introduction to the box model](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_box_model/Introduction_to_the_CSS_box_model)

---

### 3. Pseudo-classes vs pseudo-elements  `Easy`

**Pattern:** Selectors

**Problem.** What's the difference between :hover and ::before? When do you use each, and what's with the colons?

**What it tests.** Whether you distinguish selecting by STATE from generating/styling a sub-part.

**Approach & answer.** A pseudo-CLASS selects an element based on its STATE or position: :hover, :focus, :active, :visited, :checked, :disabled, :first-child, :nth-child(2n), :not(.x), :is()/:where(). It targets an element that already exists in the DOM. A pseudo-ELEMENT lets you style or create a specific SUB-PART of an element that isn't its own node: ::before and ::after inject generated content (they require a content property, even content: ''), ::first-line, ::first-letter, ::selection, ::placeholder, ::marker. Convention: pseudo-classes use one colon, pseudo-elements use two (::) to tell them apart — though browsers still accept the old single-colon form for the original four pseudo-elements. Common uses: :hover/:focus for interactive feedback (always pair :hover with :focus for keyboard users), :nth-child for zebra striping, ::before/::after for decorative icons, badges, clearfix, and tooltips — purely visual bits that shouldn't clutter the HTML. Generated content from ::before/::after is not selectable text and is largely ignored by assistive tech, so never put meaningful content there.

**Use this technique when.** Interactive states (:hover/:focus), striping/positioning (:nth-child), and decorative content (::before/::after).

```html
<style>
  .btn { padding: 10px 16px; border: 0; border-radius: 6px;
         background: #4c6ef5; color: #fff; cursor: pointer; }
  .btn:hover, .btn:focus { background: #364fc7; }          /* pseudo-class: state */
  .tag::before { content: '★ '; color: gold; }             /* pseudo-element: injected */
  li:nth-child(odd) { background: #f1f3f5; }                /* pseudo-class: position */
</style>

<button class="btn">Hover or focus me</button>
<p class="tag">Starred via ::before</p>
<ul><li>one</li><li>two</li><li>three</li></ul>
```

**References.** [MDN · Pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) · [MDN · Pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)

---

### 4. Specificity and the cascade  `Medium`

**Pattern:** Cascade & Specificity

**Problem.** Two rules set the same property on one element. How does the browser decide the winner?

**What it tests.** Whether you can compute specificity and know where source order and !important fit in.

**Approach & answer.** When multiple declarations target an element, the cascade resolves them in order: (1) origin & importance, (2) specificity, (3) source order. Specificity is a tuple (a,b,c): a = number of IDs, b = classes + attribute selectors + pseudo-classes, c = element types + pseudo-elements. Compare left to right — one ID (1,0,0) beats any number of classes (0,10,0). Inline style beats all selectors. !important jumps above normal declarations entirely (and !important conflicts are resolved by specificity among themselves). If specificity ties, the LAST matching rule in source order wins — that is why order in a stylesheet matters. The universal selector * and combinators (>, +, ~) add zero specificity. Modern escape hatches: :where() contributes zero specificity (great for low-strength defaults), :is() takes the specificity of its most specific argument, and @layer lets you order whole groups of rules regardless of selector strength. Practical advice: keep selectors flat and avoid !important so overrides stay predictable.

**Use this technique when.** Debugging 'my style isn't applying': inspect which rule wins, then match or beat its specificity instead of reaching for !important.

```html
<style>
  p { color: gray; }                 /* (0,0,1) */
  .note { color: green; }            /* (0,1,0) beats element */
  #lead { color: blue; }             /* (1,0,0) beats class */
  .note { color: red; }              /* same specificity as green → later wins */
</style>

<p>plain paragraph → gray</p>
<p class="note">note → red (two class rules tie, last wins)</p>
<p id="lead" class="note">lead → blue (ID wins over class)</p>
```

**References.** [MDN · Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) · [MDN · Cascade](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Cascade)

---

### 5. px vs em vs rem vs % vs viewport units  `Medium`

**Pattern:** Values & Units

**Problem.** Compare absolute and relative length units. When do you reach for rem over em, and what does % resolve against?

**What it tests.** Whether you understand what each unit is relative TO — the source of most sizing surprises.

**Approach & answer.** px is an absolute, device-independent pixel — predictable but ignores user font-size preferences. Relative units scale: em is relative to the element's own font-size (and COMPOUNDS when nested — nested ems multiply, which surprises people); rem is relative to the ROOT font-size, so it stays flat and predictable no matter the nesting — the reason rem is the default choice for typography and spacing in scalable designs. % resolves against different things per property: width/left against the containing block's width, height against its height (only if the parent has an explicit height), and font-size against the parent's font-size. Viewport units are relative to the viewport: 1vw = 1% of width, 1vh = 1% of height, with vmin/vmax for the smaller/larger axis; great for hero sections and fluid type (often via clamp()). ch (width of '0') and ex are font-relative and handy for line lengths. Rule of thumb: rem for type and spacing (respects user zoom), % or fr for layout widths, viewport units for full-screen sections, px only for hairline borders and things that truly shouldn't scale.

**Use this technique when.** Accessibility-friendly sizing (rem so text respects zoom), fluid layouts (%, vw), and avoiding compounding surprises from nested em.

```html
<style>
  html { font-size: 16px; }
  .rem { font-size: 1.5rem; }                 /* 24px, always relative to root */
  .em  { font-size: 1.5em; }                  /* 1.5x the PARENT font-size */
  .em .em { font-size: 1.5em; }               /* compounds → 2.25x parent */
  .vw  { font-size: 5vw; }                    /* scales with viewport width */
</style>

<p class="rem">1.5rem → 24px</p>
<div class="em">1.5em
  <span class="em">nested em compounds</span>
</div>
<p class="vw">5vw → resize the preview to see it change</p>
```

**References.** [MDN · CSS values and units](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Values_and_Units) · [MDN · length](https://developer.mozilla.org/en-US/docs/Web/CSS/length)

---

### 6. Flexbox: centering and a nav bar  `Medium`

**Pattern:** Flexbox

**Problem.** Center a box both axes with flexbox. Then lay out a nav with a logo on the left and links on the right.

**What it tests.** Whether you know the main/cross axis model and the justify/align pair.

**Approach & answer.** Flexbox is one-dimensional layout along a MAIN axis (set by flex-direction: row default, or column) with a perpendicular CROSS axis. justify-content aligns items along the main axis (flex-start, center, space-between, space-around, space-evenly); align-items aligns along the cross axis (stretch default, center, flex-start, baseline). So dead-center is just display: flex; justify-content: center; align-items: center. Each item's flexibility is `flex: grow shrink basis` — flex: 1 means 'grow to share leftover space equally'; flex: 0 0 auto means 'don't grow or shrink, size to content'. gap adds spacing without margin hacks. The classic nav pattern: a flex row with justify-content: space-between pushes the logo and the link group to opposite ends; wrap the links in their own flex container with a gap. margin-left: auto on a single item also shoves it (and everything after) to the far end — a handy trick. Use flexbox for one axis (a row of buttons, a toolbar, centering); use grid when you need rows AND columns together.

**Use this technique when.** Toolbars, nav bars, button rows, and centering a single element — any time layout runs along one axis.

```html
<style>
  .center { display: flex; justify-content: center; align-items: center;
            height: 120px; background: #eef; }
  .center .dot { width: 48px; height: 48px; border-radius: 50%; background: #4c6ef5; }
  nav { display: flex; justify-content: space-between; align-items: center;
        padding: 10px 16px; background: #1f2933; color: #fff; }
  nav .links { display: flex; gap: 16px; }
  nav a { color: #9fd3ff; }
</style>

<div class="center"><div class="dot"></div></div>
<nav>
  <strong>Logo</strong>
  <span class="links"><a href="#">Home</a><a href="#">Docs</a><a href="#">About</a></span>
</nav>
```

**References.** [MDN · Basic concepts of flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox) · [MDN · justify-content](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)

---

### 7. CSS Grid: a responsive card layout  `Medium`

**Pattern:** CSS Grid

**Problem.** Build a card grid that fits as many columns as the width allows, with no media queries. How does auto-fit + minmax work?

**What it tests.** Whether you can reach for 2D grid and the auto-fit/minmax idiom for responsiveness.

**Approach & answer.** Grid is TWO-dimensional: you define columns AND rows and place items into cells. grid-template-columns sets the tracks; the fr unit distributes leftover space (1fr 1fr = two equal columns). repeat(N, …) avoids repetition. The responsive idiom needs no media queries: grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)). minmax(200px, 1fr) means each column is at least 200px and grows to share extra space; auto-fit creates as many 200px+ columns as fit, then stretches them to fill the row. (auto-fill is the sibling — it keeps empty phantom tracks instead of stretching the real ones, so items don't widen to fill.) gap spaces tracks. You can also place items explicitly with grid-column: 1 / 3 (span two columns) or name areas with grid-template-areas for whole-page layouts. Rule of thumb: grid for the overall page skeleton and any rows-and-columns arrangement; flexbox for one-axis content inside a grid cell. They compose — grid outside, flex inside — constantly.

**Use this technique when.** Card galleries, dashboards, image grids, and any full-page skeleton with both rows and columns.

```html
<style>
  .grid { display: grid; gap: 12px;
          grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
  .card { background: #4c6ef5; color: #fff; padding: 18px; border-radius: 8px; text-align: center; }
</style>

<div class="grid">
  <div class="card">1</div><div class="card">2</div>
  <div class="card">3</div><div class="card">4</div>
  <div class="card">5</div><div class="card">6</div>
</div>
<!-- Resize the preview: columns reflow automatically, no media query -->
```

**References.** [MDN · Basic concepts of grid layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Basic_concepts_of_grid_layout) · [MDN · minmax()](https://developer.mozilla.org/en-US/docs/Web/CSS/minmax)

---

### 8. Mobile-first & media queries  `Medium`

**Pattern:** Responsive Design

**Problem.** What does 'mobile-first' mean in CSS, and why write min-width queries rather than max-width?

**What it tests.** Whether you build up from a simple base rather than patching a desktop layout down.

**Approach & answer.** Mobile-first means the BASE (unqualified) styles target the smallest screen — a single column, full-width, stacked — and you layer complexity with min-width media queries as more space becomes available. Writing min-width (rather than max-width) matches this additive direction: each breakpoint only ADDS rules, so you never fight and undo desktop styles on small screens. Benefits: small devices (often the weakest, on the worst networks) get the leanest CSS and don't parse rules meant for large screens; the cascade stays additive and easy to reason about. Choose breakpoints by where YOUR content breaks, not by specific device widths — resize until the layout looks bad, put a breakpoint there. Always include <meta name='viewport' content='width=device-width, initial-scale=1'> or mobile browsers render at a fake 980px. Media queries also cover capability and preference: prefers-color-scheme (dark mode), prefers-reduced-motion (disable animations for users who ask), pointer/hover (touch vs mouse), and orientation. Modern container queries (@container) let a component respond to its OWN width instead of the viewport — better for reusable components.

**Use this technique when.** Any layout serving phones and desktops: start with the stacked mobile base, enhance up at min-width breakpoints.

```html
<style>
  /* base = mobile: stacked */
  .row { display: flex; flex-direction: column; gap: 8px; }
  .row > div { background: #4c6ef5; color: #fff; padding: 16px; text-align: center; }

  /* enhance up when there's room */
  @media (min-width: 480px) {
    .row { flex-direction: row; }
    .row > div { flex: 1; }
  }
</style>

<div class="row"><div>A</div><div>B</div><div>C</div></div>
<!-- Narrow preview → stacked; widen past 480px → side by side -->
```

**References.** [MDN · Media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) · [MDN · Responsive design](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design)

---

### 9. CSS custom properties & theming  `Medium`

**Pattern:** Custom Properties

**Problem.** How do CSS variables differ from Sass variables? Use them to implement a light/dark theme.

**What it tests.** Whether you know custom properties are live, inherited, runtime values — not compile-time text.

**Approach & answer.** Custom properties (CSS variables) are declared as --name: value on a selector and read with var(--name, fallback). Unlike Sass/Less variables — which are compile-time text substitutions that vanish from the output — custom properties are LIVE and part of the cascade: they inherit to descendants, can be overridden per-selector, respond to media queries, and can be read and written at runtime from JavaScript (element.style.setProperty('--x', …)). That runtime nature is what makes them ideal for theming: define your palette once on :root, then override the same variable names under a [data-theme='dark'] selector (or inside a prefers-color-scheme media query). Every component that references var(--bg) instantly re-themes when the variable changes — no rebuild, no duplicated rule sets. They cascade like any property, so you can also scope a variable to a subtree (e.g. a card that redefines --accent). Gotchas: they are case-sensitive, only usable in property VALUES (not selectors or property names), and invalid values fall back to the declared fallback or the inherited/initial value. Pair with calc() for derived values.

**Use this technique when.** Theming (light/dark), design tokens shared across components, and any value you want to tweak at runtime from JS.

```html
<style>
  :root { --bg: #ffffff; --fg: #1f2933; --accent: #4c6ef5; }
  [data-theme="dark"] { --bg: #1f2933; --fg: #e9ecef; --accent: #9fd3ff; }

  .panel { background: var(--bg); color: var(--fg);
           border: 2px solid var(--accent); padding: 16px; border-radius: 8px; }
</style>

<div class="panel">Light theme (default variables)</div>
<div data-theme="dark" class="panel" style="margin-top:8px">Dark theme (same var names, overridden)</div>
```

**References.** [MDN · Using CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) · [MDN · var()](https://developer.mozilla.org/en-US/docs/Web/CSS/var)

---

### 10. Transitions & transforms  `Medium`

**Pattern:** Animation

**Problem.** Animate a hover effect smoothly. Which properties are cheap to animate and which cause jank?

**What it tests.** Whether you know transform/opacity are compositor-friendly while width/top force layout.

**Approach & answer.** A transition interpolates a property between its old and new value over time: transition: <property> <duration> <timing-function> <delay>. It fires whenever the property changes (on :hover, a class toggle, etc.). transform applies a visual change — translate(), scale(), rotate(), skew() — WITHOUT affecting layout: the element's box in the flow is unchanged, so neighbours don't move. The performance point interviewers want: animate ONLY transform and opacity for smooth 60fps. Those two can be handled by the compositor on the GPU and skip layout and paint. Animating width, height, top, left, or margin forces the browser to recompute geometry (reflow) and repaint every frame — the usual source of jank. So to move something, use transform: translateX() instead of animating left; to resize, use scale() instead of width. will-change: transform can hint the browser to promote an element to its own layer ahead of time (use sparingly — each layer costs memory). Respect prefers-reduced-motion and disable or soften animation for users who request it. timing functions (ease, ease-in-out, cubic-bezier, steps) shape the acceleration curve.

**Use this technique when.** Hover/press feedback, entrance animations, and reordering — reach for transform+opacity to keep it smooth.

```html
<style>
  .card { width: 120px; padding: 20px; background: #4c6ef5; color: #fff;
          border-radius: 8px; text-align: center;
          transition: transform 200ms ease, box-shadow 200ms ease; }
  .card:hover { transform: translateY(-6px) scale(1.04);          /* GPU-friendly */
                box-shadow: 0 10px 20px rgba(0,0,0,.25); }
</style>

<div class="card">Hover me</div>
<!-- transform moves it without reflowing neighbours -->
```

**References.** [MDN · Using CSS transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions/Using_CSS_transitions) · [MDN · transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform)

---

### 11. Inheritance & inherit / initial / unset  `Medium`

**Pattern:** Inheritance

**Problem.** Which properties inherit and which don't? Explain the inherit, initial, unset, and revert keywords.

**What it tests.** Whether you understand default inheritance and the four global CSS-wide keywords.

**Approach & answer.** Some properties inherit by default — mostly text-related ones: color, font-family, font-size, line-height, text-align, visibility, list-style, white-space. Most box/layout properties do NOT inherit: margin, padding, border, width, height, background, display, position — otherwise a container's border would repeat on every child. The four CSS-wide keywords let you override this per property: `inherit` forces a property to take the parent's computed value (useful to make a normally-non-inheriting property, e.g. border-color, follow its parent). `initial` resets to the property's SPEC default (its defined initial value, independent of any stylesheet) — note the spec default for color is black, not necessarily what you expect. `unset` is the smart one: it acts like inherit if the property naturally inherits, otherwise like initial. `revert` rolls back to the value the USER-AGENT (browser) stylesheet would give — so revert on display restores <div>'s block, unlike initial which would give inline. Practical use: `all: unset` on a button strips inherited and default styling to build a clean custom control; individual keywords fix one-off cascade surprises.

**Use this technique when.** Stripping inherited styles from custom controls (all: unset), or forcing/resetting a single property with inherit/initial.

```html
<style>
  .parent { color: crimson; border: 2px solid crimson; padding: 12px; }
  .parent p { }                                  /* color inherits → crimson */
  .reset { color: initial; }                     /* back to spec default (black) */
  .follow { border-color: inherit; border-style: solid; border-width: 2px; }
</style>

<div class="parent">
  <p>Inherits color → crimson</p>
  <p class="reset">color: initial → black</p>
  <p class="follow">border-color: inherit → crimson border</p>
</div>
```

**References.** [MDN · Inheritance](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Inheritance) · [MDN · unset](https://developer.mozilla.org/en-US/docs/Web/CSS/unset)

---

### 12. Positioning, stacking context & z-index  `Hard`

**Pattern:** Positioning

**Problem.** Explain the position values. Then explain why a higher z-index sometimes still renders BEHIND a lower one.

**What it tests.** Whether you understand containing blocks and — the real trap — stacking contexts.

**Approach & answer.** position values: static (default, in normal flow); relative (offset from its normal spot, still occupies original space, becomes a positioning context for children); absolute (removed from flow, positioned against the nearest positioned ancestor); fixed (against the viewport, ignores scroll); sticky (relative until it hits a scroll threshold, then fixed within its container). top/left/right/bottom only affect positioned elements. The z-index trap: z-index only orders elements WITHIN THE SAME stacking context. A stacking context is a self-contained layer; once an element forms one, ALL its descendants are painted as a group at that context's level, so a child with z-index: 9999 can never escape above a sibling context with z-index: 2. Contexts are created by: the root element, position + a z-index other than auto, opacity < 1, transform / filter / will-change, isolation: isolate, and flex/grid children with a z-index. So the fix for 'my huge z-index does nothing' is usually to raise the z-index of the ANCESTOR that forms the competing context, or to stop creating an unwanted context (e.g. a stray opacity: 0.99).

**Use this technique when.** Modals, dropdowns, and tooltips that render behind other content despite a big z-index — trace the stacking contexts.

```html
<style>
  .wrap { position: relative; height: 140px; }
  .a { position: absolute; top: 20px; left: 20px; width: 120px; height: 90px;
       background: #ff6b6b; z-index: 2; }
  .b { position: absolute; top: 50px; left: 80px; width: 120px; height: 90px;
       background: #4c6ef5; z-index: 1; color: #fff; }
  /* .a (z-index 2) paints above .b (z-index 1) — same stacking context */
</style>

<div class="wrap">
  <div class="a">z-index: 2</div>
  <div class="b">z-index: 1</div>
</div>
```

**References.** [MDN · position](https://developer.mozilla.org/en-US/docs/Web/CSS/position) · [MDN · Stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context)

---

## Browser

> Beyond the language, seniors are expected to understand the platform the code runs on: how a URL becomes pixels, the critical rendering path, reflow vs repaint, storage options and their trade-offs, the same-origin policy and CORS, and the observer/scheduling APIs (rAF, IntersectionObserver). These questions separate people who memorized React from people who understand what the browser is doing underneath.

### 1. localStorage vs sessionStorage vs cookies  `Easy`

**Pattern:** Storage

**Problem.** Compare the three client storage mechanisms: capacity, lifetime, scope, and whether they're sent to the server.

**What it tests.** Whether you pick the right store and know cookies ride on every request.

**Approach & answer.** localStorage: ~5–10MB, key/value strings, persists until explicitly cleared, scoped to the ORIGIN, NOT sent to the server, synchronous API — good for user preferences and non-sensitive cached data. sessionStorage: same API and size, but scoped to a single TAB and cleared when that tab closes — good for per-tab wizard/form state. Cookies: tiny (~4KB each), and their defining trait is that the browser attaches matching cookies to EVERY HTTP request to the origin — which is exactly why they're used for session/auth tokens the server needs, but also why overusing them bloats every request. Cookies have an expiry (or are session cookies) and security attributes (HttpOnly, Secure, SameSite). Key contrasts: use localStorage/sessionStorage for client-only data you don't want on the wire; use cookies only for what the server must read on each request. Security note: anything readable by JS (localStorage, non-HttpOnly cookies) is exposed to XSS, so never store secrets there — auth tokens belong in HttpOnly cookies. For large or structured data, reach for IndexedDB (async, hundreds of MB). Storage values are always strings — JSON.stringify/parse objects.

**Use this technique when.** Choosing where to keep client state: prefs → localStorage, per-tab flow → sessionStorage, server-read session → cookie.

```js
// (Sandboxed here: storage/cookie access throws SecurityError, so this is read-only.)
localStorage.setItem('theme', 'dark');        // persists across tabs & restarts
console.log(localStorage.getItem('theme'));

sessionStorage.setItem('step', '2');          // dies when THIS tab closes
console.log(sessionStorage.getItem('step'));

document.cookie = 'sid=abc; max-age=3600; path=/'; // sent on every request to origin
console.log(document.cookie);

// objects must be serialized — storage only holds strings
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Ada' }));
console.log(JSON.parse(localStorage.getItem('user')).name);
```

**References.** [MDN · Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) · [MDN · Document.cookie](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie)

---

### 2. What happens when you type a URL and hit Enter?  `Medium`

**Pattern:** Networking

**Problem.** Walk through everything from the keystroke to pixels on screen. This is the classic 'do you understand the whole stack' question.

**What it tests.** Whether you can narrate the full pipeline: DNS → TCP/TLS → HTTP → parse → render.

**Approach & answer.** (1) URL parsing — the browser splits scheme, host, path; if it's not a valid URL it hands the text to the default search engine. (2) DNS resolution — the hostname is resolved to an IP, checking browser cache → OS cache → router → recursive resolver. (3) TCP handshake — a connection opens (SYN/SYN-ACK/ACK); HTTPS adds a TLS handshake to negotiate keys and verify the certificate. (4) HTTP request — the browser sends GET with headers (cookies, Accept, User-Agent); the server responds with status, headers, and the HTML body. (5) Parsing & render — the HTML is parsed into the DOM; CSS into the CSSOM; the two combine into the render tree; then layout computes geometry and paint fills pixels, composited into layers on screen. Along the way the preload scanner fetches subresources (CSS, JS, images) in parallel; render-blocking CSS and synchronous JS pause parsing. Modern answers also mention HTTP caching, connection reuse (keep-alive/HTTP/2 multiplexing), and that a service worker may intercept the request entirely. The interviewer is probing breadth — hit each stage and mention one detail per stage.

**Use this technique when.** System-level interview warm-up; also the mental map for diagnosing where a slow page loses time.

```text
URL parse → DNS lookup → TCP + TLS handshake → HTTP request/response
        → parse HTML (DOM) + CSS (CSSOM) → render tree → layout → paint → composite
(preload scanner fetches CSS/JS/images in parallel; a Service Worker may intercept)
```

**References.** [MDN · Populating the page: how browsers work](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/How_browsers_work) · [MDN · What is a URL?](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL)

---

### 3. The critical rendering path  `Medium`

**Pattern:** Rendering

**Problem.** How does the browser turn HTML, CSS, and JS into pixels? Why is CSS render-blocking and where does JS block parsing?

**What it tests.** Whether you know DOM+CSSOM→render tree→layout→paint and what blocks each step.

**Approach & answer.** The critical rendering path is the sequence the browser runs to first paint: (1) parse HTML into the DOM tree incrementally; (2) parse CSS into the CSSOM; (3) combine DOM + CSSOM into the RENDER TREE (only visible nodes — display:none nodes are excluded); (4) LAYOUT (reflow) computes each box's exact position and size; (5) PAINT rasterizes pixels; (6) COMPOSITE assembles layers. CSS is render-blocking: the browser won't paint until the CSSOM is ready, because it would otherwise flash unstyled content — so ship critical CSS small and early. A synchronous <script> (no async/defer) is PARSER-blocking: when the parser hits it, it must stop building the DOM, download and execute the script (which can also read/modify the not-yet-complete CSSOM/DOM), then resume — which is why scripts traditionally go at the end of <body>. defer downloads in parallel and runs after parsing in order; async runs as soon as it downloads, order not guaranteed. Optimizations: inline critical CSS, defer non-critical JS, preload key assets, minimize the number of round-trips before first paint.

**Use this technique when.** Optimizing first paint / LCP: shrink render-blocking CSS, defer JS, preload above-the-fold assets.

```text
DOM  ─┐
       ├─► Render Tree ─► Layout (reflow) ─► Paint ─► Composite
CSSOM ─┘

CSS  = render-blocking (no paint until CSSOM ready)
<script>          = parser-blocking (stops DOM construction)
<script defer>    = runs after parse, in order
<script async>    = runs on download, order not guaranteed
```

**References.** [MDN · Critical rendering path](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path) · [MDN · script defer/async](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)

---

### 4. Event bubbling, capturing & delegation  `Medium`

**Pattern:** Events

**Problem.** Explain the three phases of event propagation, then use delegation to handle clicks on a list with one listener. Click the items in the preview.

**What it tests.** Whether you understand the propagation model and can exploit it for delegation.

**Approach & answer.** A DOM event travels in three phases: (1) CAPTURING — from the window down through ancestors to the target; (2) TARGET — it reaches the element clicked; (3) BUBBLING — it travels back up through ancestors to the window. addEventListener listens in the bubbling phase by default; pass { capture: true } (or a third arg true) to listen while capturing. event.stopPropagation() halts further travel; event.stopImmediatePropagation() also skips other listeners on the same node; event.preventDefault() cancels the browser's default action (following a link, submitting a form) but does NOT stop propagation — they're independent. EVENT DELEGATION exploits bubbling: instead of binding a listener to every child, bind ONE listener on a common ancestor and inspect event.target (often with .closest(selector)) to find which descendant was hit. Benefits: far fewer listeners (less memory), and it automatically covers elements added to the DOM LATER — a big win for dynamic lists. Caveat: some events don't bubble (focus, blur, scroll on most elements) — use their capturing variants (focusin/focusout) or the capture phase. event.currentTarget is the node the listener is attached to; event.target is where it originated.

**Use this technique when.** Lists, tables, menus, and any dynamically-added elements: one delegated listener beats N per-item listeners.

```html
<style>
  ul { font: 15px system-ui; } li { cursor: pointer; padding: 4px; }
  li:hover { background: #eef; }
</style>

<ul id="list">
  <li>Apple</li><li>Banana</li><li>Cherry</li>
</ul>
<button id="add">Add item</button>

<script>
  var list = document.getElementById('list');
  // ONE listener handles all current AND future <li> via bubbling
  list.addEventListener('click', function (e) {
    var li = e.target.closest('li');
    if (!li) return;
    console.log('clicked:', li.textContent);
    li.style.textDecoration = 'line-through';
  });
  var n = 3;
  document.getElementById('add').addEventListener('click', function () {
    n++; var li = document.createElement('li');
    li.textContent = 'Item ' + n; list.appendChild(li); // still handled, no rebind
  });
</script>
```

**References.** [MDN · Event bubbling](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Event_bubbling) · [MDN · EventTarget.addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)

---

### 5. Cookie attributes: HttpOnly, Secure, SameSite  `Medium`

**Pattern:** Security

**Problem.** What do the HttpOnly, Secure, SameSite, Domain/Path, and Expires/Max-Age cookie attributes control, and how do they defend against attacks?

**What it tests.** Whether you can harden a session cookie against XSS and CSRF.

**Approach & answer.** HttpOnly: the cookie is invisible to JavaScript (document.cookie can't read it) — this is the primary defense against XSS stealing a session token; set it on all auth cookies. Secure: the cookie is only sent over HTTPS, preventing interception on plaintext connections. SameSite controls cross-site sending and is the main CSRF defense: Strict never sends the cookie on cross-site requests (safest, but breaks inbound links to logged-in pages); Lax (the modern default) sends it on top-level GET navigations but not on cross-site POSTs or subresource requests; None sends it always but REQUIRES Secure — used for legitimate third-party/embedded contexts. Domain scopes which hosts receive it (omit to keep it host-only; setting a parent domain shares it with subdomains); Path scopes it to a URL prefix. Expires (absolute date) / Max-Age (seconds) set lifetime — omit both for a session cookie that dies when the browser closes. Hardened session cookie: `Set-Cookie: sid=…; HttpOnly; Secure; SameSite=Lax; Path=/`. Note these are set by the SERVER via the Set-Cookie header; JS can only set non-HttpOnly cookies. Prefix names with __Host- to lock a cookie to Secure + host-only + Path=/ for extra hardening.

**Use this technique when.** Configuring auth/session cookies: HttpOnly + Secure + SameSite is the baseline against XSS token theft and CSRF.

```text
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600

HttpOnly   → hidden from document.cookie  (blocks XSS token theft)
Secure     → HTTPS only                   (blocks interception)
SameSite   → Strict | Lax | None          (blocks CSRF; None requires Secure)
Domain/Path→ scope of who/where receives it
Max-Age/Expires → lifetime (omit both = session cookie)
```

**References.** [MDN · Set-Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) · [MDN · SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)

---

### 6. fetch + AbortController  `Medium`

**Pattern:** Networking

**Problem.** Use the Fetch API to load JSON with proper error handling, and cancel an in-flight request with AbortController (e.g. a timeout or a superseded search).

**What it tests.** Whether you check response.ok, parse safely, and can abort a request.

**Approach & answer.** fetch(url, options) returns a promise for a Response. A key gotcha: fetch only REJECTS on network failure — a 404 or 500 still RESOLVES, so you must check response.ok (or response.status) yourself and throw otherwise. Read the body with an async method matching the content: response.json(), .text(), .blob() (each returns a promise and can only be read once). Cancellation uses AbortController: create one, pass controller.signal in the options, and call controller.abort() to cancel — the fetch promise rejects with a DOMException whose name is 'AbortError', which you special-case in catch. This powers two common patterns: a TIMEOUT (abort after N ms; or use AbortSignal.timeout(ms)), and SUPERSEDING — in a search-as-you-type box, abort the previous request when a new keystroke fires so a slow earlier response can't overwrite a newer one (a race fix). Always clear the timeout in finally. For parallel requests use Promise.all; for the first to settle, Promise.race. Add credentials:'include' to send cookies cross-origin (subject to CORS).

**Use this technique when.** Data fetching with cancellation: request timeouts, and aborting stale search/autocomplete requests on new input.

```js
// (Sandboxed here: offline fetch fails, so this is read-only reference.)
async function getJSON(url, ms) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), ms);   // cancel if too slow
  try {
    const res = await fetch(url, { signal: ctrl.signal });
    if (!res.ok) throw new Error('HTTP ' + res.status); // fetch does NOT throw on 404/500
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') console.log('aborted (timeout)');
    else console.log('failed:', err.message);
    throw err;
  } finally {
    clearTimeout(timer);
  }
}
getJSON('/api/user', 5000).then(u => console.log(u)).catch(() => {});
```

**References.** [MDN · Using Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) · [MDN · AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)

---

### 7. requestAnimationFrame  `Medium`

**Pattern:** Animation

**Problem.** Why animate with requestAnimationFrame instead of setInterval? Run the demo to count frames rendered in ~500ms.

**What it tests.** Whether you know rAF syncs to the display refresh and pauses in background tabs.

**Approach & answer.** requestAnimationFrame(callback) schedules the callback to run BEFORE the browser's next repaint, syncing your work to the display's refresh rate (typically ~60fps, but 120fps on high-refresh screens — so never hard-code 16ms). The callback receives a high-resolution timestamp (a DOMHighResTimeStamp) you use to compute elapsed time and make motion frame-rate INDEPENDENT (move by velocity × delta, not a fixed step). Advantages over setInterval/setTimeout for animation: (1) it's aligned to paint, so you never draw more often than the screen updates or land mid-frame (which causes tearing/jank); (2) the browser PAUSES it in background tabs and when the element isn't visible, saving CPU and battery — timers keep firing and waste work; (3) callbacks are batched, so multiple animations share one frame. To animate continuously, call requestAnimationFrame again from inside the callback (a self-scheduling loop); cancel with cancelAnimationFrame(id). For the actual visual change, still mutate transform/opacity so the compositor can handle it. rAF is also the right place to batch DOM reads/writes to avoid layout thrashing. Use it for JS-driven animation and smooth scroll effects; prefer CSS transitions/animations when the movement is declarative.

**Use this technique when.** JS-driven animation, smooth scroll/parallax, and batching DOM writes — anything that should track the refresh rate.

```js
// Count how many frames the browser paints in ~500ms (≈ your refresh rate / 2).
let frames = 0;
const start = performance.now();

function tick(now) {
  frames++;
  if (now - start < 500) {
    requestAnimationFrame(tick);          // self-scheduling loop
  } else {
    const fps = Math.round(frames / ((now - start) / 1000));
    console.log('Rendered ' + frames + ' frames in ~500ms → ~' + fps + ' fps');
  }
}
requestAnimationFrame(tick);
```

**References.** [MDN · window.requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) · [MDN · cancelAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame)

---

### 8. IntersectionObserver (lazy loading & infinite scroll)  `Medium`

**Pattern:** Observers

**Problem.** How do you detect when an element enters the viewport without janky scroll listeners? Implement lazy-loading with IntersectionObserver.

**What it tests.** Whether you replace scroll-handler + getBoundingClientRect polling with the async observer.

**Approach & answer.** IntersectionObserver asynchronously notifies you when a target element's visibility relative to a root (the viewport by default) crosses configured thresholds — WITHOUT running code on every scroll event. The old approach — a scroll listener calling getBoundingClientRect on each element — fires constantly, forces synchronous layout, and janks the main thread. The observer instead batches these checks off the main thread and calls your callback only when a threshold is crossed. API: new IntersectionObserver(callback, { root, rootMargin, threshold }); call observer.observe(el) per target. threshold (0–1, or an array) sets how much must be visible to fire (0 = any pixel, 1 = fully visible). rootMargin grows/shrinks the root box — e.g. '200px' fires 200px BEFORE the element scrolls in, perfect for pre-loading. In the callback, each entry has isIntersecting, intersectionRatio, and target; typically you act then observer.unobserve(entry.target) so it fires once. Classic uses: lazy-loading images (swap data-src → src when near viewport — though native loading='lazy' now covers the simple case), infinite scroll (observe a sentinel at the list bottom and fetch the next page), and firing analytics/animations when a section appears. Sibling APIs: ResizeObserver (size changes) and MutationObserver (DOM changes).

**Use this technique when.** Lazy-loading, infinite scroll (sentinel), and viewport-triggered animation/analytics — instead of scroll polling.

```js
// (Sandboxed here: needs real scrollable layout, so this is read-only reference.)
const io = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;   // swap in the real image when near viewport
      io.unobserve(img);           // load once, then stop watching
    }
  }
}, {
  root: null,          // viewport
  rootMargin: '200px', // start loading 200px BEFORE it scrolls in
  threshold: 0
});

document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));
```

**References.** [MDN · Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) · [MDN · IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)

---

### 9. Reflow vs repaint  `Hard`

**Pattern:** Rendering

**Problem.** What's the difference between a reflow and a repaint? Which DOM/CSS operations trigger each, and how do you avoid layout thrashing?

**What it tests.** Whether you know geometry changes are expensive and how batched reads/writes prevent thrashing.

**Approach & answer.** REFLOW (layout) recomputes the geometry — positions and sizes — of elements; because a box can shift its siblings and ancestors, a reflow can cascade across much of the tree, making it the expensive one. REPAINT redraws pixels without changing geometry (e.g. color, background, visibility, box-shadow). Reflow always forces a subsequent repaint; a repaint doesn't force reflow. Triggers of reflow: changing width/height/margin/padding/top/left, adding/removing DOM nodes, changing font-size, or READING a layout property (offsetHeight, getBoundingClientRect, scrollTop, getComputedStyle) while the layout is dirty — the read forces a synchronous reflow to return a fresh value. That last point causes LAYOUT THRASHING: a loop that alternates writes and reads forces reflow every iteration. Fix by BATCHING — read all layout values first, then do all writes (or use requestAnimationFrame / the FastDOM pattern). Cheapest of all: animate only transform and opacity, which skip layout and paint and run on the compositor. Also minimize affected scope, use position:absolute/fixed for animated elements to isolate them, and toggle a single class instead of many inline style writes.

**Use this technique when.** Janky scroll/animation performance: batch DOM reads then writes, and animate transform/opacity only.

```text
// ❌ layout thrashing: read → write → read → write forces reflow each loop
for (const el of items) {
  el.style.height = el.offsetHeight + 10 + 'px'; // read offsetHeight, then write
}

// ✅ batch: read all, then write all
const heights = items.map(el => el.offsetHeight); // all reads
items.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px'; }); // all writes
```

**References.** [MDN · Reflow](https://developer.mozilla.org/en-US/docs/Glossary/Reflow) · [web.dev · Avoid large, complex layouts and layout thrashing](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing)

---

### 10. Same-origin policy & CORS  `Hard`

**Pattern:** Security

**Problem.** What defines an 'origin'? What does the same-origin policy block, and how does CORS selectively relax it? Explain preflight requests.

**What it tests.** Whether you understand that CORS is server-granted permission, not a client bypass.

**Approach & answer.** An ORIGIN is the triple (scheme, host, port) — https://app.com and http://app.com differ (scheme), as do app.com:443 and app.com:8080 (port). The SAME-ORIGIN POLICY is a browser security rule: script from one origin can send requests to another, but by default cannot READ the cross-origin response, and can't touch another origin's DOM or cookies. It's what stops a malicious site from reading your bank's responses using your session. CORS (Cross-Origin Resource Sharing) is how a SERVER opts in to sharing: it returns Access-Control-Allow-Origin (a specific origin or *) plus optional -Allow-Methods/-Allow-Headers/-Allow-Credentials, and the browser only exposes the response to JS if those headers permit it. Crucially CORS is enforced by the browser and granted by the server — you cannot disable it from client code. 'Simple' requests (GET/POST/HEAD with safe headers and standard content types) go straight through and are checked on the response. Anything else (PUT/DELETE, custom headers, application/json) triggers a PREFLIGHT: the browser first sends an OPTIONS request asking permission; only if the server approves does the real request go. With credentials (cookies), Allow-Origin cannot be * and Allow-Credentials must be true. The server always RECEIVES the request — CORS only gates whether JS may read the reply.

**Use this technique when.** Debugging 'blocked by CORS policy' errors: the fix is server headers, not client code; watch for the OPTIONS preflight.

```text
origin = scheme + host + port   (https://app.com:443)

Preflight (for non-simple requests):
  Browser →  OPTIONS /api        Origin: https://app.com
             Access-Control-Request-Method: PUT
  Server  →  Access-Control-Allow-Origin: https://app.com
             Access-Control-Allow-Methods: PUT
  → approved, browser sends the real PUT

With cookies: Allow-Origin must be exact (not *) AND Allow-Credentials: true
```

**References.** [MDN · Same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) · [MDN · Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)

---

### 11. XSS: textContent vs innerHTML  `Hard`

**Pattern:** Security

**Problem.** Why is assigning user input to innerHTML dangerous? Run the demo — the safe box shows text, the unsafe box executes an injected handler.

**What it tests.** Whether you default to textContent and treat innerHTML as a code-execution surface.

**Approach & answer.** Cross-Site Scripting (XSS) is when attacker-controlled input is interpreted as CODE in another user's browser, running with that page's privileges — letting it read the DOM, steal non-HttpOnly cookies/tokens, and act as the user. The core mistake is putting untrusted data into an HTML-parsing sink. innerHTML PARSES its string as HTML, so markup like an <img> with an onerror handler (or <svg>, event attributes, etc.) executes — note that a raw <script> inserted via innerHTML does NOT run, but onerror/onload attributes do, which is the common vector. textContent (and .innerText) treat the value as PLAIN TEXT — tags are shown literally, never parsed, so injection is inert. Rule: default to textContent / createTextNode; only use innerHTML with trusted or sanitized content. Defenses in depth: (1) prefer textContent and DOM APIs; (2) if you must render HTML, sanitize with a vetted library (DOMPurify) or the Sanitizer API; (3) frameworks like React escape by default — the danger there is dangerouslySetInnerHTML; (4) a Content-Security-Policy header limits what can execute even if injection slips through; (5) keep session tokens in HttpOnly cookies so XSS can't read them; (6) escape/encode by context (HTML, attribute, URL, JS). Never trust input length or a blocklist — allowlist and encode.

**Use this technique when.** Rendering any user-supplied string into the DOM: reach for textContent; gate innerHTML behind sanitization.

```html
<style> div { font: 14px system-ui; padding: 6px; } .box { border: 1px solid #ccc; margin: 4px 0; } </style>

<div class="box">safe (textContent): <span id="safe"></span></div>
<div class="box">unsafe (innerHTML): <span id="unsafe"></span></div>

<script>
  // Pretend this string came from a user / URL param:
  var userInput = '<img src=x onerror="console.log(\'XSS fired via innerHTML!\')">';

  // ✅ textContent: rendered as literal text, inert
  document.getElementById('safe').textContent = userInput;

  // ❌ innerHTML: parsed as HTML → the onerror handler executes
  document.getElementById('unsafe').innerHTML = userInput;
</script>
```

**References.** [MDN · Cross-site scripting (XSS)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS) · [MDN · Node.textContent](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)

---

### 12. CSR vs SSR vs SSG & hydration  `Hard`

**Pattern:** Rendering Strategy

**Problem.** Compare client-side rendering, server-side rendering, and static generation. What is hydration, and what trade-offs drive the choice?

**What it tests.** Whether you can reason about TTFB/FCP/TTI, SEO, and server cost across strategies.

**Approach & answer.** CSR (client-side rendering): the server sends a near-empty HTML shell + a JS bundle; the browser fetches data and builds the DOM. Fast TTFB but slow first paint (blank until JS loads/runs), weaker SEO for crawlers that don't execute JS, and heavy client work — a classic SPA. SSR (server-side rendering): the server renders full HTML per request, so the user sees content fast (good FCP) and crawlers get real markup; costs are higher server load and TTFB, plus the page isn't interactive until HYDRATION completes. SSG (static generation): HTML is rendered at BUILD time and served from a CDN — fastest and cheapest, ideal for content that rarely changes (docs, blogs, marketing); the downside is stale data and rebuilds, softened by ISR (incremental static regeneration) which re-renders pages on a schedule/on-demand. HYDRATION is the step where the client-side framework attaches event listeners and reconciles state onto server-rendered HTML to make it interactive; between paint and hydration the page LOOKS ready but doesn't respond (a UX gap), and shipping the JS to hydrate can be costly — hence newer approaches: partial/progressive hydration, islands architecture (hydrate only interactive widgets), streaming SSR, and React Server Components (send rendered output, not component JS). Choose by need: static marketing → SSG; SEO + dynamic data → SSR/ISR; highly interactive app behind a login → CSR (SEO doesn't matter).

**Use this technique when.** Choosing a rendering strategy / framework mode: weigh SEO, time-to-content, interactivity, and server cost.

```text
             TTFB   First paint   SEO      Server cost   Data freshness
CSR (SPA)    fast   slow (blank)  weak     low           live
SSR          slower fast          strong   high          live (per request)
SSG          fast   fast          strong   lowest (CDN)  stale (build-time; ISR helps)

Hydration = attach JS listeners onto server HTML → interactive.
Gap: page LOOKS ready but ignores clicks until hydration finishes.
Fixes: islands / partial & progressive hydration, streaming SSR, Server Components.
```

**References.** [web.dev · Rendering on the Web](https://web.dev/articles/rendering-on-the-web) · [MDN · Hydration](https://developer.mozilla.org/en-US/docs/Glossary/Hydration)

---

## JavaScript

> These rounds probe whether you actually understand the runtime, not just syntax: closures, `this`, the event loop, prototypes, and coercion. The 'implement from scratch' questions (debounce, throttle, deepClone, Promise.all) are the classic senior filter — they test closures, async, and edge-case thinking at once.

### 1. What is a closure?  `Easy`

**Pattern:** Scope & Closures

**Problem.** Explain closures. Then explain the classic var-in-a-loop bug and how to fix it.

**What it tests.** Whether you understand that functions capture their lexical environment, not a snapshot of values.

**Approach & answer.** A closure is a function bundled with references to its surrounding lexical scope — it keeps those variables alive after the outer function returns. The loop bug: with var, all three callbacks close over the SAME single i, which is 3 by the time they run. Fix with let (block-scoped — a fresh binding per iteration) or an IIFE that captures the current value as an argument. The mental model that unlocks every closure question: a closure captures the variable, not its value at capture time — so a later mutation is visible to the closure. That's why `var` (one function-scoped binding shared by all iterations) misbehaves while `let` (a new binding created per loop iteration) works. Closures are also how JavaScript gets private state without a `private` keyword: variables in the outer scope are reachable only through the returned function.

**Use this technique when.** Closures power data privacy (module pattern), function factories, memoization, and every React hook's captured state.

```js
// Bug: prints 3, 3, 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

// Fix 1: let gives each iteration its own binding -> 0, 1, 2
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}

// Fix 2: IIFE captures the current value
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 0))(i);
}
```

**References.** [MDN · Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures) · [MDN · let](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let)

---

### 2. How does this work?  `Easy`

**Pattern:** this binding

**Problem.** Explain the rules that determine `this`. Why do arrow functions behave differently?

**What it tests.** The four binding rules and the arrow-function exception — a top source of real bugs.

**Approach & answer.** `this` is determined by HOW a function is called, not where it's defined: (1) new binding — new Fn() gives the new object; (2) explicit — call/apply/bind set it; (3) implicit — obj.method() gives obj; (4) default — a standalone call gives undefined (strict) or the global object. Arrow functions ignore all four: they capture `this` lexically from the enclosing scope, which is why they are ideal for callbacks inside methods (no more const self = this). The precedence when several rules could apply, highest to lowest: new > explicit (bind/call/apply) > implicit (method call) > default. So a bound function beats a later method call, and `new` beats even bind. The single most common bug this causes: passing obj.method as a callback (setTimeout(obj.method), onClick={obj.method}) strips the implicit receiver, so `this` falls back to default — fix with obj.method.bind(obj) or an arrow wrapper () => obj.method(). Arrow functions have no `this`, `arguments`, or `prototype` of their own, so they can't be constructors.

**Use this technique when.** Losing `this` when passing a method as a callback — bind it or wrap in an arrow. Never use an arrow for an object method that needs its own `this`.

```js
const obj = {
  name: 'Domo',
  regular() { return this.name; },   // depends on call site
  arrow: () => this?.name,           // 'this' = enclosing scope, NOT obj
};
obj.regular();                 // 'Domo'    (implicit binding)
const f = obj.regular; f();    // undefined (default binding, lost 'this')
obj.regular.call({name:'X'});  // 'X'       (explicit binding)
```

**References.** [MDN · this](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) · [MDN · Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)

---

### 3. var vs let/const, hoisting, and the TDZ  `Easy`

**Pattern:** Scope & Hoisting

**Problem.** Explain hoisting. What is the difference between var, let, and const, and what is the temporal dead zone?

**What it tests.** Understanding of hoisting mechanics and block scope — a very common warm-up that seeds closure-in-loop bugs.

**Approach & answer.** Declarations are 'hoisted': the engine registers them before running the code. A var is function-scoped and initialized to undefined at hoist time, so reading it before its assignment gives undefined (not an error). let and const are block-scoped and also hoisted, but they stay uninitialized in the temporal dead zone (TDZ) from the top of the block until the declaration line — touching them there throws a ReferenceError, which catches typos and use-before-init. const additionally forbids reassignment of the binding (the referenced object can still mutate). Function declarations are fully hoisted and callable before their line; function expressions and arrows follow their variable's rules. Classic trap: var i in a for loop is shared across all iterations, so async callbacks all see the final value — let creates a fresh binding per iteration and fixes it. Prefer const by default, let when you must reassign, and avoid var.

**Use this technique when.** Any 'what does this print' question, closure-in-loop bugs, or reasoning about block scope.

```js
console.log(a); // undefined (var hoisted + initialized)
var a = 1;
// console.log(b); // ReferenceError: b is in the TDZ
let b = 2;

// Loop-closure trap:
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0 1 2
```

**References.** [MDN · Hoisting](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting) · [MDN · let (temporal dead zone)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let)

---

### 4. == vs === and type coercion  `Easy`

**Pattern:** Coercion & Equality

**Problem.** What is the difference between == and ===? When does coercion produce surprising results?

**What it tests.** Knowing coercion rules and why strict equality should be the default.

**Approach & answer.** === (strict) compares type and value with no conversion; == (loose) coerces both operands to a common type first, which produces surprises: 0 == '' and 0 == '0' are true, '' == '0' is false, [] == false is true, and null == undefined is true (but neither == 0). Rule of thumb: always use === / !==. The one pragmatic exception many teams allow is x == null, which is true for exactly null and undefined — a concise nullish check. Also memorize the standalone gotchas: NaN === NaN is false (use Number.isNaN), typeof null is 'object', and objects/arrays compare by reference, not by contents, so {} !== {}. For structural comparison, compare fields explicitly or serialize.

**Use this technique when.** 'What does this evaluate to' trivia, defensive null checks, and code review of equality logic.

```js
0 == '';        // true  (both coerce to 0)
null == undefined; // true
[] == false;    // true  ([] -> '' -> 0, false -> 0)
NaN === NaN;    // false -> use Number.isNaN(x)
{} === {};      // false -> different references

if (value == null) { /* runs for null OR undefined */ }
```

**References.** [MDN · Equality comparisons and sameness](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Equality_comparisons_and_sameness) · [MDN · Strict equality (===)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality)

---

### 5. Event loop: micro vs macro tasks  `Medium`

**Pattern:** Event Loop / Async

**Problem.** What does this log and why? console.log(1); setTimeout(cb2); Promise.resolve().then(cb3); console.log(4);

**What it tests.** Whether you know the microtask queue drains before the next macrotask.

**Approach & answer.** Logs 1, 4, 3, 2. Synchronous code runs first (1, 4). Then the event loop drains ALL microtasks (Promise callbacks) before any macrotask (setTimeout) — so 3 before 2. The rule: after each task, the engine empties the entire microtask queue before rendering or picking up the next timer. The full model: the call stack runs synchronous code to completion; then, on each tick, the loop runs one macrotask (a timer callback, an I/O callback, a UI event), then drains the microtask queue COMPLETELY — including any microtasks those microtasks schedule — before the browser gets a chance to render and before the next macrotask. Microtasks: Promise .then/.catch/.finally, await continuations, queueMicrotask, MutationObserver. Macrotasks: setTimeout/setInterval, message events, I/O. This is why an infinite chain of promises can starve rendering (microtasks never yield), while setTimeout loops let frames paint between iterations.

**Use this technique when.** Explaining why a Promise .then beats a setTimeout(0), why await resumes 'soon' but not synchronously, and starvation bugs where microtasks block rendering.

```js
console.log(1);
setTimeout(() => console.log(2), 0);            // macrotask
Promise.resolve().then(() => console.log(3));   // microtask
console.log(4);
// Output: 1, 4, 3, 2
```

**References.** [MDN · The event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model) · [MDN · Using microtasks](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide)

---

### 6. Implement debounce  `Medium`

**Pattern:** Implement from scratch

**Problem.** Write debounce(fn, delay): return a function that delays calling fn until `delay` ms have passed since the LAST call.

**What it tests.** Closures + timer management. The #1 asked utility. Know debounce vs throttle cold.

**Approach & answer.** Keep a timer id in the closure. Every call clears the pending timer and schedules a new one, so fn only fires once the calls stop for `delay` ms. Preserve `this` and args by using a regular function and fn.apply. Debounce = 'wait for quiet' (search input, resize). Throttle = 'at most once per interval' (scroll, mousemove). Interview-grade extensions to mention: a leading-edge option that fires immediately on the first call then suppresses the trailing one; and a cancel()/flush() method (attach them to the returned function) so callers can abort a pending call on unmount or force it to run now. In React, wrap the debounced function in useMemo/useRef so a new debounced instance isn't created every render (which would reset the timer and defeat the whole thing).

**Use this technique when.** Search-as-you-type, autosave, resize/scroll handlers — anywhere rapid events should collapse into one action.

```js
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Usage: only fires 300ms after the user stops typing
searchInput.addEventListener('input', debounce(query, 300));
```

**References.** [MDN · setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) · [Lodash · _.debounce](https://lodash.com/docs/#debounce)

---

### 7. Implement throttle  `Medium`

**Pattern:** Implement from scratch

**Problem.** Write throttle(fn, limit): fn runs at most once per `limit` ms, no matter how often it's called.

**What it tests.** The difference from debounce, and getting the leading-edge timing right.

**Approach & answer.** Track whether we're in a cooldown. On call, if not cooling down, run immediately and start a timer that re-opens the gate. Calls during the cooldown are ignored (this is the leading-edge variant). Contrast: debounce resets its timer on every call; throttle enforces a steady maximum rate. Two implementation styles worth knowing: the timestamp style (compare Date.now() to the last-run time — simple, leading-edge) and the timer style shown here. The subtle bug in the naive leading-edge version is that the LAST call during a cooldown is dropped, so the UI can end up stale (e.g. the final scroll position never handled); production throttles (lodash) therefore also fire a trailing call with the most recent args when the interval ends. Rule of thumb: throttle for continuous streams where you want regular sampling (scroll, mousemove, resize, drag); debounce for bursts where only the final state matters (typeahead, autosave).

**Use this technique when.** scroll/mousemove/resize where you want regular updates but not on every pixel; rate-limiting API calls.

```js
function throttle(fn, limit) {
  let waiting = false;
  return function (...args) {
    if (waiting) return;
    fn.apply(this, args);
    waiting = true;
    setTimeout(() => { waiting = false; }, limit);
  };
}
```

**References.** [MDN · setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) · [Lodash · _.throttle](https://lodash.com/docs/#throttle)

---

### 8. Prototypal inheritance & the chain  `Medium`

**Pattern:** Prototypes

**Problem.** Explain the prototype chain. What happens on property lookup? How does `class` relate to it?

**What it tests.** Whether you know `class` is syntactic sugar over prototypes, not a new model.

**Approach & answer.** Every object has an internal [[Prototype]] link (read via Object.getPrototypeOf). On property lookup, the engine checks the object, then its prototype, then that prototype's prototype, up the chain until it finds the key or hits null. Methods live on the prototype so all instances share one copy. `class` is sugar: methods go on the prototype, `extends` wires the chain, `super` walks up it. Key distinctions interviewers probe: an instance's [[Prototype]] points to its constructor's .prototype object (not to the constructor itself) — new Dog() links to Dog.prototype, whose [[Prototype]] links to Animal.prototype. Property WRITES don't walk the chain: assigning obj.x creates an own property on obj (shadowing), it never mutates the prototype — which is why shared state on a prototype is a footgun. hasOwnProperty distinguishes own from inherited keys, and for...in walks inherited enumerables while Object.keys returns only own ones. Prefer class/Object.create over the legacy Constructor.prototype = new Parent() pattern.

**Use this technique when.** Understanding instanceof, why adding to Array.prototype affects all arrays, and the memory efficiency of shared methods.

```js
class Animal {
  constructor(name) { this.name = name; }
  speak() { return this.name + ' makes a sound'; }
}
class Dog extends Animal {
  speak() { return super.speak() + ' (woof)'; }
}
const d = new Dog('Rex');
Object.getPrototypeOf(d) === Dog.prototype;               // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
```

**References.** [MDN · Inheritance and the prototype chain](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain) · [MDN · Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)

---

### 9. Deep clone an object  `Medium`

**Pattern:** Implement from scratch

**Problem.** Implement a deep clone. Handle nested objects/arrays and circular references.

**What it tests.** Recursion over unknown structure, and awareness of edge cases (circularity, Date, Map).

**Approach & answer.** Recurse: for objects/arrays, create a new container and clone each value. Guard circular references with a WeakMap of already-cloned sources. Mention structuredClone() as the modern built-in (handles Dates, Maps, Sets, circular refs) — knowing when NOT to hand-roll is senior signal. JSON.parse(JSON.stringify(x)) is the naive answer but drops functions, undefined, and Dates, and throws on cycles. The WeakMap is doing two jobs: correctness (a node that appears twice in the graph is cloned once and shared, preserving identity) and termination (without it, a cycle recurses forever). Edge cases a thorough answer names: preserve the prototype with Object.create(Object.getPrototypeOf(value)) if you care about class instances; handle Map/Set/RegExp/typed arrays explicitly; and note that structuredClone still can't clone functions, DOM nodes, or prototype chains — so for React/Redux state the pragmatic choice is often shallow copies at each changed level (spread) rather than a full deep clone.

**Use this technique when.** Cloning state before mutation (Redux/immutability), snapshotting config. Prefer structuredClone or a library in production.

```js
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== 'object') return value;
  if (value instanceof Date) return new Date(value);
  if (seen.has(value)) return seen.get(value);        // circular ref
  const copy = Array.isArray(value) ? [] : {};
  seen.set(value, copy);
  for (const key of Object.keys(value)) {
    copy[key] = deepClone(value[key], seen);
  }
  return copy;
}
// Modern built-in: structuredClone(value)
```

**References.** [MDN · structuredClone()](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) · [MDN · WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)

---

### 10. Implement curry  `Medium`

**Pattern:** Implement from scratch

**Problem.** Write curry(fn) so that sum(1)(2)(3), sum(1,2)(3) and sum(1,2,3) all work for a 3-arg fn.

**What it tests.** Closures + fn.length + recursion. Tests functional-programming fluency.

**Approach & answer.** Collect arguments across calls. If we've gathered at least fn.length args, invoke; otherwise return a function that keeps collecting. fn.length gives the expected arity. This is closures capturing accumulated args. Each partial call returns a new function that closes over the args gathered so far, so the accumulation is immutable per branch — sum(1) and sum(2) don't interfere. fn.length counts only parameters before the first default/rest parameter, so currying a variadic function (...args) reports arity 0 and fires immediately; for those you need an explicit arity argument. Currying (one arg at a time) is a special case of partial application (fix any number of args up front); both trade generality for reusable, pre-configured functions and enable point-free composition.

**Use this technique when.** Building reusable specialized functions, point-free pipelines, partial application of config.

```js
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args);
    return (...next) => curried.apply(this, [...args, ...next]);
  };
}
const sum = curry((a, b, c) => a + b + c);
sum(1)(2)(3);   // 6
sum(1, 2)(3);   // 6
sum(1)(2, 3);   // 6
```

**References.** [MDN · Function.length](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length) · [Wikipedia · Currying](https://en.wikipedia.org/wiki/Currying)

---

### 11. Implement Function.prototype.bind  `Medium`

**Pattern:** Implement from scratch

**Problem.** Implement your own bind, and explain how it relates to call and apply.

**What it tests.** Deep understanding of `this` binding, partial application, and function invocation.

**Approach & answer.** call and apply invoke a function immediately with an explicit `this`: call takes arguments individually, apply takes them as an array. bind does not invoke — it returns a new function that, when later called, runs the original with the bound `this` and any pre-filled (curried) leading arguments, concatenated with the arguments passed at call time. A faithful polyfill also handles being called as a constructor with `new`: in that case the bound `this` must be ignored and the newly-created instance used instead, while the prototype chain is preserved. Signal to reach for bind: fixing `this` for a detached callback (React class handlers, setTimeout), or partial application. Modern code often replaces bind with arrow functions (which capture `this` lexically) for callbacks.

**Use this technique when.** Partial application, fixing `this` for callbacks, and understanding legacy class-component handlers.

```js
Function.prototype.myBind = function (ctx, ...bound) {
  const fn = this;
  function boundFn(...args) {
    // If called with 'new', ignore ctx and use the fresh instance.
    const self = this instanceof boundFn ? this : ctx;
    return fn.apply(self, [...bound, ...args]);
  }
  boundFn.prototype = Object.create(fn.prototype || null);
  return boundFn;
};
```

**References.** [MDN · Function.prototype.bind()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) · [MDN · Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call)

---

### 12. Implement an EventEmitter (pub/sub)  `Medium`

**Pattern:** Implement from scratch

**Problem.** Build a small event emitter supporting on, off, emit, and once.

**What it tests.** Closures, data-structure choice, and the observer pattern that underpins most UI event code.

**Approach & answer.** Keep a Map from event name to a Set of listener functions. on adds a listener (returning an unsubscribe function is a nice ergonomic touch that mirrors addEventListener and RxJS). off removes one. emit iterates the set and calls each listener with the payload — iterate over a copy so a listener that unsubscribes mid-emit doesn't corrupt the iteration. once wraps the listener in a self-removing wrapper so it fires at most one time. A Map of Sets gives O(1) add/remove and naturally de-dupes identical listeners. This is the observer pattern: it decouples producers from consumers, which is exactly how DOM events, Node's EventEmitter, Redux subscriptions, and custom hooks over external stores work.

**Use this technique when.** Decoupling modules, bridging non-React stores into hooks, and cross-component signaling without prop drilling.

```js
class EventEmitter {
  #map = new Map();
  on(name, fn) {
    if (!this.#map.has(name)) this.#map.set(name, new Set());
    this.#map.get(name).add(fn);
    return () => this.off(name, fn);   // unsubscribe handle
  }
  off(name, fn) { this.#map.get(name)?.delete(fn); }
  emit(name, ...args) {
    for (const fn of [...(this.#map.get(name) ?? [])]) fn(...args);
  }
  once(name, fn) {
    const wrap = (...a) => { this.off(name, wrap); fn(...a); };
    return this.on(name, wrap);
  }
}
```

**References.** [MDN · EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) · [Node.js · EventEmitter](https://nodejs.org/api/events.html#class-eventemitter)

---

### 13. Generators and iterators  `Medium`

**Pattern:** Iteration Protocols

**Problem.** What are the iterable and iterator protocols? How do generators implement them, and what problems do they solve?

**What it tests.** Understanding lazy sequences, custom iteration, and how generators pause/resume execution.

**Approach & answer.** An object is an iterator if it has a next() method returning { value, done }. It is iterable if it has a [Symbol.iterator]() method returning such an iterator — that is what for...of, spread, and destructuring consume. Generators (function*) are the ergonomic way to produce both: calling one returns a generator object that is simultaneously an iterator AND iterable, and each yield pauses execution, handing a value out and suspending the function's entire stack frame until next() resumes it. This lazy, pull-based evaluation is the payoff: you can model infinite sequences (an ID generator), stream large or expensive data without materializing it all, and write stateful iteration as straight-line code instead of a hand-rolled state machine. yield* delegates to another iterable. Generators also accept values back in via next(value) (two-way communication) and were the mechanism async/await was originally built on. async generators (async function*) + for-await-of extend this to asynchronous streams like paginated APIs.

**Use this technique when.** Lazy/infinite sequences, custom for...of over your own data structures, streaming pagination, and coroutine-style control flow.

**Complexity.** Lazy: O(1) memory per step regardless of sequence length.

```js
function* idGenerator() {
  let id = 1;
  while (true) yield id++;      // infinite, but pulled one at a time
}
const ids = idGenerator();
ids.next().value; // 1
ids.next().value; // 2

// Make a custom object iterable:
const range = {
  from: 1, to: 3,
  *[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) yield i; }
};
[...range]; // [1, 2, 3]
```

**References.** [MDN · Iteration protocols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) · [MDN · function*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*)

---

### 14. Symbols and well-known symbols  `Medium`

**Pattern:** Symbols

**Problem.** What are Symbols, why are they useful, and what are well-known symbols?

**What it tests.** Understanding unique property keys and the hooks that customize built-in language behavior.

**Approach & answer.** A Symbol is a unique, immutable primitive; every Symbol('desc') is distinct even with the same description, so it can be used as a property key that will never collide with another key — including keys added by other libraries on the same object. That makes symbols ideal for non-enumerable-ish metadata and quasi-private fields (they don't show up in for...in or JSON.stringify, though Object.getOwnPropertySymbols and Reflect.ownKeys can still reach them, so they're not true privacy — use # class fields for that). Symbol.for(key) uses a global registry to share a symbol across realms/files by string key. Well-known symbols are built-in symbols the engine looks up to customize language behavior: Symbol.iterator (makes an object iterable for for...of/spread), Symbol.asyncIterator (for-await-of), Symbol.hasInstance (customize instanceof), Symbol.toPrimitive (control coercion), and Symbol.toStringTag (the [object X] tag). Implementing these lets your objects integrate with core syntax rather than requiring special methods.

**Use this technique when.** Collision-free metadata keys on shared objects, making objects iterable/awaitable, and customizing instanceof/coercion via well-known symbols.

```js
const id = Symbol('id');
const user = { name: 'Ada', [id]: 42 };
JSON.stringify(user);        // '{"name":"Ada"}' — symbol key is skipped
Object.keys(user);           // ['name']

// Well-known symbol: control instanceof
class Even {
  static [Symbol.hasInstance](n) { return n % 2 === 0; }
}
4 instanceof Even; // true
3 instanceof Even; // false
```

**References.** [MDN · Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) · [MDN · Well-known symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#well-known_symbols)

---

### 15. WeakMap, WeakSet, and avoiding memory leaks  `Medium`

**Pattern:** Memory Management

**Problem.** How do WeakMap and WeakSet differ from Map and Set, and how do they help prevent memory leaks?

**What it tests.** Understanding garbage collection, weak references, and common leak sources.

**Approach & answer.** JavaScript reclaims memory via garbage collection: an object is collected once nothing reachable references it. A Map/Set holds STRONG references to its keys/values, so anything stored there stays alive as long as the collection does — a classic leak when you use it as a side cache keyed by objects (DOM nodes, component instances) and forget to delete entries. WeakMap (object keys only) and WeakSet hold WEAK references: if the key object becomes otherwise unreachable, the entry is garbage-collected automatically. That makes them the right tool for associating private/auxiliary data with an object whose lifetime you don't control — per-node metadata, memoization keyed by object identity, marking 'seen' objects — without pinning those objects in memory. The trade-off: because entries can vanish at any time, WeakMap/WeakSet are not enumerable and have no size or iteration. Common leak sources to name in an interview: forgotten timers/intervals, detached DOM nodes still referenced in JS, event listeners never removed, and closures capturing large scopes. WeakRef and FinalizationRegistry give even lower-level control but are rarely needed and should be a last resort.

**Use this technique when.** Caching or attaching metadata keyed by object identity, tracking objects you don't own, and any per-object side table that must not prevent GC.

**Complexity.** O(1) get/set/has; entries auto-released when keys are unreachable.

```js
const metadata = new WeakMap();
function tag(node) {
  metadata.set(node, { lastSeen: Date.now() }); // no leak: entry dies with node
}
// When 'node' is removed from the DOM and no other reference exists,
// both the node AND its WeakMap entry become eligible for GC.

// A plain Map here would keep every node alive forever:
// const metadata = new Map(); // leak if you never .delete()
```

**References.** [MDN · WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) · [MDN · Memory management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management)

---

### 16. Tagged template literals  `Medium`

**Pattern:** Templates & Metaprogramming

**Problem.** What is a tagged template literal, and what are its practical uses?

**What it tests.** Understanding how a function can intercept and process the parts of a template literal.

**Approach & answer.** A tagged template is a function invoked with a template literal, where the function receives the literal's static string parts and its interpolated values separately: the first argument is an array of the string segments (with a .raw property holding un-escaped versions), and the remaining arguments are the evaluated expressions. This lets the tag transform or sanitize the interpolations rather than blindly concatenating — the key safety and power difference from a plain template string. Practical uses: escaping/sanitizing to prevent injection (auto-escaping HTML or SQL interpolations), CSS-in-JS (styled-components' styled.div`...` is a tag), internationalization/formatting, GraphQL's gql`...` for parsing queries, and String.raw for keeping backslashes literal (useful in regex/paths). The mental model: `tag`x${y}z`` calls tag(['x','z'], y). Reach for it when you need to process interpolated values consistently at the boundary — anywhere raw string concatenation would be unsafe or repetitive.

**Use this technique when.** Auto-escaping user input in HTML/SQL, CSS-in-JS, embedded DSLs (GraphQL), and String.raw for literal backslashes.

```js
function safeHtml(strings, ...values) {
  const esc = s => String(s).replace(/[&<>]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;' }[c]));
  return strings.reduce((out, str, i) =>
    out + str + (i < values.length ? esc(values[i]) : ''), '');
}
const name = '<script>';
safeHtml`Hi ${name}!`;   // "Hi &lt;script&gt;!"

String.raw`C:\path`;      // "C:\path" (backslash kept literal)
```

**References.** [MDN · Template literals (tagged templates)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) · [MDN · String.raw()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw)

---

### 17. ES Modules vs CommonJS  `Medium`

**Pattern:** Modules

**Problem.** What is the difference between ES Modules and CommonJS, and why does it matter?

**What it tests.** Understanding the two module systems, static vs dynamic resolution, and live bindings.

**Approach & answer.** CommonJS (require/module.exports) was Node's original system: it is synchronous and dynamic — require() runs at call time, resolves and executes the module, and returns a COPY of the exported values (a snapshot). Because it's dynamic you can require conditionally, but tools can't statically know the dependency graph. ES Modules (import/export) are the standard, and are static: imports/exports are resolved before execution by parsing, so the dependency graph is known ahead of time. That statically-analyzable structure is what enables tree-shaking (dead-export elimination) and is why import statements must be top-level. ESM exports are LIVE BINDINGS, not copies — if the exporting module reassigns an exported variable, importers see the new value; CJS would not. ESM is asynchronous-friendly and supports top-level await; dynamic import() returns a promise for code-splitting/lazy loading. Interop friction (naming, __dirname, JSON imports, the .mjs/.cjs/`type: module` rules) exists because the systems differ fundamentally. Rule of thumb: author ESM for new code (browser-native, tree-shakeable, future-proof); understand CJS for legacy Node and its require semantics.

**Use this technique when.** Explaining bundler tree-shaking, code-splitting via dynamic import(), and debugging Node interop / live-binding surprises.

```js
// CommonJS — synchronous, dynamic, exports a snapshot
const fs = require('fs');
module.exports = { helper };

// ES Modules — static, tree-shakeable, live bindings
import { helper } from './util.js';
export const helper = () => {};

// Lazy load a chunk on demand (both worlds):
const mod = await import('./heavy.js'); // returns a Promise
```

**References.** [MDN · JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) · [Node.js · ECMAScript modules](https://nodejs.org/api/esm.html)

---

### 18. async/await — desugaring, sequential vs parallel, error handling  `Medium`

**Pattern:** Async / await

**Problem.** How does async/await actually work under the hood, and how do you avoid accidentally serialising independent awaits?

**What it tests.** Whether you understand async/await is syntax over promises — and can spot the classic 'await in a loop' performance trap.

**Approach & answer.** An async function ALWAYS returns a promise; `return x` resolves it with x, `throw` rejects it. `await p` suspends the function until p settles, then resumes with the resolved value (or throws the rejection reason into the surrounding try/catch). It is pure syntax over promises + the microtask queue — nothing runs on a separate thread; the function yields control back to the event loop at each await and resumes as a microtask when the awaited promise settles. The #1 mistake is treating await as 'do this, then that' when the operations are INDEPENDENT: `const a = await f(); const b = await g();` runs g only after f finishes (sequential, sum of both latencies). If they don't depend on each other, kick both off first and await together: `const [a, b] = await Promise.all([f(), g()])` (parallel, max of the two). The same trap hides inside `for` loops with `await` in the body — each iteration waits for the previous; map to a promise array and Promise.all instead when order-independent. Error handling: wrap awaits in try/catch, or attach .catch to the returned promise. Remember an un-awaited async call is a floating promise — unhandled rejections can crash Node; always await or .catch.

**Use this technique when.** Sequencing async work; converting promise chains to readable linear code; and diagnosing slow request waterfalls that should be parallel.

```js
// Sequential — b waits for a even though they're independent (SLOW)
async function slow() {
  const a = await fetchUser();     // e.g. 100ms
  const b = await fetchOrders();   // + 100ms  => ~200ms total
  return { a, b };
}

// Parallel — start both, then await together (FAST ~100ms)
async function fast() {
  const [a, b] = await Promise.all([fetchUser(), fetchOrders()]);
  return { a, b };
}

// Error handling + the loop trap
async function run(ids) {
  try {
    // WRONG: serialises N requests
    // for (const id of ids) results.push(await get(id));
    // RIGHT: fire all, await once
    return await Promise.all(ids.map(id => get(id)));
  } catch (err) {
    console.error('one failed:', err);
    throw err; // rethrow so callers can react
  }
}
```

**References.** [MDN · async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) · [MDN · await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)

---

### 19. Optional chaining ?. , nullish coalescing ?? , and logical assignment  `Medium`

**Pattern:** Safe Access & Defaults

**Problem.** Explain ?. and ?? , how ?? differs from || , and what ??= / ||= / &&= do.

**What it tests.** Precise semantics of nullish (null/undefined) vs falsy — the subtle bug source when defaulting 0 or empty string.

**Approach & answer.** Optional chaining `a?.b` short-circuits to `undefined` (not an error) the moment the value to its LEFT is null or undefined, so `user?.address?.city` is safe when address is missing. It works for property access, dynamic keys `a?.[k]`, and calls `fn?.()` (calls only if fn is not nullish). Crucially it short-circuits the WHOLE remaining chain — `a?.b.c.d` stops at a being nullish and doesn't touch b.c.d. Nullish coalescing `x ?? fallback` returns fallback ONLY when x is null or undefined — unlike `||`, which also falls back on any falsy value (0, '', false, NaN). That difference is the classic bug: `count || 10` turns a legitimate 0 into 10; `count ?? 10` keeps the 0. The two combine idiomatically: `const city = user?.address?.city ?? 'Unknown'`. Logical assignment operators are the compound forms: `x ??= v` assigns v only if x is nullish; `x ||= v` assigns if x is falsy; `x &&= v` assigns if x is truthy — each short-circuits (skips the assignment, and evaluating v, when the condition isn't met), which is useful for lazy defaulting without clobbering existing values.

**Use this technique when.** Reading deep/optional API payloads, applying defaults where 0 and '' are valid, and lazily initialising config without overwriting.

```js
const user = { address: null };

// Optional chaining: no TypeError, whole tail short-circuits
user?.address?.city;        // undefined
user.save?.();              // no-op if save is not a function

// ?? vs || — the 0 / '' trap
const qty = 0;
qty || 5;    // 5   (0 is falsy — bug!)
qty ?? 5;    // 0   (0 is a real value — correct)

// Combine for safe deep read + real default
const city = user?.address?.city ?? 'Unknown';

// Logical assignment (short-circuits)
const opts = { retries: 0 };
opts.timeout ??= 3000;   // sets 3000 (was undefined)
opts.retries ??= 3;      // stays 0  (already defined)
opts.mode   ||= 'auto';  // sets 'auto' (was falsy)
```

**References.** [MDN · Optional chaining (?.)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) · [MDN · Nullish coalescing (??)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing)

---

### 20. Destructuring, defaults, and rest/spread in depth  `Medium`

**Pattern:** Destructuring

**Problem.** Show non-trivial destructuring: renaming, nested, defaults, rest, and swapping — and where defaults actually fire.

**What it tests.** Fluency with the assignment-target grammar, and knowing defaults trigger only on undefined (not null).

**Approach & answer.** Destructuring unpacks arrays by position and objects by key into bindings. Object destructuring can rename (`{ a: x }` binds x), reach into nested shapes (`{ user: { name } }`), and supply defaults (`{ page = 1 }`). Array destructuring binds by index, can SKIP holes (`[, , third]`), and collects the tail with a rest element (`[first, ...rest]`). The rest pattern also works on objects (`{ id, ...others }`) — a clean way to omit a key while keeping the remainder (great for React prop forwarding). Two precise rules interviewers probe: (1) a default value fires ONLY when the source value is `undefined`, never when it is `null` — `const { x = 5 } = { x: null }` yields null, not 5. (2) Defaults are evaluated lazily and can reference earlier bindings (`{ a, b = a * 2 }`). Combined with default parameters you get self-documenting function signatures: `function f({ retries = 3, signal } = {})` — the `= {}` guard lets you call f() with no argument at all. Swapping without a temp is the one-liner `[a, b] = [b, a]`. Watch the gotcha: a statement STARTING with `{` is parsed as a block, so wrap standalone object-destructuring assignments in parens: `({ a } = obj)`.

**Use this technique when.** Cleanly pulling fields from props/options objects, forwarding 'the rest' of props, and writing ergonomic optional-config function signatures.

```js
// Rename + nested + default (default fires only on undefined)
const res = { data: { user: { name: 'Ada' } }, x: null };
const { data: { user: { name } }, x = 5 } = res;
// name === 'Ada', x === null  (NOT 5 — null is a value)

// Array skip + rest
const [first, , third, ...rest] = [10, 20, 30, 40, 50];
// first=10, third=30, rest=[40,50]

// Object rest = omit a key, keep remainder (prop forwarding)
const props = { id: 1, className: 'btn', onClick: null };
const { className, ...forwarded } = props;

// Default params with destructured, guarded options object
function connect({ retries = 3, timeout = 3000 } = {}) {
  return { retries, timeout };
}
connect();                 // { retries: 3, timeout: 3000 }

// Swap without a temp
let a = 1, b = 2;
[a, b] = [b, a];           // a=2, b=1
```

**References.** [MDN · Destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) · [MDN · Default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters)

---

### 21. Promise.all vs allSettled vs race vs any  `Medium`

**Pattern:** Promise Combinators

**Problem.** Compare the four Promise combinators. When do you reach for each, and how does failure behave?

**What it tests.** Choosing the right aggregation semantics — especially all-or-nothing vs collect-all-outcomes.

**Approach & answer.** All four take an iterable of promises and return one promise, but they settle differently. Promise.all resolves to an array of all values IF every input resolves; it rejects IMMEDIATELY on the first rejection (fail-fast) — use it when you need all results and any failure invalidates the whole batch. Note the others keep running even after all() rejects (there's no cancellation in JS promises). Promise.allSettled NEVER rejects: it waits for every input and resolves to an array of `{status:'fulfilled', value}` / `{status:'rejected', reason}` objects — use it when you want every outcome regardless of individual failures (e.g. fire N independent requests and render partial results). Promise.race settles as soon as the FIRST input settles, adopting its value OR rejection — use for timeouts (race real work against a reject-after-Xms promise) or first-response-wins. Promise.any resolves with the first FULFILLED value, ignoring rejections; it rejects only if ALL inputs reject, with an AggregateError whose `.errors` holds every reason — use for redundancy (try several mirrors, take whichever succeeds first). Mnemonic: all = every value or first error; allSettled = every outcome; race = first to settle either way; any = first success or all-failed AggregateError.

**Use this technique when.** Fan-out requests (all/allSettled), timeouts and first-wins (race), and redundant/fallback sources (any).

```js
// all — fail fast; one rejection rejects the whole thing
await Promise.all([getA(), getB()]);        // [a, b] or throws

// allSettled — collect every outcome, never rejects
const rs = await Promise.allSettled([getA(), getB()]);
rs.filter(r => r.status === 'fulfilled').map(r => r.value);

// race — first to SETTLE wins (used for timeouts)
const timeout = new Promise((_, rej) =>
  setTimeout(() => rej(new Error('timeout')), 5000));
await Promise.race([fetchData(), timeout]);

// any — first to SUCCEED wins; all-fail => AggregateError
try {
  await Promise.any([mirror1(), mirror2(), mirror3()]);
} catch (e) {
  console.log(e.errors);   // every rejection reason
}
```

**References.** [MDN · Promise.allSettled()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) · [MDN · Promise.any()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any) · [MDN · Promise.race()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race)

---

### 22. Dynamic import() and top-level await  `Medium`

**Pattern:** Modules / Async Loading

**Problem.** Contrast static import with dynamic import(), and explain top-level await and its trade-offs.

**What it tests.** Code-splitting mechanics, conditional/lazy loading, and how top-level await affects a module graph.

**Approach & answer.** Static `import ... from` is hoisted, resolved before execution, and must sit at the top level — that static shape is what lets bundlers build the dependency graph and tree-shake. Dynamic `import(specifier)` is a function-like operator that returns a PROMISE for the module namespace object; it can appear anywhere (inside conditionals, event handlers, functions) and the specifier can be computed at runtime. Bundlers turn each dynamic import into a separate CHUNK, so it's the primary mechanism for code-splitting and lazy loading — load a heavy editor/chart library only when the feature is actually used, shrinking the initial bundle. You consume it with await or .then, and destructure named exports off the namespace (`const { render } = await import('./chart.js')`). Top-level await lets an ES MODULE use `await` at module scope (no wrapping async function) — handy for modules that must resolve async config, a DB connection, or a dynamically chosen dependency before exporting. The trade-off: a module with top-level await becomes async, so every importer WAITS for it to finish evaluating before their own code runs — it can serialise and slow the module graph's startup, and it only works in ESM (not CommonJS). Use it for genuine module-initialisation needs, not as a convenience.

**Use this technique when.** Route-based/feature code-splitting, conditionally loading polyfills or heavy libs, and module-level async initialisation.

```js
// Static — hoisted, tree-shakeable, top-level only
import { debounce } from './util.js';

// Dynamic — lazy chunk, runs on demand, specifier can be computed
button.addEventListener('click', async () => {
  const { openEditor } = await import('./editor.js'); // own chunk
  openEditor();
});

// Conditional / computed specifier
const locale = navigator.language.startsWith('fr') ? 'fr' : 'en';
const messages = await import(`./i18n/${locale}.js`);

// Top-level await (ESM only) — importers wait for this module
const config = await fetch('/config.json').then(r => r.json());
export const apiBase = config.apiBase;
```

**References.** [MDN · import() (dynamic import)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) · [MDN · JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)

---

### 23. Private class fields and methods (#)  `Medium`

**Pattern:** Class Encapsulation

**Problem.** How do #private class fields work, and how do they differ from closures, WeakMap privacy, or a leading-underscore convention?

**What it tests.** Real language-level encapsulation vs conventions, plus the brand-check idiom.

**Approach & answer.** A field or method prefixed with `#` is TRULY private: it's accessible only inside the class body, enforced by the language (not a convention). Unlike a leading `_name`, `#name` is invisible to outside code, to `Object.keys`, to `for...in`, to JSON.stringify, and to Proxies — reaching for `obj.#x` outside the class is a SYNTAX error, caught at parse time, not a runtime undefined. This beats the older patterns: the leading-underscore convention is just discipline (anyone can still touch it); closure-based privacy (capturing vars in the constructor) truly hides state but creates a fresh copy of every method per instance (memory cost) and can't be shared on the prototype; the WeakMap pattern works and predates `#` but is verbose. Private members can be fields, methods, getters/setters, and STATIC (`static #count`). A powerful idiom is the brand check: `#field in obj` is a boolean that tells you whether obj was constructed by this class (it has the private slot) WITHOUT throwing — useful for `static isInstance(x)` guards that work even across realms where instanceof is unreliable. Caveats: private names aren't reflectable (that's the point — no metaprogramming access), and they're per-class, so a subclass can't see the parent's `#` members.

**Use this technique when.** Enforcing invariants no consumer can bypass, hiding internal state from serialisation/Proxies, and writing robust type-guard helpers via brand checks.

```js
class Counter {
  #count = 0;                 // private instance field
  static #instances = 0;      // private static field
  constructor() { Counter.#instances++; }

  #clamp(n) { return Math.max(0, n); }   // private method
  increment() { this.#count = this.#clamp(this.#count + 1); }
  get value() { return this.#count; }

  // Brand check — true only for real Counter instances, never throws
  static isCounter(obj) { return #count in obj; }
}

const c = new Counter();
c.increment();
c.value;                 // 1
// c.#count;             // SyntaxError at parse time
Object.keys(c);          // []  — #count is invisible
Counter.isCounter(c);    // true
Counter.isCounter({});   // false
```

**References.** [MDN · Private properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties) · [MDN · Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes)

---

### 24. Immutable array copies (toSorted/toReversed/with) & structuredClone  `Medium`

**Pattern:** Immutable Operations

**Problem.** Which array methods mutate vs return a copy, and how do the newer change-by-copy methods plus structuredClone help state management?

**What it tests.** Knowing the mutating traps (sort/reverse/splice) and the modern non-mutating alternatives for React/Redux state.

**Approach & answer.** Several classic Array methods MUTATE in place and surprise people: sort(), reverse(), splice(), push/pop/shift/unshift, fill, copyWithin — calling `state.sort()` in React silently mutates the existing array (breaking referential-equality change detection) and returns the same reference. ES2023 added change-by-copy counterparts that leave the original untouched and return a NEW array: `toSorted()` (copy of sort), `toReversed()` (copy of reverse), `toSpliced(start, delete, ...items)` (copy of splice), and `with(index, value)` (copy with one index replaced — the immutable alternative to `arr[i] = v`). These are exactly what you want for immutable state updates: `setItems(items.toSorted(cmp))` gives a fresh array so React sees a new reference. For DEEP copies of nested objects/arrays, the spread operator and Object.assign are only SHALLOW (nested references are shared); `structuredClone(value)` (a global) makes a true deep clone, handling nested structures, Dates, Maps, Sets, typed arrays, and even cyclic references — things JSON.parse(JSON.stringify(x)) silently corrupts (drops functions/undefined, mangles Dates, throws on cycles). structuredClone can't copy functions, DOM nodes, or class prototypes (it throws / returns plain objects). Rule: use with/toSorted/toSpliced for one-level array edits, structuredClone for deep nested clones, spread for shallow.

**Use this technique when.** Updating React/Redux state without mutation, sorting/reordering derived data safely, and deep-cloning nested config or cached payloads.

**Complexity.** Copy methods are O(n); structuredClone is O(size of graph).

```js
const nums = [3, 1, 2];

// MUTATES original, returns same ref — bug in React state
// nums.sort();

// Change-by-copy (ES2023): original untouched, new array back
const sorted   = nums.toSorted((a, b) => a - b); // [1,2,3]
const reversed = nums.toReversed();               // [2,1,3]
const replaced = nums.with(0, 99);                // [99,1,2]
const spliced  = nums.toSpliced(1, 1, 'x');       // [3,'x',2]
// nums is still [3,1,2]

// Shallow vs deep
const state = { list: [{ id: 1 }], when: new Date() };
const shallow = { ...state };            // shares state.list ref
const deep = structuredClone(state);     // fully independent, Date preserved
```

**References.** [MDN · Array.prototype.toSorted()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted) · [MDN · Array.prototype.with()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/with) · [MDN · structuredClone()](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone)

---

### 25. Polyfill Array map / filter / reduce  `Medium`

**Pattern:** Polyfills

**Problem.** Reimplement Array.prototype.map, filter, and reduce from scratch. What edge cases must a faithful polyfill handle?

**What it tests.** Knowing the exact callback contract (value, index, array), the this-arg, sparse-array skipping, and reduce's no-initial-value rule.

**Approach & answer.** These are the bread-and-butter polyfill challenges — the interviewer is checking you know the SIGNATURE and edge cases, not just the happy path. The callback everywhere is `(element, index, array)`, and map/filter accept an optional `thisArg` bound via callback.call(thisArg, ...). map returns a NEW array of the same length with each element transformed; filter returns a new array containing only elements for which the callback is truthy. Sparse arrays: native map/filter/reduce SKIP holes (indices never assigned) — a faithful polyfill guards each index with `Object.hasOwn(this, i)` (or `i in this`) so holes stay holes rather than becoming undefined. reduce is the subtle one: with an initialValue, the accumulator starts there and iteration begins at index 0; WITHOUT an initialValue, the accumulator is the first present element and iteration starts at the next — and calling reduce on an empty array with no initial value must THROW a TypeError ('Reduce of empty array with no initial value'). All three should validate the callback is a function (throw TypeError otherwise) and read length once up front. Getting reduce's two-mode initialisation and the empty-array throw right is what separates a real answer from a toy one.

**Use this technique when.** Interview polyfill rounds, understanding what native iteration methods actually guarantee, and reasoning about sparse arrays and reduce's initial-value semantics.

**Complexity.** O(n) time, O(n) space for map/filter, O(1) extra for reduce.

```js
Array.prototype.myMap = function (cb, thisArg) {
  if (typeof cb !== 'function') throw new TypeError('cb must be a function');
  const out = new Array(this.length);
  for (let i = 0; i < this.length; i++) {
    if (Object.hasOwn(this, i)) out[i] = cb.call(thisArg, this[i], i, this);
  }
  return out;
};

Array.prototype.myFilter = function (cb, thisArg) {
  const out = [];
  for (let i = 0; i < this.length; i++) {
    if (Object.hasOwn(this, i) && cb.call(thisArg, this[i], i, this)) out.push(this[i]);
  }
  return out;
};

Array.prototype.myReduce = function (cb, initial) {
  if (typeof cb !== 'function') throw new TypeError('cb must be a function');
  const len = this.length;
  const noInit = arguments.length < 2;
  if (noInit && len === 0) throw new TypeError('Reduce of empty array with no initial value');
  let acc = noInit ? this[0] : initial;
  let i = noInit ? 1 : 0;
  for (; i < len; i++) if (Object.hasOwn(this, i)) acc = cb(acc, this[i], i, this);
  return acc;
};
```

**References.** [MDN · Array.prototype.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) · [MDN · Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)

---

### 26. Polyfill Function.prototype.call & apply  `Medium`

**Pattern:** Polyfills / this binding

**Problem.** Implement call and apply from scratch. How do you invoke a function with an explicit `this` without using call/apply/bind?

**What it tests.** The core trick: a function called as a METHOD gets its object as `this` — so temporarily attach the fn to the target, invoke, then clean up.

**Approach & answer.** The insight is that JavaScript's implicit `this` binding does the work for you: when you call `obj.fn()`, `this` inside fn IS obj. So to force a function to run with an arbitrary `this`, you TEMPORARILY make it a property of that object, call it as a method, then remove the property. `call` takes the this-arg then individual arguments; `apply` is identical except it takes an array of arguments. Robust details: (1) coerce the this-arg — null/undefined should become globalThis (non-strict semantics), and primitives should be boxed via Object(thisArg) so the property assignment works. (2) Use a unique Symbol as the temporary key so you never collide with or clobber a real property on the target object. (3) Wrap the invocation in try/finally and `delete` the temp key in finally, so you clean up even if the function throws. (4) Return the function's result. Once you have call, apply is a one-liner (`this.call(thisArg, ...args)`) and vice versa — they're duals. This is the mechanism behind method borrowing (e.g. `Array.prototype.slice.call(arguments)`), and it's the foundation bind builds on (bind returns a new function that internally applies the saved this-arg and partial args).

**Use this technique when.** Explaining how `this` binding is implemented, method borrowing, and building bind/partial-application from first principles.

**Complexity.** O(1) overhead plus the wrapped call.

```js
Function.prototype.myCall = function (thisArg, ...args) {
  // null/undefined -> global; primitives -> boxed object
  thisArg = (thisArg === null || thisArg === undefined) ? globalThis : Object(thisArg);
  const key = Symbol('fn');          // unique, no collision
  thisArg[key] = this;               // 'this' is the function myCall was called on
  try {
    return thisArg[key](...args);    // called as a method => this === thisArg
  } finally {
    delete thisArg[key];             // clean up even on throw
  }
};

Function.prototype.myApply = function (thisArg, args = []) {
  return this.myCall(thisArg, ...args);   // apply = call with an array
};

const person = { name: 'Ada' };
function greet(greeting) { return greeting + ', ' + this.name; }
greet.myCall(person, 'Hi');      // "Hi, Ada"
greet.myApply(person, ['Hey']);  // "Hey, Ada"
```

**References.** [MDN · Function.prototype.call()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) · [MDN · Function.prototype.apply()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)

---

### 27. Deep get by path string (lodash.get)  `Medium`

**Pattern:** Safe Access

**Problem.** Implement get(obj, path, defaultValue) that safely reads a nested value via a 'a.b[0].c' path, returning the default if any link is missing.

**What it tests.** Path parsing (dot + bracket notation), null-safe traversal, and distinguishing 'missing' from a legitimately-undefined leaf.

**Approach & answer.** This is the runtime cousin of optional chaining, needed when the path is a DYNAMIC string (config keys, form field names, i18n). The algorithm: normalise the path into an array of keys, then walk the object one key at a time, bailing to the default the instant the current value is null/undefined. Path normalisation is the tricky part — you must support both dot notation and bracket/array indices, so convert `a[0].b` into `['a','0','b']`: a common approach is `path.replace(/\[(\w+)\]/g, '.$1')` to turn brackets into dots, strip a leading dot, then split on '.'. (If the caller already passes an array of keys, use it directly.) Walk with a guard: `while (obj != null && index < length) { obj = obj[keys[index++]]; }`. The important subtlety is the RETURN condition: only return the default when traversal stopped EARLY (we didn't consume the whole path because we hit a nullish link) — i.e. `return index === length ? obj : defaultValue`. That means if the full path resolves to a real `undefined` leaf, you return that undefined, not the default — matching lodash semantics. This cleanly handles arrays (numeric string keys index them), missing intermediate objects, and a nullish root.

**Use this technique when.** Reading deeply nested config/API data by a computed string path, form libraries, and anywhere optional chaining can't be written literally.

**Complexity.** O(d) where d is path depth.

```js
function get(obj, path, defaultValue) {
  const keys = Array.isArray(path)
    ? path
    : path.replace(/\[(\w+)\]/g, '.$1')  // a[0] -> a.0
          .replace(/^\./, '')             // drop leading dot
          .split('.');
  let index = 0;
  const length = keys.length;
  while (obj != null && index < length) {
    obj = obj[String(keys[index])];
    index++;
  }
  // Only fall back if we bailed out early (hit a nullish link)
  return index === length && obj !== undefined ? obj : defaultValue;
}

const data = { a: [{ b: { c: 3 } }] };
get(data, 'a[0].b.c');            // 3
get(data, 'a.0.b.c');            // 3
get(data, 'a[1].b.c', 'none');   // 'none'
```

**References.** [MDN · Optional chaining (?.)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) · [MDN · Property accessors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors)

---

### 28. once() — invoke a function at most once  `Medium`

**Pattern:** Higher-Order Functions

**Problem.** Write once(fn) that returns a wrapper calling fn only the first time; subsequent calls return the first result without re-invoking.

**What it tests.** Closure over a 'called' flag + cached value, and preserving this/args on that first call.

**Approach & answer.** once is a higher-order function that enforces single execution — the classic use is idempotent initialisation (set up a connection, attach a one-time handler, run an expensive bootstrap exactly once no matter how many callers fire it). The implementation is a closure capturing two private variables: a boolean flag (has it run?) and the cached return value. The returned wrapper checks the flag; on the FIRST call it invokes fn — using `fn.apply(this, args)` so the wrapper transparently forwards both the receiver and all arguments — stores the result, flips the flag, and returns it; on every later call it skips fn entirely and returns the cached value. Preserving `this` matters so `obj.method = once(fn)` still binds correctly. This differs from memoize (js-10): memoize caches PER distinct-arguments key and may call fn many times for different inputs; once ignores arguments after the first call and never invokes fn again regardless of input. It's the runtime analogue of a lazy singleton. A common refinement is to release the reference to fn after the first call (set it to null) so any large closure it captured can be garbage-collected.

**Use this technique when.** One-time setup/initialisation, guarding event handlers that must fire once, and building lazy singletons.

**Complexity.** O(1) per call after the first.

```js
function once(fn) {
  let called = false;
  let value;
  return function (...args) {
    if (!called) {
      called = true;
      value = fn.apply(this, args); // forward this + args on the real call
      fn = null;                    // let the original be GC'd
    }
    return value;                   // cached thereafter
  };
}

let count = 0;
const init = once(() => ++count);
init(); // 1  (runs)
init(); // 1  (cached, does NOT increment)
init(); // 1
```

**References.** [MDN · Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures) · [MDN · Function.prototype.apply()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)

---

### 29. Cancellable interval / self-correcting timer  `Medium`

**Pattern:** Timers & Cancellation

**Problem.** Build a cancellable repeating timer. Why can setInterval drift, and how do you build a self-correcting one with setTimeout?

**What it tests.** Timer lifecycle (returning a cancel handle), and understanding interval drift vs a recursive setTimeout that corrects for it.

**Approach & answer.** Two things are being probed. First, ergonomics: wrap the timer so it returns a CANCEL function (a closure over the handle) rather than leaking a raw id — this is the clean pattern for effects that must be torn down (React cleanup, aborting polling). Second, accuracy: setInterval schedules callbacks every N ms measured from each fire, but the callback's own execution time and event-loop congestion cause DRIFT — if a tick's work takes 20ms, subsequent ticks accumulate lateness, and if the tab is throttled, setInterval can even queue up back-to-back catch-up calls. The fix is a self-correcting timer built from recursive setTimeout: record the intended next fire time, and after each tick compute the delay to the NEXT scheduled instant (`expected += period; delay = Math.max(0, expected - Date.now())`) so errors don't compound — the schedule stays anchored to absolute time rather than relative gaps. Recursive setTimeout also guarantees the previous callback FINISHED before the next is scheduled (no overlap/pile-up), unlike setInterval. Return a cancel that calls clearTimeout on the pending handle and sets a stopped flag so an in-flight tick won't reschedule. Always clear timers on teardown to avoid callbacks firing against unmounted state (a common React memory-leak/'setState on unmounted' bug).

**Use this technique when.** Polling with cleanup, animation/clock loops needing accuracy, and any repeating effect that must be reliably cancelled.

**Complexity.** O(1) per tick; no drift accumulation.

```js
function setCancellableInterval(fn, period, ...args) {
  let stopped = false;
  let handle;
  let expected = Date.now() + period;
  const tick = () => {
    if (stopped) return;
    fn(...args);
    expected += period;                     // anchor to absolute time
    const drift = Date.now() - expected;
    handle = setTimeout(tick, Math.max(0, period - drift)); // self-correct
  };
  handle = setTimeout(tick, period);
  return function cancel() {                // closure over the handle
    stopped = true;
    clearTimeout(handle);
  };
}

const cancel = setCancellableInterval(() => console.log('tick'), 1000);
// later, to stop and avoid leaks:
// cancel();
```

**References.** [MDN · setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) · [MDN · setInterval()](https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval)

---

### 30. Promise-based sleep and a timeout wrapper  `Medium`

**Pattern:** Promises / Timers

**Problem.** Implement sleep(ms) as a promise, then a withTimeout(promise, ms) that rejects if the work doesn't settle in time.

**What it tests.** Promisifying setTimeout, composing with Promise.race for deadlines, and cleaning up the timer to avoid leaks.

**Approach & answer.** sleep is the canonical promisify example: wrap setTimeout so `await sleep(ms)` pauses an async function without blocking the thread — `return new Promise(resolve => setTimeout(resolve, ms))`. It reads linearly inside async code and is the building block for delays between retries, staggering requests, or animations. The timeout wrapper is a Promise.race composition: race the real work against a promise that REJECTS after ms — whichever settles first wins, so if the work is slow the timeout rejection propagates and callers can surface 'request timed out'. Two important refinements interviewers look for: (1) capture the timer id and clearTimeout it once the race settles, so a resolved-fast promise doesn't leave a dangling timer (and, in Node, doesn't keep the process alive) — do this in a .finally or by clearing in both race branches. (2) Note the losing promise is NOT cancelled — JS promises have no built-in cancellation, so the slow work keeps running in the background; for real cancellation (e.g. fetch) you pair this with an AbortController and abort in the timeout branch. This race-against-a-timer is exactly how you add deadlines to fetches, and how you avoid an async operation hanging forever.

**Use this technique when.** Adding deadlines to fetches/RPCs, delays between retries or polls, and staggering async work — pairing with AbortController for true cancellation.

**Complexity.** O(1); one extra timer per wrapped call.

```js
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function withTimeout(promise, ms) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error('Timed out after ' + ms + 'ms')), ms);
  });
  try {
    return await Promise.race([promise, timeout]); // first to settle wins
  } finally {
    clearTimeout(timer);                            // never leak the timer
  }
}

// Usage
async function demo() {
  await sleep(200);                       // non-blocking pause
  // reject if fetchData takes > 5s (fetchData still runs in background)
  const data = await withTimeout(fetchData(), 5000);
  return data;
}
```

**References.** [MDN · Promise.race()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) · [MDN · setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout)

---

### 31. Singleton — one instance, shared everywhere  `Medium`

**Pattern:** Creational Patterns

**Problem.** Implement the Singleton pattern in JavaScript. When is it justified, and why do many consider it an anti-pattern?

**What it tests.** Recognising that JS gives you singletons almost for free (module caching, object literals) and weighing the shared-global-state downsides.

**Approach & answer.** Singleton guarantees a class has exactly ONE instance and gives a global access point to it. The signal: a resource that must be shared and coordinated app-wide — a config store, a logger, a connection pool, a single cache. In JavaScript you rarely need the textbook class version because the language already hands you singletons: a plain object literal IS a singleton, and — crucially — ES modules are evaluated ONCE and cached, so `export const store = createStore()` yields the same instance to every importer for free. The classic implementation uses a closure with lazy initialisation: a `getInstance()` that creates the instance on first call and returns the cached one thereafter, so you pay construction cost only if it's used. The interviewer usually wants the CAVEAT too: Singleton is often an anti-pattern because it's global mutable state wearing a design-pattern hat — it introduces hidden coupling (callers depend on it without it appearing in their signatures), makes unit testing hard (you can't easily swap a mock, and state leaks between tests unless you add a reset), and can mask ordering/lifecycle bugs. The modern frontend alternative is dependency injection / passing the instance explicitly (or a React context/provider) so the dependency is visible and mockable. Reach for Singleton when there genuinely must be one authority over a resource; avoid it when it's just a convenient global.

**Use this technique when.** A single shared authority is required — app config, logger, feature-flag client, connection pool, or one cache; prefer DI/context when you need testability.

**Complexity.** O(1) construction (once) and O(1) access thereafter.

```js
// Closure-based lazy singleton
const Config = (function () {
  let instance = null;              // private, shared
  function create() {
    return { apiBase: '/api', loadedAt: 'once', flags: {} };
  }
  return {
    getInstance() {
      if (!instance) instance = create();  // lazy init on first use
      return instance;
    },
  };
})();

Config.getInstance() === Config.getInstance(); // true

// Idiomatic ES-module singleton: modules evaluate once and cache,
// so every importer receives the SAME object.
//   export const config = { apiBase: '/api', flags: {} };
```

**References.** [Refactoring.Guru · Singleton](https://refactoring.guru/design-patterns/singleton) · [Patterns.dev · Singleton Pattern](https://www.patterns.dev/vanilla/singleton-pattern)

---

### 32. Factory & Abstract Factory  `Medium`

**Pattern:** Creational Patterns

**Problem.** Explain the Factory and Abstract Factory patterns. How do they decouple creation from use, and when do you reach for each?

**What it tests.** Spotting when object creation logic should be centralised behind a function so callers depend on an interface, not concrete constructors.

**Approach & answer.** A Factory is a function/method whose job is to CREATE and return objects, hiding the decision of which concrete type to instantiate. The signal to reach for it: callers keep branching on a type to `new` different classes (`if type==='email' new EmailNotifier else new SmsNotifier`), or construction is complex enough that scattering `new` everywhere is fragile. Centralising that in `createNotifier(type)` means callers depend only on the returned interface (`.send()`), so adding a new type touches one place and the rest of the code is untouched — that's the Open/Closed benefit. In JS a factory is often just a function returning an object literal (no class needed), which also sidesteps `new`/`this` pitfalls. Abstract Factory goes one level up: it's a factory that produces FAMILIES of related objects that must be used together, chosen by a single switch. Example: a UI theme factory returns a whole matching set — `{ createButton, createInput, createModal }` — for 'dark' vs 'light', guaranteeing you never mix a dark button with a light modal. So: Factory = 'give me the right ONE object for this input'; Abstract Factory = 'give me a coherent SET of objects for this variant'. Both trade a little indirection for decoupling and a single point of change; skip them when there's only one concrete type and no real variation — that's premature abstraction.

**Use this technique when.** Creation branches on a type or config, construction is non-trivial, or you must produce a matching family of objects for a chosen variant (theme, platform, environment).

**Complexity.** O(1) per object created.

```js
// Factory: pick the right implementation behind one interface
function createNotifier(type) {
  const map = {
    email: (to, msg) => ({ send: () => 'email->' + to + ': ' + msg }),
    sms:   (to, msg) => ({ send: () => 'sms->' + to + ': ' + msg }),
  };
  const make = map[type];
  if (!make) throw new Error('Unknown notifier: ' + type);
  return make;
}
createNotifier('sms')('+1', 'hi').send();

// Abstract Factory: a coherent family per variant
const themeFactory = {
  dark:  { button: () => 'dark-btn',  input: () => 'dark-input'  },
  light: { button: () => 'light-btn', input: () => 'light-input' },
};
const ui = themeFactory['dark'];  // matching set, no mixing
ui.button(); ui.input();
```

**References.** [Refactoring.Guru · Factory Method](https://refactoring.guru/design-patterns/factory-method) · [Refactoring.Guru · Abstract Factory](https://refactoring.guru/design-patterns/abstract-factory)

---

### 33. Builder — construct complex objects step by step  `Medium`

**Pattern:** Creational Patterns

**Problem.** Implement the Builder pattern with a fluent API. What problem does it solve that a constructor doesn't?

**What it tests.** Recognising the telescoping-constructor / many-optional-params smell and solving it with incremental, readable, chainable construction.

**Approach & answer.** Builder separates the construction of a complex object from its representation, letting you assemble it step by step. The signal: a constructor with many parameters, most of them optional — the 'telescoping constructor' smell where call sites read `new Request(url, null, null, 'POST', headers, null, true)` and nobody can tell what the nulls mean. A Builder replaces that with named, chainable steps: `new RequestBuilder(url).method('POST').header('Auth', t).json(body).build()`. Each setter mutates internal state and RETURNS `this`, which is what enables the fluent chaining; a final `build()` validates and returns the finished (ideally frozen/immutable) object. Benefits: call sites are self-documenting, order-independent, and you only specify what you need; you can enforce invariants in `build()` (required fields present, mutually-exclusive options rejected); and you can produce an immutable result while keeping construction ergonomic. This is everywhere in frontend tooling — query builders (Knex), request builders, test-data builders, and fluent config APIs. Contrast with Factory: a Factory decides WHICH object to make in one call; a Builder assembles ONE known object gradually across many calls. Don't reach for it when a plain object literal or a single options object (`fn({ method, headers })`) is already clear — Builder earns its keep only when construction is genuinely multi-step or needs staged validation.

**Use this technique when.** Objects with many optional parameters, staged/validated construction, or a readable fluent API — query builders, request/config builders, test-data factories.

**Complexity.** O(k) for k build steps.

```js
class RequestBuilder {
  constructor(url) { this.req = { url, method: 'GET', headers: {} }; }
  method(m) { this.req.method = m; return this; }        // return this => chainable
  header(k, v) { this.req.headers[k] = v; return this; }
  json(body) { this.req.body = JSON.stringify(body);
               this.req.headers['Content-Type'] = 'application/json';
               return this; }
  build() {
    if (!this.req.url) throw new Error('url required'); // validate invariants
    return Object.freeze({ ...this.req });              // immutable result
  }
}

const req = new RequestBuilder('/users')
  .method('POST')
  .header('Authorization', 'Bearer x')
  .json({ name: 'Ada' })
  .build();
```

**References.** [Refactoring.Guru · Builder](https://refactoring.guru/design-patterns/builder) · [Wikipedia · Builder pattern](https://en.wikipedia.org/wiki/Builder_pattern)

---

### 34. Module & Revealing Module pattern  `Medium`

**Pattern:** Structural Patterns

**Problem.** Explain the Module and Revealing Module patterns. How do closures create private state, and how do ES modules relate?

**What it tests.** Understanding encapsulation via closures — a public API over hidden private state — and why ES modules are the modern successor.

**Approach & answer.** The Module pattern uses a closure to create PRIVATE state and expose only a curated PUBLIC API — JavaScript's original answer to encapsulation before the language had real modules or private class fields. The mechanism: an IIFE (immediately-invoked function expression) runs once, declares variables and functions in its local scope (invisible from outside), and RETURNS an object containing just the functions meant to be public. Those returned functions close over the private variables, so they can read/mutate them while the outside world cannot touch them directly — the closure IS the privacy boundary. The Revealing Module variant is a stylistic refinement: define everything (private and public) as locals inside the IIFE, then return an object that simply MAPS public names to those inner functions — so the return statement reads as a clean manifest of the public interface, and internal calls reference the real functions rather than `this`. Why it mattered: it prevented global-namespace pollution and simulated private members. Today ES modules (js-20) are the successor and should be your default — top-level `const`/`let` in a module file are module-scoped (private) unless `export`ed, giving the same encapsulation with static analysis, tree-shaking, and no IIFE boilerplate; and class `#private` fields (js-27) give per-instance privacy. Knowing the Module pattern still matters because you'll meet it in legacy code and it explains WHY closures are the foundation of encapsulation in JS.

**Use this technique when.** Encapsulating private state behind a small public API in non-module scripts, legacy codebases, or singletons; superseded by ES modules and #private fields in new code.

**Complexity.** O(1) — structural, no algorithmic cost.

```js
const Counter = (function () {
  let count = 0;                    // PRIVATE — closed over, unreachable outside
  function change(by) { count += by; return count; }
  // Revealing module: return maps public names to inner functions
  return {
    increment: () => change(1),
    decrement: () => change(-1),
    value: () => count,             // read-only view of private state
  };
})();

Counter.increment(); // 1
Counter.increment(); // 2
Counter.value();     // 2
Counter.count;       // undefined — no direct access

// Modern equivalent (ES module): private by default, export the API
//   let count = 0;
//   export const increment = () => (count += 1);
```

**References.** [Patterns.dev · Module Pattern](https://www.patterns.dev/vanilla/module-pattern) · [MDN · Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures)

---

### 35. Decorator — extend behavior by wrapping  `Medium`

**Pattern:** Structural Patterns

**Problem.** Implement the Decorator pattern. How does wrapping add behavior without modifying the original, and how does it relate to HOCs?

**What it tests.** Choosing composition (wrap-and-delegate) over subclassing to layer behavior, and recognising the pattern in function wrappers, HOCs, and TS decorators.

**Approach & answer.** Decorator attaches new responsibilities to an object or function by WRAPPING it in another object/function with the same interface, delegating to the original and adding behavior around it. The signal: you want to add a cross-cutting concern — logging, caching, timing, retry, authorization — to something without editing its source and without a subclass explosion (if every combination of features needs its own subclass, you get 2^n classes; decorators let you STACK features at runtime instead). Because each decorator preserves the wrapped thing's interface, decorators compose — you can wrap a wrap a wrap, and the order is meaningful. In JavaScript the most common form is the function decorator: a higher-order function that takes a function and returns a new function calling through to it with extra behavior (this is exactly how `once` (js-32), `memoize` (js-10), `debounce` (js-4), and `throttle` (js-5) work — they're all decorators). It shows up structurally across the ecosystem: React Higher-Order Components (`withAuth(Component)`) wrap a component to inject props/behavior; Express middleware wraps request handling; TypeScript/Angular `@decorators` are the syntactic form applied to classes and members. Contrast with inheritance: subclassing fixes behavior at author time and is static; decoration composes behavior at runtime and stays flexible. The trade-off is more small wrappers and indirection in stack traces — worth it for orthogonal concerns you want to mix and match.

**Use this technique when.** Layering cross-cutting behavior (logging, caching, retry, auth) onto functions or components without editing them — function wrappers, React HOCs, middleware, TS decorators.

**Complexity.** O(1) overhead per wrap layer.

```js
// Function decorator: add behavior around any function, same interface
function withLogging(fn, label) {
  return function (...args) {
    console.log(label, 'called with', args);
    const result = fn.apply(this, args);   // delegate to the original
    console.log(label, 'returned', result);
    return result;
  };
}
function withRetry(fn, times) {
  return function (...args) {
    let lastErr;
    for (let i = 0; i < times; i++) {
      try { return fn.apply(this, args); } catch (e) { lastErr = e; }
    }
    throw lastErr;
  };
}

// Decorators compose / stack — order matters
const add = (a, b) => a + b;
const decorated = withLogging(withRetry(add, 3), 'add');
decorated(2, 3); // logs, retries on throw, returns 5
```

**References.** [Refactoring.Guru · Decorator](https://refactoring.guru/design-patterns/decorator) · [Wikipedia · Decorator pattern](https://en.wikipedia.org/wiki/Decorator_pattern)

---

### 36. Facade — a simple interface over a complex subsystem  `Medium`

**Pattern:** Structural Patterns

**Problem.** Explain the Facade pattern. How does it tame subsystem complexity, and where does it appear in frontend code?

**What it tests.** Recognising when to hide a tangle of low-level calls behind one clean, intention-revealing interface.

**Approach & answer.** Facade provides a single, simplified interface to a complex subsystem — the client calls one clear method and the facade orchestrates the messy details behind it. The signal: callers repeatedly perform the SAME multi-step dance against low-level APIs — build headers, attach a token, call fetch, check status, parse JSON, map errors — and that sequence is duplicated and easy to get wrong. A facade like `api.getUser(id)` collapses all of it into one intention-revealing call, so callers depend on WHAT they want, not HOW it's assembled. Benefits: it decouples client code from subsystem internals (you can swap fetch for axios, add retry/caching, or change auth in ONE place without touching callers), reduces cognitive load, and gives you a natural seam to test/mock. Frontend is full of facades: an API-client module wrapping fetch+auth+error-handling; a storage service hiding localStorage/IndexedDB/quota logic behind `save/load`; jQuery historically was a giant facade over inconsistent DOM/XHR APIs; a custom React hook like `useUser()` is a facade over fetching, caching, and state. Facade differs from Adapter (which converts one interface to another expected shape) and from Decorator (which adds behavior while keeping the same interface): Facade INTRODUCES a new, smaller interface over many pieces. It doesn't hide the subsystem — advanced callers can still reach underneath — it just offers the easy path for the common case. The only caution is letting a facade grow into a god-object; keep each focused on one subsystem.

**Use this technique when.** Wrapping a repeated multi-step subsystem interaction behind one clean method — API clients over fetch, storage/service layers, SDK wrappers, custom hooks.

**Complexity.** O(1) structural; cost is that of the delegated calls.

```js
// Complex subsystem: fetch + auth + status check + parse + error mapping,
// all hidden behind a small, intention-revealing facade.
class ApiClient {
  constructor(baseUrl, getToken) { this.baseUrl = baseUrl; this.getToken = getToken; }
  async request(path, options = {}) {
    const res = await fetch(this.baseUrl + path, {
      ...options,
      headers: { 'Authorization': 'Bearer ' + this.getToken(), ...(options.headers || {}) },
    });
    if (!res.ok) throw new Error('HTTP ' + res.status);
    return res.status === 204 ? null : res.json();
  }
  // Facade methods: callers say WHAT, not HOW
  getUser(id) { return this.request('/users/' + id); }
  createUser(data) { return this.request('/users', { method: 'POST', body: JSON.stringify(data) }); }
}

const api = new ApiClient('/api', () => 'token');
// await api.getUser(42);  // one clean call hides the whole dance
```

**References.** [Refactoring.Guru · Facade](https://refactoring.guru/design-patterns/facade) · [Wikipedia · Facade pattern](https://en.wikipedia.org/wiki/Facade_pattern)

---

### 37. Strategy — interchangeable algorithms  `Medium`

**Pattern:** Behavioral Patterns

**Problem.** Implement the Strategy pattern. How does it replace conditional logic with pluggable, swappable behavior?

**What it tests.** Spotting the growing if/switch-on-type smell and refactoring branches into a map of interchangeable strategy functions selected at runtime.

**Approach & answer.** Strategy defines a family of interchangeable algorithms, encapsulates each one, and makes them swappable at runtime behind a common interface — so the choice of algorithm is data, not a hard-coded branch. The signal: a function that keeps growing a `switch`/`if-else` on a 'type' or 'mode', where each branch is a self-contained algorithm — shipping-cost by carrier, validation by field type, pricing by customer tier, sort by strategy, export by format. Each new case means editing that one big function (violating Open/Closed) and the branches tend to share nothing. Strategy refactors each branch into its own function (the strategy), stores them in a map keyed by the selector, and the context just looks up and delegates: `strategies[key](input)`. Now adding a behavior means adding a map entry — no existing code changes — and each strategy is independently testable. In JavaScript strategies are usually just functions (no need for classes/interfaces), which makes this pattern especially lightweight: a plain object of functions IS the strategy set. It's the backbone of countless frontend features — form validators, comparator functions passed to `sort`, formatting/serialization by type, and configurable behaviors injected as props/callbacks. Strategy vs State (js-43): both swap behavior via composition, but Strategy is chosen by the CLIENT for a one-shot operation and strategies don't know about each other, whereas State transitions are driven internally by the object as it moves between states. Reach for Strategy the moment a conditional is really 'which algorithm', and skip it for a single stable branch.

**Use this technique when.** A conditional selects among self-contained algorithms — validators, formatters, comparators, pricing/shipping rules, export formats — especially when new variants are added often.

**Complexity.** O(1) lookup + the chosen strategy's own cost.

```js
// Instead of switch(carrier) { case ... } scattered around:
const shippingStrategies = {
  standard: (w) => w * 1.0 + 2,
  express:  (w) => w * 2.5 + 5,
  overnight:(w) => w * 4.0 + 12,
};

function shippingCost(strategyKey, weight) {
  const strategy = shippingStrategies[strategyKey];
  if (!strategy) throw new Error('Unknown shipping: ' + strategyKey);
  return strategy(weight);      // delegate to the chosen algorithm
}

shippingCost('express', 3);     // 12.5
// Add 'freight' => one new map entry, zero edits to shippingCost.
```

**References.** [Refactoring.Guru · Strategy](https://refactoring.guru/design-patterns/strategy) · [Wikipedia · Strategy pattern](https://en.wikipedia.org/wiki/Strategy_pattern)

---

### 38. Command — actions as objects (undo/redo)  `Medium`

**Pattern:** Behavioral Patterns

**Problem.** Implement the Command pattern with undo. How does encapsulating a request as an object enable queues, logging, and undo/redo?

**What it tests.** Recognising when to reify an action into an object with execute()/undo() to decouple invoker from receiver and gain history/queueing.

**Approach & answer.** Command turns a request into a standalone OBJECT that bundles everything needed to perform it — the action, its target, and its parameters — behind a uniform interface (typically `execute()` and often `undo()`). The signal: you need to do more with an action than just run it immediately — queue it, log it, schedule/retry it, run it remotely, or (most tellingly) UNDO it. Because each command captures its own inputs and knows how to reverse itself, you can push executed commands onto a history stack and pop-and-`undo()` to walk backwards; a redo stack mirrors it. This also decouples the INVOKER (a button, a keyboard shortcut, a queue) from the RECEIVER (the object that actually does the work) — the invoker only knows it has something with `execute()`, so the same command can be triggered from a menu, a hotkey, or a script, and new commands don't require changing the invoker. Frontend manifestations are everywhere: Redux/Flux ACTIONS are commands (plain objects describing 'what happened' that a reducer executes; middleware can log/queue/replay them — time-travel debugging is literally re-executing a command log); editor undo/redo stacks; a command bus/CQRS on the backend; and macro-recording (a macro is just a list of commands). The trade-off is boilerplate — each action becomes an object — so reserve it for when you genuinely need undo, queuing, logging, or invoker/receiver decoupling; a direct method call is fine otherwise.

**Use this technique when.** Undo/redo stacks, action logging/replay (Redux, time-travel), queuing or scheduling operations, macro recording, and decoupling UI triggers from the code that runs.

**Complexity.** O(1) per execute/undo; O(n) history memory for n actions.

```js
// Each command knows how to do AND undo itself
class AddText {
  constructor(doc, text) { this.doc = doc; this.text = text; }
  execute() { this.doc.content += this.text; }
  undo() { this.doc.content = this.doc.content.slice(0, -this.text.length); }
}

class History {
  constructor() { this.done = []; this.undone = []; }
  run(cmd) { cmd.execute(); this.done.push(cmd); this.undone = []; }
  undo() { const c = this.done.pop(); if (c) { c.undo(); this.undone.push(c); } }
  redo() { const c = this.undone.pop(); if (c) { c.execute(); this.done.push(c); } }
}

const doc = { content: '' };
const h = new History();
h.run(new AddText(doc, 'Hello'));   // "Hello"
h.run(new AddText(doc, ' World'));  // "Hello World"
h.undo();                            // "Hello"
h.redo();                            // "Hello World"
```

**References.** [Refactoring.Guru · Command](https://refactoring.guru/design-patterns/command) · [Wikipedia · Command pattern](https://en.wikipedia.org/wiki/Command_pattern)

---

### 39. State — behavior driven by a state machine  `Medium`

**Pattern:** Behavioral Patterns

**Problem.** Implement the State pattern / a finite state machine. How does it replace scattered boolean flags and guard the transitions a UI can make?

**What it tests.** Recognising 'boolean soup' (isLoading/isError/isSuccess) and modeling it as explicit states with legal transitions.

**Approach & answer.** The State pattern lets an object change its behavior when its internal state changes — it looks like the object changed class. Concretely, you model the system as a finite state machine (FSM): a set of named STATES, and a table of legal TRANSITIONS mapping (currentState, event) to the next state. The signal is 'boolean soup' — a component juggling `isLoading`, `isError`, `isSuccess`, `isEmpty` where impossible combinations (loading AND error) are representable and creep in as bugs, and behavior is decided by tangled `if` chains scattered across handlers. Modeling it as ONE state variable that can only be `idle | loading | success | error` makes illegal states UNREPRESENTABLE, centralises 'what can happen next' in the transition table (an event that isn't legal for the current state is simply ignored or throws), and makes the logic self-documenting and testable. Each state can also carry state-specific behavior/data. This is exactly what a traffic light, a checkout flow, a media player (playing/paused/stopped), a form wizard, and async data-fetching are — and it's why XState and the useReducer 'state machine' pattern are popular in React: a reducer keyed on the current state plus an action IS an FSM. Contrast with Strategy (js-41): both delegate behavior, but Strategy is picked by the client for a single call and the strategies are independent, whereas State transitions are driven by the machine itself as events arrive, and states are aware of which states follow. The payoff is fewer impossible-state bugs and a clear map of allowed flows; the cost is upfront modeling, so use it when a thing has genuinely distinct modes with rules about moving between them.

**Use this technique when.** Anything with distinct modes and rules for moving between them — async fetch status, wizards/checkout, media players, connection lifecycles; pairs with useReducer / XState.

**Complexity.** O(1) per transition (table lookup).

```js
// Finite state machine: legal transitions live in one table
const machine = {
  idle:    { FETCH: 'loading' },
  loading: { SUCCESS: 'success', ERROR: 'error' },
  success: { FETCH: 'loading' },
  error:   { RETRY: 'loading' },
};

function createFetchState() {
  let state = 'idle';
  return {
    get state() { return state; },
    send(event) {
      const next = machine[state][event];
      if (!next) return state;          // illegal transition ignored
      state = next;                     // impossible states unrepresentable
      return state;
    },
  };
}

const fsm = createFetchState();
fsm.send('FETCH');   // 'loading'
fsm.send('SUCCESS'); // 'success'
fsm.send('ERROR');   // 'success' (ignored — not legal from success)
```

**References.** [Refactoring.Guru · State](https://refactoring.guru/design-patterns/state) · [Wikipedia · Finite-state machine](https://en.wikipedia.org/wiki/Finite-state_machine)

---

### 40. Implement Promise.all  `Hard`

**Pattern:** Implement from scratch

**Problem.** Implement Promise.all(promises): resolve with an array of results in order, or reject on the first rejection.

**What it tests.** Async coordination, preserving order, the count-down-to-done pattern, and fail-fast semantics.

**Approach & answer.** Return a new Promise. Track a results array and a completed counter. For each input, resolve it (Promise.resolve wraps non-promises), write its result at its ORIGINAL index (not push — they finish out of order, order must be preserved), and when the counter hits the length, resolve. Any rejection rejects the outer promise immediately (fail-fast). Handle the empty-array case by resolving with []. Two correctness details interviewers look for: use a completed counter, NOT results.length, to detect done — an out-of-order early result at index 5 would make length wrong and a value of undefined at index 2 would be missed; and remember a promise settles only once, so a later rejection after the outer promise already resolved is harmless. Know the family: allSettled waits for every promise and never rejects (returns {status,value|reason}[]); race settles as soon as the first promise settles (fulfilled OR rejected); any resolves on the first fulfillment and rejects only if all reject (AggregateError).

**Use this technique when.** Firing independent requests in parallel and waiting for all. Contrast allSettled (never rejects), race (first settle), any (first fulfill).

```js
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;
    if (promises.length === 0) return resolve(results);
    promises.forEach((p, i) => {
      Promise.resolve(p).then((val) => {
        results[i] = val;               // preserve order by index
        if (++completed === promises.length) resolve(results);
      }, reject);                       // fail fast on first rejection
    });
  });
}
```

**References.** [MDN · Promise.all()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) · [MDN · Promise.allSettled()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled)

---

### 41. Implement memoize  `Hard`

**Pattern:** Implement from scratch

**Problem.** Write a generic memoize(fn) that caches results by arguments.

**What it tests.** Closures + cache-key design + tradeoffs (unbounded cache, key collisions).

**Approach & answer.** Keep a Map keyed by a serialization of args (JSON.stringify for simple args; for a single object arg, a WeakMap is better and lets entries be garbage-collected). Return cached value if present, else compute, store, return. Call out the tradeoffs: a JSON key breaks on functions/circular args and is order-sensitive; an unbounded cache is a memory leak — add an LRU cap for hot paths. Correctness preconditions: memoization is only safe for PURE functions (same args → same result, no side effects) — memoizing a function that reads mutable external state or the clock returns stale answers. Use Map, not a plain object, so keys can't collide with prototype names like 'constructor' and so insertion order is preserved (needed for LRU eviction). For a bounded cache, on a hit re-insert the key to mark it most-recently-used, and when size exceeds the cap delete the first (oldest) key from the Map. React.useMemo is the same idea scoped to a component render, keyed by a dependency array instead of arguments.

**Use this technique when.** Pure, expensive, repeatedly-called functions (parsing, derived computations). This is what React.useMemo does conceptually.

```js
function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}
```

**References.** [MDN · Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) · [Wikipedia · Memoization](https://en.wikipedia.org/wiki/Memoization)

---

### 42. Proxy and Reflect  `Hard`

**Pattern:** Metaprogramming

**Problem.** What is a Proxy? What are traps, and how does Reflect complement it? Give a real use case.

**What it tests.** Metaprogramming: intercepting fundamental object operations and forwarding default behavior correctly.

**Approach & answer.** A Proxy wraps a target object and lets you intercept fundamental operations via handler functions called traps — get, set, has, deleteProperty, apply, construct, and more. Reading, writing, or calling through the proxy runs your trap instead of (or before) the default behavior. Reflect is the companion: it exposes those same operations as plain functions (Reflect.get, Reflect.set, ...) whose signatures mirror the traps exactly, so inside a trap you call the matching Reflect method to perform the default operation and return its result — this is cleaner and more correct than target[key], especially for preserving the right receiver so inherited getters/setters bind to the proxy. Real use cases: reactive state (Vue 3's reactivity is Proxy-based — a set trap notifies subscribers), validation/schema enforcement on assignment, negative array indexing, default values for missing keys, logging/tracing, and access control. Caveats: proxies add per-operation overhead, cannot be fully transparent (you can detect them), and some invariants can't be violated. Reach for a Proxy only when you genuinely need to intercept operations generically; a getter/setter or a plain wrapper is simpler when the surface is small.

**Use this technique when.** Reactive stores, validation-on-write, default/computed properties, API mocking, and cross-cutting logging without touching call sites.

**Complexity.** Adds a trap-call overhead to each intercepted operation.

```js
const withDefaults = (obj, fallback) => new Proxy(obj, {
  get(target, key, receiver) {
    return key in target ? Reflect.get(target, key, receiver) : fallback;
  },
  set(target, key, value, receiver) {
    if (typeof value !== 'number') throw new TypeError(key + ' must be a number');
    return Reflect.set(target, key, value, receiver); // default set, correct receiver
  },
});
const scores = withDefaults({}, 0);
scores.missing; // 0
scores.points = 5; // ok
```

**References.** [MDN · Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) · [MDN · Reflect](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect)

---

### 43. Async iterators & for-await-of  `Hard`

**Pattern:** Async Iteration

**Problem.** How do async iterators and for-await-of work? Implement an async generator that paginates an API.

**What it tests.** Understanding Symbol.asyncIterator, async generators, and sequential consumption of streamed/paginated data.

**Approach & answer.** A sync iterator implements `[Symbol.iterator]()` returning an object with `next()` -> `{value, done}`. An ASYNC iterator implements `[Symbol.asyncIterator]()` whose `next()` returns a PROMISE of `{value, done}` — so each step can await I/O. `for await (const x of source)` consumes it: on each turn it awaits source.next(), unwraps the promise, and runs the body, pausing between items. It works over async iterables, sync iterables of promises, and — importantly — async generators. An async generator (`async function*`) is the ergonomic way to build one: `yield` produces a value, and you can `await` between yields, so it models a pull-based stream where the consumer sets the pace (natural backpressure — the next page isn't fetched until the consumer asks). The canonical use is pagination: yield items page by page, fetching the next page lazily only when the consumer has drained the current one. This keeps memory flat regardless of total size and lets the consumer `break` early to stop fetching. `for await` also propagates rejections (wrap in try/catch) and respects `return()`/`break` to clean up. Node streams and fetch response bodies are async iterables, so you can `for await (const chunk of stream)`.

**Use this technique when.** Streaming large/paginated datasets, consuming Node streams or ReadableStreams, and any pull-based flow needing per-item await with backpressure.

**Complexity.** Memory O(page) instead of O(total); consumer-paced.

```js
// Async generator that lazily paginates an API
async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const page = await res.json();
    for (const item of page.items) yield item; // stream items
    next = page.nextUrl;                        // fetched only when needed
  }
}

// Consume sequentially; break stops further fetching
for await (const item of paginate('/api/things?page=1')) {
  console.log(item.id);
  if (item.id === 'stop') break;   // no more pages fetched
}

// Manual shape of an async iterator
const range = {
  [Symbol.asyncIterator]() {
    let i = 0;
    return { next: async () => ({ value: i, done: i++ >= 3 }) };
  },
};
```

**References.** [MDN · for await...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) · [MDN · AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator)

---

## TypeScript

> TS rounds test whether you can model intent in the type system so bugs fail at compile time. Know when to reach for generics, utility types, discriminated unions, and narrowing — and when NOT to over-type.

### 1. type vs interface — when to use which?  `Easy`

**Pattern:** Types vs Interfaces

**Problem.** What's the difference between `type` and `interface`? When do you reach for each?

**What it tests.** Whether you have a principled default, not just 'they're basically the same'.

**Approach & answer.** Both describe object shapes. Interfaces are open (declaration merging — multiple declarations combine) and are the idiom for public object/class contracts. Type aliases are closed but far more expressive: unions, intersections, tuples, mapped and conditional types, and aliasing primitives/functions. Practical rule: interface for object shapes and things classes implement; type when you need a union, tuple, or any computed type. Consistency inside a codebase matters more than the choice. More precisely: `interface extends` produces slightly better error messages and is cached by the compiler (it can be marginally faster in huge codebases), while `type` uses `&` intersection to combine. Declaration merging is a double-edged feature — great for augmenting third-party module types (declare module), dangerous inside app code because two files can silently reshape the same interface. A `type` alias can't be reopened, which some teams prefer for exactly that reason. Both support generics; only `type` can alias a primitive, a union, or a mapped/conditional type.

**Use this technique when.** interface for a component's props contract a library might extend; type for `type Status = 'idle' | 'loading' | 'error'`.

```ts
interface User { id: string; name: string; }
interface User { email: string; }   // merges -> { id, name, email }

type Status = 'idle' | 'loading' | 'error';  // union: only 'type' can do this
type Point = [number, number];               // tuple
type Handler = (e: Event) => void;            // function alias
```

**References.** [TS Handbook · Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) · [TS Handbook · type vs interface differences](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces)

---

### 2. Why generics? identity + a constrained generic  `Easy`

**Pattern:** Generics

**Problem.** Explain generics with a concrete example. Then constrain a generic so it only accepts objects with an `id`.

**What it tests.** Whether you understand generics preserve type relationships instead of erasing to `any`.

**Approach & answer.** Generics let a function/type work over many types while KEEPING the relationship between input and output. identity<T>(x: T): T returns exactly what it got — pass a string, get a string back (not any). `extends` constrains the type parameter: <T extends { id: string }> means T can be any object as long as it has an id, and you keep full type safety on that field. The mental model: a generic is a type-level function — it takes types as parameters and produces a type. Inference usually fills them in for you (you rarely write identity<string>(x) — TS infers T from the argument), which is why generics feel invisible until you need to constrain or relate them. Common patterns: a constraint (<T extends HasId>), a default (<T = string>), and relating two params (function pick<T, K extends keyof T>(obj: T, key: K): T[K] — the return type is derived from which key you pass). Prefer generics over `any` or overloads whenever the output type depends on the input type; they preserve type information all the way through the call.

**Use this technique when.** Reusable utilities/hooks/containers: a typed useFetch<T>(), a Repository<T>, array helpers that keep element types.

```ts
function identity<T>(value: T): T { return value; }
const s = identity('hi');   // s: string  (not any)

function getId<T extends { id: string }>(item: T): string {
  return item.id;           // safe: T is guaranteed to have id
}
getId({ id: 'u1', name: 'A' }); // ok
// getId({ name: 'A' });        // compile error
```

**References.** [TS Handbook · Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) · [TS Handbook · keyof / indexed access](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html)

---

### 3. unknown vs any vs never  `Easy`

**Pattern:** Top & Bottom Types

**Problem.** Explain the difference between unknown, any, and never, and when to use each.

**What it tests.** Whether you understand the type lattice and use unknown to keep type-safety at boundaries.

**Approach & answer.** any opts out of the type system entirely — every operation is allowed and errors slip through silently; it should be a last resort. unknown is the type-safe top type: any value is assignable TO unknown, but you can do nothing WITH an unknown until you narrow it (typeof/instanceof/a type guard). That makes unknown the correct type for anything crossing a trust boundary — JSON.parse results, external API payloads, catch clause errors — because it forces validation before use. never is the bottom type: it has no values and is assignable to every type but nothing is assignable to it. It surfaces for a function that never returns (throws or infinite-loops), for the impossible branch after an exhaustive switch, and for empty intersections. The exhaustiveness pattern — assigning the switch's default value to a never-typed variable — turns a missed union case into a compile error, which is one of TypeScript's highest-leverage safety idioms.

**Use this technique when.** Typing catch errors and parsed JSON (unknown); enforcing exhaustive switches (never); avoid any except in genuine escape hatches.

```ts
function parse(json: string): unknown {   // safe: caller must narrow
  return JSON.parse(json);
}
const data = parse('{}');
// data.foo;               // error: object is of type 'unknown'
if (typeof data === 'object' && data) { /* now usable */ }

type Shape = { kind: 'circle' } | { kind: 'square' };
function area(s: Shape) {
  switch (s.kind) {
    case 'circle': return 1;
    case 'square': return 2;
    default: const _exhaustive: never = s; return _exhaustive; // compile error if a case is missed
  }
}
```

**References.** [TS Handbook · Everyday Types (any, unknown)](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) · [TS Handbook · Narrowing (never & exhaustiveness)](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)

---

### 4. Structural typing & excess-property checks  `Easy`

**Pattern:** Structural Typing

**Problem.** TypeScript uses structural typing, not nominal. What does that mean — and why does an object literal with an extra field error when a variable with the same shape doesn't?

**What it tests.** Whether you understand duck typing and the object-literal excess-property check that trips people up.

**Approach & answer.** TS decides compatibility by SHAPE, not by name (nominal). Any value with at least the required members is assignable — a `{ name: string; age: number }` satisfies `{ name: string }` because it has everything the target needs. This is 'duck typing': two independently-declared interfaces with identical members are interchangeable. The surprise is excess-property checking: when you assign an OBJECT LITERAL directly to a typed target, TS flags properties the target doesn't declare — a deliberate lint against typos (`colour` for `color`). Assign through a variable first and the check disappears, because the value is now judged by plain shape-compatibility rather than as a fresh literal. Escape hatches when you genuinely want extra fields: assign to a variable first, add an index signature (`[k: string]: unknown`), or use `as`. The deeper point: structural typing is why generics, utility types, and 'make illegal states unrepresentable' all compose — TS reasons about shapes, so derived and combined types stay compatible automatically. It's also exactly why you sometimes WANT nominal typing (branding) to stop two same-shaped-but-semantically-different types from mixing.

**Use this technique when.** Passing partial config objects, understanding why a typo errors on a literal but not a variable, deciding when to brand a type.

```ts
interface Point { x: number; y: number; }
function log(p: Point) {}

const named = { x: 1, y: 2, z: 3 };
log(named);              // OK — structural: has x & y (extra z ignored)

// log({ x: 1, y: 2, z: 3 });  // ERROR: excess property 'z' on a literal

interface Named   { name: string; }
interface Labeled { name: string; }
const n: Named = { name: 'a' };
const l: Labeled = n;    // OK — same shape, different name
```

**References.** [TS Handbook · Type Compatibility](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) · [TS Handbook · Excess Property Checks](https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks)

---

### 5. Type assertions: as, !, and why they're unsafe  `Easy`

**Pattern:** Type Assertions

**Problem.** Explain type assertions (`as T`), the non-null assertion (`!`), and `as unknown as T`. Why are they escape hatches rather than conversions?

**What it tests.** Whether you know assertions are compile-time-only claims TS trusts, not runtime checks or casts.

**Approach & answer.** A type assertion `value as T` tells the compiler 'trust me, this is a T' — it changes only the STATIC type and emits zero runtime code. It is not a cast: nothing is converted or validated, so a wrong assertion silently produces a value whose real shape doesn't match its type, and the bug surfaces later as an inexplicable `undefined`. TS only allows `as` between 'sufficiently overlapping' types; `as unknown as T` launders through the top type to force any conversion, which is a loud signal you're overriding the checker entirely — reserve it for genuinely justified cases (test doubles, gradual migration). The non-null assertion `x!` asserts x isn't null/undefined without a check — handy after logic the compiler can't follow, dangerous because it removes the very guard that would catch the bug (it's erased at runtime, so `x!.foo` on a null x still throws). Prefer real narrowing (`typeof`, a type guard, `if (x)`) over assertions whenever possible: narrowing PROVES the type at runtime, assertions merely assert it. Note `as const` is a different thing — a const assertion that narrows to literal, readonly types, not an override. Rule: every `as` is a small hole in type safety; each one should be defensible in review.

**Use this technique when.** Casting DOM query results (`as HTMLInputElement`), narrowing after runtime logic TS can't follow, test mocks (`as unknown as T`). Avoid when a type guard would prove it.

```ts
const el = document.getElementById('name') as HTMLInputElement;
el.value = 'hi';         // as: "trust me, it's an input" — no runtime check

let maybe: string | null = Math.random() > 0.5 ? 'x' : null;
// maybe.length;         // error: possibly null
// maybe!.length;        // ! erased at runtime -> throws if maybe is null

const mock = {} as unknown as HTMLInputElement; // force: overlap too small for 'as'
```

**References.** [TS Handbook · Type Assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) · [TS Handbook · Narrowing (prefer over assertions)](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)

---

### 6. Tuples, readonly arrays, and named elements  `Easy`

**Pattern:** Tuples & Readonly Arrays

**Problem.** How do you type a fixed-length tuple vs an array? Show named tuple members, a rest element, and `readonly` arrays — and why readonly matters.

**What it tests.** Precise array/tuple typing and using readonly to prevent mutation at the type level.

**Approach & answer.** An array type `T[]` (or `Array<T>`) is homogeneous and any length; a tuple `[string, number]` fixes BOTH the length and the type at each position — index 0 is a string, index 1 a number. Tuples can name elements for readability (`[first: number, second: number]`; the names are documentation only, erased at runtime), mark trailing elements optional (`[number, number?]`), and use a rest element to capture 'the rest' (`[string, ...number[]]`) — which is exactly how variadic function parameter lists are typed. `readonly` makes an array/tuple immutable at the type level: `readonly number[]` (or `ReadonlyArray<number>`) removes push/pop/splice and index assignment from the type, and `readonly [a, b]` freezes a tuple. This is compile-time only — nothing is frozen at runtime (that's `Object.freeze`) — but it's how you promise a function won't mutate an array it's handed, and it's what `as const` produces. A readonly array is deliberately NOT assignable to a mutable one (that would let a callee mutate it), which is the safety the modifier buys. Reach for tuples for fixed heterogeneous data (a `useState`-style [value, setter] pair, coordinates, key/value entries) and readonly for any array you pass around but don't own.

**Use this technique when.** Typing [value, setter] hook returns, coordinate pairs, Object.entries results, and function args you promise not to mutate (readonly).

```ts
let pair: [string, number] = ['age', 30];        // fixed length + positions
let coord: [x: number, y: number] = [1, 2];      // named (labels are docs only)
let rest: [string, ...number[]] = ['scores', 90, 85]; // rest element

const nums: readonly number[] = [1, 2, 3];
// nums.push(4);        // error: 'push' does not exist on readonly number[]
const frozen = [1, 2] as const;  // readonly [1, 2]
```

**References.** [TS Handbook · Tuple Types](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types) · [TS Handbook · The ReadonlyArray Type](https://www.typescriptlang.org/docs/handbook/2/objects.html#the-readonlyarray-type)

---

### 7. Partial, Pick, Omit, Record — and when  `Medium`

**Pattern:** Utility Types

**Problem.** Explain Partial, Required, Pick, Omit, and Record with a use case for each.

**What it tests.** Fluency with the built-ins senior TS devs use daily instead of re-declaring shapes.

**Approach & answer.** Partial<T> makes all fields optional (patch/update payloads). Required<T> is the inverse. Pick<T,K> selects a subset of keys (a narrow view of a big model). Omit<T,K> removes keys (props minus the ones a wrapper injects). Record<K,V> builds a map type (Record<string, User>). These are derived types — change the source model and they update automatically, which is the point: one source of truth. Under the hood they're all just mapped/conditional types you could write yourself: Partial<T> is { [K in keyof T]?: T[K] }, Pick<T,K> is { [P in K]: T[P] }, and Omit<T,K> is Pick<T, Exclude<keyof T, K>>. Knowing that lets you compose them: Partial<Pick<User, 'name' | 'email'>> is an optional-fields update over just two columns. Other high-value ones: Readonly<T>, ReturnType<typeof fn>, Parameters<typeof fn>, Awaited<T> (unwraps a Promise), and NonNullable<T>. Reach for these before hand-writing a second interface that duplicates the first.

**Use this technique when.** updateUser(patch: Partial<User>); type CardProps = Pick<User,'name'|'avatar'>; type ButtonProps = Omit<NativeButtonProps,'ref'>; const byId: Record<string,User>.

```ts
interface User { id: string; name: string; email: string; age: number; }

type UserPatch = Partial<User>;             // all optional
type UserCard  = Pick<User, 'id' | 'name'>; // { id; name }
type SafeUser  = Omit<User, 'email'>;       // drop email
type UserMap   = Record<string, User>;      // { [id]: User }
```

**References.** [TS Handbook · Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html) · [TS Handbook · Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html)

---

### 8. Model state with a discriminated union  `Medium`

**Pattern:** Discriminated Unions

**Problem.** Model an async request's state so impossible states are unrepresentable, and TS narrows correctly.

**What it tests.** The single most valuable TS pattern for UI: making illegal states impossible to construct.

**Approach & answer.** Give each variant a common literal 'tag' field (here status). A switch on the tag narrows the type in each branch — inside case 'success' TS KNOWS data exists; inside case 'error' it knows error exists and data does NOT. This beats a bag of optional fields (isLoading?, data?, error?) where you can accidentally represent 'loading AND error' — a bug the union makes uncompilable. This is 'make illegal states unrepresentable' applied to everyday UI: model the four request phases as { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: E } and the compiler forces every consumer to handle each phase and only lets them touch fields that actually exist in that phase. Pair it with an exhaustiveness check (assign the switch value to a never in default) so adding a fifth state is a compile error until you handle it. The tag can be any literal type — string, number, or boolean — as long as every member has it and the values are distinct.

**Use this technique when.** Any request/reducer/state machine. Redux action types are discriminated unions on `type`. Pairs with exhaustiveness checking via never.

```ts
type State<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

function render(s: State<string>) {
  switch (s.status) {
    case 'success': return s.data;    // narrowed: data exists
    case 'error':   return s.error;   // narrowed: error exists
    default:        return 'pending';
  }
}
```

**References.** [TS Handbook · Discriminated Unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) · [TS Handbook · never & exhaustiveness](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking)

---

### 9. Type guards & narrowing  `Medium`

**Pattern:** Narrowing

**Problem.** How does TS narrow types? Write a user-defined type guard, and show exhaustiveness with never.

**What it tests.** typeof/in/instanceof narrowing plus custom `x is T` predicates — and exhaustiveness with never.

**Approach & answer.** TS narrows a union based on control flow: typeof for primitives, instanceof for classes, `in` for property presence, and equality against literals. A user-defined guard `function isCat(a): a is Cat` teaches TS a custom narrowing rule. For exhaustiveness, assign the value to a never in the default branch — add a new union member and forget to handle it, and the code stops compiling. Key subtlety: the `a is Cat` return type is a TYPE PREDICATE — you're asserting to the compiler that a truthy return means the argument is a Cat, and TS trusts you, so the runtime check inside must actually be correct (a wrong predicate silently corrupts every downstream type). Truthiness narrowing (if (value) ...) removes null/undefined and other falsy types; the non-null assertion value! does it without a check (use sparingly). Prefer discriminated-union narrowing (a tag field) over `in`/typeof gymnastics when you control the types — it's cheaper to read and the exhaustiveness check comes for free.

**Use this technique when.** Discriminating shapes without a tag, validating unknown API data, and ensuring every case of a union is handled.

```ts
type Animal =
  | { kind: 'cat'; meow: () => void }
  | { kind: 'dog'; bark: () => void };

function assertNever(x: never): never {
  throw new Error('unhandled: ' + JSON.stringify(x));
}
function speak(a: Animal) {
  switch (a.kind) {
    case 'cat': return a.meow();
    case 'dog': return a.bark();
    default:    return assertNever(a); // compile error if a case is added
  }
}
```

**References.** [TS Handbook · Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) · [TS Handbook · Type predicates (a is T)](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)

---

### 10. as const and literal types vs enum  `Medium`

**Pattern:** Literal Types

**Problem.** What does `as const` do, and when would you prefer a literal union over a TypeScript enum?

**What it tests.** Understanding literal narrowing, readonly inference, and the runtime cost of enums.

**Approach & answer.** By default TypeScript widens literals: `let s = 'circle'` is inferred as string. `as const` freezes a value to its narrowest, deeply-readonly literal form — a string becomes its literal type, an array becomes a readonly tuple, and object properties become readonly with literal values. That is how you derive a union type from a runtime array: `typeof COLORS[number]`. Prefer a literal union (`type Color = 'red' | 'blue'`) over an enum in most cases: unions are erased at compile time (zero runtime code), they interoperate directly with plain string data from APIs, and `as const` objects give you the same grouping without emitting a bidirectional mapping object. Reach for a real enum only when you specifically want a named runtime namespace or numeric auto-increment. Note `const enum` avoids the runtime object but has its own build/isolatedModules caveats. The single-source pattern — define the array once with `as const`, derive both the values and the type from it — eliminates the drift between a type and its runtime list.

**Use this technique when.** Deriving a union from a config array, typing action strings, and avoiding enum runtime overhead.

```ts
const COLORS = ['red', 'green', 'blue'] as const;
type Color = typeof COLORS[number];   // 'red' | 'green' | 'blue'

const point = { x: 1, y: 2 } as const; // { readonly x: 1; readonly y: 2 }

// Prefer this union over: enum Color { Red, Green, Blue }
function paint(c: Color) {/* accepts plain strings from an API directly */}
```

**References.** [TS Handbook · Everyday Types (literal types)](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) · [TS Handbook · Enums](https://www.typescriptlang.org/docs/handbook/enums.html)

---

### 11. Function overloads vs generics vs unions  `Medium`

**Pattern:** Function Signatures

**Problem.** When should you use function overloads, and when is a generic or a union parameter the better tool?

**What it tests.** Choosing the right signature technique so the return type follows the input precisely.

**Approach & answer.** Overloads let one implementation advertise several distinct call signatures, so the return type depends on which argument shape the caller used — classic for DOM APIs like createElement('a') returning HTMLAnchorElement. But overloads are verbose and the single implementation signature must be a supertype of all of them. Prefer a generic when the relationship between input and output is uniform — the return type is a function of a type parameter (identity, arrays, mapping) — because one generic signature captures infinitely many cases that overloads would have to enumerate. Prefer a plain union parameter when the function accepts several types but treats them the same way and returns a single type; narrow inside with a type guard. Rule of thumb: same transformation over many types -> generic; different return type per input shape -> overload (or a conditional-type generic if the mapping is expressible); many inputs, one output -> union. Overuse of overloads is a smell that a generic or conditional type would be cleaner.

**Use this technique when.** Modeling APIs whose return type varies by argument (overload); write-once transformations over many types (generic).

```ts
// Overload: return type depends on the literal argument.
function make(tag: 'a'): HTMLAnchorElement;
function make(tag: 'div'): HTMLDivElement;
function make(tag: string): HTMLElement {
  return document.createElement(tag);
}
const link = make('a'); // typed HTMLAnchorElement

// Generic: uniform relationship, no overloads needed.
function first<T>(arr: readonly T[]): T | undefined {
  return arr[0];
}
```

**References.** [TS Handbook · Functions (overloads)](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads) · [TS Handbook · Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)

---

### 12. The satisfies operator  `Medium`

**Pattern:** satisfies Operator

**Problem.** What problem does `satisfies` solve that neither a type annotation nor `as` does? Show a config object where it matters.

**What it tests.** Whether you can validate a value against a type while keeping its precise inferred type.

**Approach & answer.** `satisfies` checks that a value conforms to a type WITHOUT widening the value's inferred type. The classic dilemma: annotate `const config: Record<string, string | number>` and you get validation but lose specifics (config.port is now `string | number`, and TS forgets which keys exist); omit the annotation and you keep precise types but get no guarantee the object matches the intended shape. `as` is worse — it asserts and can hide real mismatches. `satisfies` gives you both: TS verifies the literal is assignable to the constraint (catching typos and wrong value types at the definition site), then keeps the NARROW inferred type for everything downstream. So `const routes = {…} satisfies Record<string, Route>` still lets you read `routes.home` as a concrete `Route` (not an index-signature `Route | undefined`) and preserves the literal types of the values. It's the modern answer to typed config, palettes, action maps, and `as const`-style objects that also need to honor an interface. Rule of thumb: reach for `satisfies` whenever you want a compile-time guarantee about a value's shape but don't want to lose the exact type the literal would otherwise infer. Combine it with `as const` when you additionally need readonly/literal narrowing on top of the constraint check.

**Use this technique when.** Typed config objects, color palettes, route tables, reducer action maps — anywhere you want validation AND precise inferred value types.

```ts
type Palette = Record<string, number[] | string>;

const colors = {
  red: [255, 0, 0],
  bg:  '#ffffff',
} satisfies Palette;

colors.red.map(n => n);   // number[] — precise type kept, not (number[] | string)
colors.bg.toUpperCase();  // string   — TS knows this key is the string branch
// const bad = { red: [1], n: 2 } satisfies Palette; // error: 2 isn't string|number[]
```

**References.** [TS 4.9 Release Notes · The satisfies Operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator) · [TS Handbook · Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html)

---

### 13. keyof, typeof, and indexed access types  `Medium`

**Pattern:** keyof, typeof & Indexed Access

**Problem.** Explain `keyof`, the `typeof` type operator, and indexed access types. Use them to write a fully type-safe property getter.

**What it tests.** Deriving types from other types/values so a getter's return type follows the key you pass.

**Approach & answer.** These three operators let types be DERIVED instead of hand-written. `keyof T` is the union of T's property names (`keyof {a:1;b:2}` is `'a' | 'b'`). The `typeof` type operator (distinct from the runtime JS `typeof`) lifts a value into its type — `typeof config` gives the type TS inferred for the value, so you define data once and derive its type. Indexed access `T[K]` looks up the type of a property: `User['id']` is that field's type, and `T[keyof T]` is the union of all value types. Together they express a type-safe getter: `get<T, K extends keyof T>(obj: T, key: K): T[K]` — the key parameter is constrained to real keys of the object (a typo'd key is a compile error) and the return type `T[K]` is the type of THAT specific property, not a widened union. Pass `'name'` and you get back exactly `string`; pass `'age'` and you get `number`. This is the backbone of typed form libraries, ORMs, and prop utilities. Combine with `typeof`: `type Keys = keyof typeof config` derives the allowed keys straight from a runtime object, keeping type and data in lockstep. And `T[number]` on an array/tuple extracts the element type — the trick behind `typeof ARRAY[number]` unions.

**Use this technique when.** Type-safe get/set/pluck helpers, form field names, deriving key unions from a config object (keyof typeof), extracting array element types.

```ts
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { id: 'u1', name: 'Ada', age: 36 };
const a = get(user, 'age');   // a: number
const n = get(user, 'name');  // n: string
// get(user, 'nope');         // error: 'nope' is not a key of user

type UserKeys = keyof typeof user;      // 'id' | 'name' | 'age'
type AgeType  = (typeof user)['age'];   // number
```

**References.** [TS Handbook · keyof & typeof Operators](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html) · [TS Handbook · Indexed Access Types](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html)

---

### 14. Write your own mapped + conditional type  `Hard`

**Pattern:** Mapped & Conditional Types

**Problem.** Implement DeepReadonly<T> and explain mapped types, keyof, and conditional types.

**What it tests.** Type-level programming — the ceiling question that separates 'uses TS' from 'thinks in TS'.

**Approach & answer.** A mapped type iterates keys: { [K in keyof T]: ... }. keyof T is the union of T's keys. A conditional type A extends B ? X : Y branches at the type level. DeepReadonly maps every key to readonly and recurses into object values (conditional: if the value is an object, recurse; else leave it). This is exactly how built-ins like Partial are implemented. Two power tools sit on top of these: key remapping with `as` ({ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } builds getter names from keys), and `infer` inside a conditional to capture a type (type ElementType<T> = T extends (infer U)[] ? U : never pulls the element type out of an array). Modifiers add or strip with +/-: { [K in keyof T]-?: T[K] } removes optionality (that's Required<T>), and -readonly strips readonly. Distributive conditionals matter too: when the checked type is a naked type parameter, T extends U ? ... distributes over each member of a union — wrap in [T] to opt out. These are the building blocks of every advanced library type.

**Use this technique when.** Library authoring, deriving types (form types from a schema, immutable state trees), transforming API types into UI types without hand-writing them.

```ts
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

interface Config { server: { port: number }; debug: boolean; }
type Frozen = DeepReadonly<Config>;
// Frozen.server.port is readonly, deeply.
```

**References.** [TS Handbook · Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) · [TS Handbook · Conditional Types & infer](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html)

---

### 15. Template literal types  `Hard`

**Pattern:** Template Literal Types

**Problem.** What are template literal types? Build an event-handler name type from a union of events, and show inferring a piece back out of a string pattern.

**What it tests.** String-level type manipulation — deriving precise string types instead of loose `string`.

**Approach & answer.** Template literal types apply JS template-literal syntax at the TYPE level: `on${Capitalize<Event>}` produces a new string-literal type for each member of the Event union (they distribute over unions, so `'click' | 'focus'` becomes `'onClick' | 'onFocus'`). TS ships intrinsic string manipulators — `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize` — usable inside them. The power move is `infer` inside a conditional that matches a pattern: `type EventName<T> = T extends `on${infer E}` ? E : never` pulls the event back out of the handler name, so the transformation is reversible at the type level. This is how libraries type CSS-in-JS keys, i18n paths, and route params (`/users/${infer Id}`). Combined with mapped-type key remapping (`{ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }`), you can synthesize a whole API surface — a correctly-typed getter per field — from a plain interface. The constraint to remember: these operate purely in the string type domain, cost nothing at runtime, and TS caps expansion, so a template over two large unions multiplies and can blow up compile time — keep the input unions bounded.

**Use this technique when.** Typed event-handler props (onClick), route/i18n path types, deriving getter/setter names, and CSS property key types.

```ts
type Event = 'click' | 'focus';
type Handler = `on${Capitalize<Event>}`;   // 'onClick' | 'onFocus'

type EventOf<T> = T extends `on${infer E}` ? Uncapitalize<E> : never;
type Back = EventOf<'onClick'>;             // 'click'

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type G = Getters<{ name: string }>;         // { getName: () => string }
```

**References.** [TS Handbook · Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) · [TS Handbook · Key Remapping via as](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as)

---

### 16. Variance & function parameter bivariance  `Hard`

**Pattern:** Variance

**Problem.** Explain covariance and contravariance for function types. Why are method parameters bivariant in TypeScript, and what does `strictFunctionTypes` change?

**What it tests.** Deep type-system reasoning about when one function type is assignable to another.

**Approach & answer.** Variance describes how the compatibility of a function follows the compatibility of its parts. Function RETURN types are covariant: a `() => Dog` is assignable where `() => Animal` is expected (returning something more specific is safe). Function PARAMETER types are contravariant under sound rules: a handler `(a: Animal) => void` is assignable where `(d: Dog) => void` is expected — it accepts anything the callee might pass, so it's safe; the reverse (a Dog-only handler used where any Animal may arrive) is unsound. `strictFunctionTypes` turns on this contravariant checking for function-typed parameters. The catch: it does NOT apply to METHODS declared with method shorthand (`m(x: T): void`) — those stay BIVARIANT (assignable both directions) deliberately, because otherwise generic collections like `Array<T>` (whose methods take T) would become painfully un-assignable, and because a lot of existing DOM/event typings rely on it. So `(x: Dog) => void` written as a property is checked strictly, but the same signature as a method is not. Practical upshot: prefer property-style function fields (`onEvent: (e: E) => void`) over method shorthand when you want the compiler to catch unsafe handler substitutions — and know that event-handler assignability 'just working' often rests on method bivariance rather than being truly sound.

**Use this technique when.** Reasoning about handler/callback assignability, designing generic APIs, choosing method vs property function fields, debugging why a callback type does or doesn't error.

```ts
class Animal {}
class Dog extends Animal { bark() {} }

type Handler<T> = (x: T) => void;
let animalH: Handler<Animal> = () => {};
let dogH: Handler<Dog>       = (d) => d.bark();

dogH = animalH;    // OK: contravariant — an Animal handler is safe for Dog
// animalH = dogH; // error under strictFunctionTypes: Dog handler unsafe for Animal

interface WithMethod { on(x: Dog): void; }     // method shorthand: bivariant
interface WithProp   { on: (x: Dog) => void; } // property: strict
```

**References.** [TS 2.6 Release Notes · Strict Function Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types) · [TSConfig · strictFunctionTypes](https://www.typescriptlang.org/tsconfig/#strictFunctionTypes)

---

### 17. Branded (nominal) types over a structural system  `Hard`

**Pattern:** Branded Types

**Problem.** TypeScript is structural, so `UserId` and `OrderId` (both `string`) are interchangeable. How do you make them incompatible — and what's the cost?

**What it tests.** Simulating nominal typing to stop semantically-different values of the same primitive from mixing.

**Approach & answer.** Because TS is structural, two aliases of `string` are freely interchangeable, so nothing stops you passing an OrderId where a UserId is expected — a real class of bugs (mixing ids, currencies, validated vs unvalidated strings). Branding fakes nominal typing by intersecting the primitive with a phantom, unique property that exists only in the type world: `type UserId = string & { readonly __brand: 'UserId' }`. No value actually carries `__brand` at runtime — it's a compile-time tag that makes UserId and OrderId structurally distinct, so they no longer assign to each other or to a plain `string` parameter that expects the brand. You mint a branded value through a single checked constructor (`function toUserId(s: string): UserId { /* validate */ return s as UserId }`) — the one sanctioned `as`, which centralizes validation. Using a `unique symbol` for the brand key makes collisions impossible across modules. The costs: it's a convention, not enforced at runtime (a raw string cast through `as` still slips in — so guard the boundaries), and it adds a little ceremony (constructors, occasional assertions). The payoff is large for values where mixing is dangerous: entity ids, `Email`/`Url` after validation, `Cents` vs `Dollars`, `SafeHtml` vs `string`. It encodes 'this string has been checked / means X' into the type, turning a whole category of mixups into compile errors.

**Use this technique when.** Entity IDs (UserId vs OrderId), validated values (Email, Url, SafeHtml), units (Cents vs Dollars) — anywhere same-typed values must not mix.

```ts
type UserId  = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

const toUserId = (s: string): UserId => s as UserId; // sole checked constructor

function loadUser(id: UserId) { /* ... */ }

const uid = toUserId('u_1');
loadUser(uid);        // ok
// loadUser('u_1');   // error: a plain string isn't a UserId
// loadUser(orderId); // error: OrderId isn't UserId (both are strings!)
```

**References.** [TS Handbook · Type Compatibility (structural)](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) · [TS Deep Dive · Nominal Typing](https://basarat.gitbook.io/typescript/main-1/nominaltyping)

---

### 18. Recursive types & type-safe object paths  `Hard`

**Pattern:** Recursive Types

**Problem.** Write a recursive type. Then build a `Paths<T>` that produces the dotted key paths of a nested object, and a `Get<T, P>` that returns the type at a path.

**What it tests.** Type-level recursion combined with template literals and indexed access to walk a nested structure.

**Approach & answer.** A recursive type refers to itself, letting one definition describe arbitrarily nested data — the canonical example is a JSON value: `type Json = string | number | boolean | null | Json[] | { [k: string]: Json }`. The same recursion powers `DeepReadonly`/`DeepPartial` and, more ambitiously, type-safe paths. `Paths<T>` walks the object: for each key K, emit K itself, and if `T[K]` is an object, also emit `${K}.${Paths<T[K]>}` — a template-literal type concatenating the key with the child's paths, recursing until it hits primitives. `Get<T, P>` is the inverse: split the path on `.` with `P extends `${infer Head}.${infer Rest}``, index in with `T[Head]`, and recurse on Rest; a bare key is just `T[P]`. Together they give a `get(obj, 'user.address.city')` that's fully checked — an invalid path is a compile error and the return type is exactly the type at that leaf. Caveats that separate theory from practice: TS limits recursion depth (deeply nested types can hit 'type instantiation is excessively deep'), unbounded or cyclic structures need a depth guard or they won't terminate, and array/number indices need extra handling (`${number}`). This machinery is the core of typed form libraries (react-hook-form), i18n key checkers, and lodash-`get` wrappers.

**Use this technique when.** Typed lodash-get wrappers, form field paths (react-hook-form), i18n key validation, deep state selectors.

```ts
type Paths<T> = {
  [K in keyof T & string]: T[K] extends object
    ? K | `${K}.${Paths<T[K]>}`
    : K;
}[keyof T & string];

type Get<T, P extends string> =
  P extends `${infer H}.${infer R}`
    ? H extends keyof T ? Get<T[H], R> : never
    : P extends keyof T ? T[P] : never;

type Data = { user: { address: { city: string } } };
type P = Paths<Data>;                      // 'user' | 'user.address' | 'user.address.city'
type C = Get<Data, 'user.address.city'>;   // string
```

**References.** [TS 4.1 Release Notes · Recursive Conditional Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types) · [TS Handbook · Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html)

---

## React

> Beyond 'what is a hook', seniors are tested on the mental model: render → reconcile → commit, why effects run when they do, referential identity, and where re-renders come from. Interviews increasingly include a machine-coding task (build a component live).

### 1. render → reconcile → commit  `Easy`

**Pattern:** Mental Model

**Problem.** Walk through what happens when state changes. What are the three phases?

**What it tests.** Whether you understand React is declarative and diff-based, not imperative DOM manipulation.

**Approach & answer.** (1) Render — a state/prop change marks the component dirty; React calls your function to produce a new element tree (pure, no DOM touched yet). (2) Reconcile — React diffs the new tree against the previous one using keys and element type to find the minimal set of changes. (3) Commit — React applies those changes to the real DOM and then runs layout effects, then paints, then passive effects (useEffect). Understanding this explains why keys matter and why effects run after paint. A few consequences fall straight out of this model: render must be pure because React may call it multiple times or throw it away (concurrent features, StrictMode's double-invoke in dev exist to surface impurity); setState during render of the same component is how you derive state, but setState in an effect triggers a second render-commit cycle before paint only for useLayoutEffect. Reconciliation bails out early when the element type is identical and props are shallow-equal under React.memo, and it throws away the entire subtree when the type differs — which is why a changing key (or a conditional that swaps component type) remounts and resets state. 'Why did this re-render' almost always traces back to: parent re-rendered, state/context changed, or a new object/function identity defeated memoization.

**Use this technique when.** Reasoning about re-renders, why a wrong key remounts a component, and when effects fire relative to paint.

```jsx
function Counter() {
  const [n, setN] = React.useState(0);
  // setN -> RENDER (recompute) -> RECONCILE (diff) -> COMMIT (patch DOM)
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}
```

**References.** [react.dev · Render and Commit](https://react.dev/learn/render-and-commit) · [react.dev · Preserving and Resetting State](https://react.dev/learn/preserving-and-resetting-state)

---

### 2. Rules of Hooks — and why  `Easy`

**Pattern:** Hooks Rules

**Problem.** What are the rules of hooks? Why can't you call a hook inside a condition?

**What it tests.** Whether you understand hooks are matched by call ORDER, not by name.

**Approach & answer.** Two rules: call hooks only at the top level (never in conditions, loops, or nested functions), and only from React functions. React tracks hook state by the ORDER of calls on each render — it has no names to go by. If a condition skips a useState on some renders, every subsequent hook shifts by one slot and reads the wrong state. Keeping calls unconditional keeps the order stable. Mechanically, React keeps a linked list (or array) of hook 'cells' per component instance and walks it in the same sequence every render; the Nth useState call always maps to the Nth cell. That's why the fix is always to branch INSIDE the hook, not around it — put the condition in the effect body, or pass a conditional dependency, or early-return AFTER all hooks. The eslint-plugin-react-hooks rules (rules-of-hooks + exhaustive-deps) catch the vast majority of violations at lint time; treat exhaustive-deps warnings as correctness bugs, not style nits. Custom hooks inherit the same rules because they're just functions that call hooks, which is also why they must be named useSomething so the linter can recognize them.

**Use this technique when.** Explaining a 'rendered fewer hooks than expected' error, and why you branch INSIDE a hook, not around it.

```jsx
// WRONG: conditional hook shifts the order
if (loggedIn) { const [x] = useState(0); }

// RIGHT: hook is unconditional; branch inside
const [x, setX] = useState(0);
if (loggedIn) { /* use x */ }
```

**References.** [react.dev · Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) · [react.dev · State: a component's memory](https://react.dev/learn/state-a-components-memory)

---

### 3. Controlled vs uncontrolled inputs  `Easy`

**Pattern:** Forms & State Ownership

**Problem.** What is the difference between a controlled and an uncontrolled input, and when do you choose each?

**What it tests.** Who owns the form value — React state or the DOM — and the trade-offs.

**Approach & answer.** A controlled input is driven by React state: its value comes from state and every keystroke fires onChange to update that state, so React is the single source of truth. This makes validation, formatting, conditional disabling, and derived UI trivial because you always have the current value in render. An uncontrolled input lets the DOM keep its own value; you read it on demand via a ref (or from the submit event / FormData), using defaultValue for the initial value. Uncontrolled is lighter — no re-render per keystroke — and is the natural fit for file inputs (which are always uncontrolled) and for integrating non-React widgets. Choose controlled when you need live validation, cross-field logic, or to reflect the value elsewhere as the user types; choose uncontrolled for simple submit-only forms or performance-sensitive large forms. The cardinal bug is setting value without onChange (or vice versa), which makes the field read-only or drops React's control — pass both, or use defaultValue for uncontrolled.

**Use this technique when.** Controlled for live validation and dependent fields; uncontrolled/refs for submit-only forms, file inputs, and perf.

```jsx
// Controlled: React owns the value.
function Controlled() {
  const [name, setName] = React.useState('');
  return <input value={name} onChange={e => setName(e.target.value)} />;
}

// Uncontrolled: the DOM owns it; read via ref on demand.
function Uncontrolled() {
  const ref = React.useRef(null);
  const submit = () => console.log(ref.current.value);
  return <input defaultValue="" ref={ref} onBlur={submit} />;
}
```

**References.** [react.dev · <input>](https://react.dev/reference/react-dom/components/input) · [react.dev · Reacting to Input with State](https://react.dev/learn/reacting-to-input-with-state)

---

### 4. What JSX compiles to  `Easy`

**Pattern:** JSX Fundamentals

**Problem.** What is JSX, really? What does `<Foo bar={1} />` compile to, and why must components be capitalized?

**What it tests.** Whether you understand JSX is syntax sugar over function calls that return plain objects, not HTML.

**Approach & answer.** JSX is syntactic sugar — a compiler (Babel/tsc/swc) transforms each tag into a function call. Historically that call was React.createElement(type, props, ...children); since React 17's automatic runtime it's a _jsx(type, props) imported from react/jsx-runtime, which is why you no longer need React in scope to use JSX. The call returns a plain, immutable object — a React element — describing WHAT to render (type, props, key), not any actual DOM; React reconciles that object into the DOM later. Capitalization is the crux: a lowercase tag like 'div' compiles to a STRING type ('div'), meaning a host/DOM element, while a Capitalized tag like Foo compiles to a reference to the variable Foo (your component). So a lowercase component name is read as an unknown HTML tag and renders nothing useful — the compiler literally emits the string instead of your function. Attributes become the props object (bar={1} becomes {bar: 1}); nested content becomes props.children (a string, an element, or an array). Because JSX tags are just expressions, the curly braces embed any JS expression (not statements), and you can store elements in variables, return them from functions, and map arrays into them. className and htmlFor exist because 'class' and 'for' are reserved words. A Fragment (the empty-tag form) compiles to React.Fragment so you can return siblings without adding a wrapper DOM node. Finally key and ref are special-cased: React plucks them off and they are NOT passed to your component as props.

**Use this technique when.** Explaining why components must be capitalized, why elements can live in variables, and what a React element actually is.

```jsx
// This JSX...
const el = <Welcome name="Ada" className="greeting" />;

// ...compiles to a function call that returns a plain object:
const el2 = React.createElement(Welcome, { name: 'Ada', className: 'greeting' });
// -> { type: Welcome, props: { name: 'Ada', className: 'greeting' }, key: null }

// Capitalized -> component reference (Welcome, a variable)
// lowercase   -> host element string ('div')
const dom = <div />;   // React.createElement('div', null)
```

**References.** [react.dev · Writing Markup with JSX](https://react.dev/learn/writing-markup-with-jsx) · [react.dev · JavaScript in JSX with Curly Braces](https://react.dev/learn/javascript-in-jsx-with-curly-braces)

---

### 5. useRef as a mutable instance variable  `Easy`

**Pattern:** Refs

**Problem.** Besides pointing at a DOM node, what is useRef for? How is a ref different from state, and when do you reach for one?

**What it tests.** Understanding a ref as a mutable box that persists across renders WITHOUT triggering a re-render.

**Approach & answer.** useRef returns a stable, mutable object { current: initialValue } that persists for the component's entire lifetime — and crucially, mutating .current does NOT trigger a re-render. There are two distinct uses. (1) A handle to a DOM node: attach it via the ref attribute, then call inputRef.current.focus(). (2) A general mutable INSTANCE VARIABLE for values you must remember across renders but that should not appear on screen: an interval/timeout id, the previous value of a prop, a mutable flag like 'has the first render happened yet', a WebSocket or AbortController, or the latest callback (to escape a stale closure). The dividing line versus state: use STATE when a value change should re-render the UI; use a REF when it should not. Because a ref is just a plain object, writing ref.current = x DURING render is a foot-gun — renders must be pure, so mutate refs in event handlers and effects, not in the render body. Refs also do not notify anyone, so reading ref.current during render can hand you a stale value — never derive rendered output from a ref. Compared with a plain let: a let declared in the component body is reset to its initial value on every render, whereas a ref survives across renders; compared with a module-level variable, a ref is per-instance (each mounted component gets its own box) rather than shared. The classic precise use is capturing the previous value: store the current value into a ref inside an effect, and on the next render the ref still holds the prior one.

**Use this technique when.** Timer/interval ids, previous-value tracking, mutable flags, non-React object handles — anything to remember without re-rendering.

```jsx
function Timer() {
  const [seconds, setSeconds] = React.useState(0);
  const intervalRef = React.useRef(null);        // mutable box; writing it never re-renders

  function start() {
    if (intervalRef.current) return;             // already running — the ref remembers
    intervalRef.current = setInterval(() => setSeconds(s => s + 1), 1000);
  }
  function stop() {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  }
  React.useEffect(() => stop, []);               // clear the timer on unmount

  return (
    <div>
      <p>{seconds}s elapsed</p>
      <button onClick={start}>Start</button>{' '}
      <button onClick={stop}>Stop</button>
    </div>
  );
}
```

**References.** [react.dev · useRef](https://react.dev/reference/react/useRef) · [react.dev · Referencing Values with Refs](https://react.dev/learn/referencing-values-with-refs)

---

### 6. Lifting state up & colocation  `Easy`

**Pattern:** State Management

**Problem.** Two sibling components need the same data. Where should the state live? What are 'lifting state up' and 'colocation'?

**What it tests.** Reasoning about state ownership — a single source of truth and choosing where it belongs.

**Approach & answer.** When two components need the same state, move it UP to their closest common parent and pass it down as props (the value) plus a callback (to change it). That is 'lifting state up': the parent becomes the single source of truth and the children become controlled — they render the value and report intent via callbacks instead of each keeping a private copy. Keeping a duplicate copy in both siblings is exactly the bug this prevents; the two copies drift out of sync. The complementary principle is COLOCATION: keep each piece of state as low, as close to where it is used, as possible, and lift only when sharing forces you to. Over-lifting (hoisting everything into a top-level component) makes every keystroke re-render the whole tree and couples unrelated parts; under-lifting (duplicating) causes sync bugs. The rule of thumb: colocate by default, lift on demand to the nearest common ancestor and no higher. A telltale sign you have lifted too far is a prop threaded through many layers that do not use it ('prop drilling') — the cue to either colocate lower, compose by passing children through, or reach for Context when the state is genuinely cross-cutting/global. And derived data should not be state at all: compute it during render from the source of truth rather than storing a second, drift-prone copy.

**Use this technique when.** Deciding where state lives; fixing out-of-sync sibling copies; knowing when to lift vs colocate vs reach for Context.

```jsx
// Parent owns the shared state — the single source of truth.
function Form() {
  const [name, setName] = React.useState('');
  return (
    <div>
      <NameInput value={name} onChange={setName} />
      <Greeting name={name} />          {/* sibling reads the SAME state */}
    </div>
  );
}

// Children are controlled: render the prop, report intent upward.
function NameInput({ value, onChange }) {
  return <input value={value} onChange={e => onChange(e.target.value)} placeholder="Your name" />;
}
function Greeting({ name }) {
  return <p>Hello, {name || 'stranger'}!</p>;   // derived from the source, not stored
}
```

**References.** [react.dev · Sharing State Between Components](https://react.dev/learn/sharing-state-between-components) · [react.dev · Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure)

---

### 7. useEffect dependencies & cleanup  `Medium`

**Pattern:** Effects

**Problem.** Explain the dependency array and cleanup. What's the fetch-in-effect race condition and how do you fix it?

**What it tests.** The most misused hook. Dependency correctness and cleanup are senior signals.

**Approach & answer.** useEffect runs after commit; the dependency array decides WHEN it re-runs (empty = once, [x] = when x changes, omitted = every render). The cleanup function runs before the next effect and on unmount. The race: if a prop changes fast, an earlier fetch can resolve AFTER a later one and overwrite fresh data with stale. Fix with a cancelled/ignore flag in cleanup (or an AbortController) so a superseded response is dropped. Deeper points interviewers probe: the dependency array must list EVERY reactive value the effect reads (props, state, and functions/objects defined in render) — omitting one gives you a stale closure that silently reads old values; the honest fixes are to move the value inside the effect, wrap it in useCallback/useMemo, or use a ref. Many effects shouldn't exist at all: don't use an effect to transform data for rendering (compute during render), to reset state on prop change (use a key), or to handle a user event (do it in the handler). The mental model react.dev pushes is 'synchronize with an external system' — a subscription, the DOM, a network resource — and every synchronization needs its teardown, which is what cleanup is for.

**Use this technique when.** Data fetching tied to props, subscriptions, timers, event listeners — anything with setup that needs teardown.

```jsx
useEffect(() => {
  let ignore = false;
  fetch(`/api/user/${id}`)
    .then(r => r.json())
    .then(data => { if (!ignore) setUser(data); }); // drop stale response
  return () => { ignore = true; };  // cleanup on id change / unmount
}, [id]);
```

**References.** [react.dev · Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects) · [react.dev · You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)

---

### 8. useMemo, useCallback & React.memo  `Medium`

**Pattern:** Referential Identity

**Problem.** When do useMemo / useCallback actually help? Why does passing an inline object/function break React.memo?

**What it tests.** Whether you memoize for a REASON (identity/expense), not by cargo-cult on everything.

**Approach & answer.** React.memo skips a re-render when props are shallow-equal. But an inline object or function is a NEW reference every render, so shallow-equal fails and memo is defeated. useCallback stabilizes a function's identity; useMemo stabilizes a computed value's (or caches an expensive calc). Use them when (a) a value is an expensive computation, or (b) a reference is passed to a memoized child or an effect's deps. Everywhere else they add cost for no benefit. Three clarifications that separate cargo-cult from judgment: memoization has its own cost (the comparison plus holding the previous value), so wrapping a cheap leaf component buys nothing; useCallback(fn, deps) is exactly useMemo(() => fn, deps), just sugar for functions; and React.memo only compares props — it does nothing about a re-render caused by internal state or a changed context value. The chain has to be complete to work: a memoized child still re-renders if ANY prop is a fresh reference, so stabilizing one callback while passing a new inline object next to it accomplishes nothing. The forward-looking note: the React Compiler (React 19+) memoizes automatically, which will make most manual useMemo/useCallback unnecessary — another reason to reserve them for measured, specific wins today.

**Use this technique when.** Passing callbacks/objects to memoized children, stabilizing effect deps, memoizing genuinely expensive derived data.

```jsx
const Child = React.memo(({ onClick }) => <button onClick={onClick} />);

function Parent() {
  // WITHOUT useCallback: new fn each render -> Child always re-renders
  const onClick = React.useCallback(() => doThing(), []);
  return <Child onClick={onClick} />;
}
```

**References.** [react.dev · useMemo](https://react.dev/reference/react/useMemo) · [react.dev · useCallback](https://react.dev/reference/react/useCallback)

---

### 9. Context vs Redux — when to use which  `Medium`

**Pattern:** State Management

**Problem.** When is Context enough and when do you reach for Redux? What's the Context re-render pitfall?

**What it tests.** Architectural judgment — you built shared Redux patterns for 3+ teams at Domo.

**Approach & answer.** Context is a dependency-injection mechanism, not a state manager: every consumer re-renders when the provider value changes, so it fits low-frequency global values (theme, current user, locale). Redux (or Zustand/Jotai) fits high-frequency, complex, shared state that many components read/write, where you want selectors (subscribe to a SLICE, avoiding blanket re-renders), middleware, devtools, and predictable updates. Pitfall: putting fast-changing state in Context re-renders the whole subtree — split contexts or use a selector-based store. The nuance interviewers want: Context has no built-in selector, so a consumer can't subscribe to just part of the value — any change to the provider value re-renders all consumers, full stop. Mitigations are splitting into multiple contexts (e.g. separate state and dispatch, which never changes) and memoizing the value object. Modern practice also separates SERVER state from CLIENT state: React Query / RTK Query own cache, refetch, and invalidation for anything that comes from an API, leaving Redux/Zustand for genuine client state (wizard steps, selections, optimistic UI). Given your Domo background building shared Redux patterns for multiple teams, the strongest answer frames it as: Context for injection, a selector store for cross-cutting client state, a data-fetching library for server cache — three tools, three jobs.

**Use this technique when.** Context: theme/auth/i18n. Redux/store: server cache, cross-team shared domain state, anything needing selectors or middleware.

```jsx
// Context re-render pitfall: value is a NEW object each render
<ThemeContext.Provider value={{ theme, setTheme }}>  // re-renders all consumers

// Fix: memoize the value, or split state/dispatch into two contexts
const value = React.useMemo(() => ({ theme, setTheme }), [theme]);
```

**References.** [react.dev · useContext](https://react.dev/reference/react/useContext) · [Redux · When (and when not) to reach for Redux](https://redux.js.org/faq/general#when-should-i-use-redux)

---

### 10. Extract logic into a custom hook  `Medium`

**Pattern:** Custom Hooks

**Problem.** Write a reusable useDebouncedValue(value, delay) hook and explain when to build a custom hook.

**What it tests.** Composition — extracting stateful logic (not UI) for reuse. Core to a shared component library.

**Approach & answer.** A custom hook is just a function that calls other hooks; it lets you share stateful LOGIC without sharing UI or resorting to HOCs/render-props. useDebouncedValue keeps a debounced copy of a value in state and updates it via a timer that resets on each change, cleaning up the timer on change/unmount. Build a custom hook when the same use-of-hooks pattern (fetching, subscriptions, form state, media queries) repeats across components. Two properties make custom hooks powerful and safe: each call site gets its OWN isolated state (two components using useDebouncedValue don't share a timer), because a custom hook is a code-reuse mechanism, not a shared-state mechanism — for shared state you still need context or a store. And they compose: a useSearch hook can call useDebouncedValue and useFetch internally, building higher-level behavior from lower-level hooks the same way functions compose. Naming matters mechanically — the use prefix is what lets the linter enforce the Rules of Hooks inside them. The design guideline react.dev gives: a good custom hook wraps a concrete, nameable behavior ('debounce a value', 'track online status') rather than being a grab-bag of unrelated logic, and it returns the minimal interface its callers need.

**Use this technique when.** Search inputs (debounce), data fetching, form handling, window/media listeners — any repeated stateful behavior.

```jsx
function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = React.useState(value);
  React.useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);   // reset on change / unmount
  }, [value, delay]);
  return debounced;
}
// const q = useDebouncedValue(input); useEffect(() => search(q), [q]);
```

**References.** [react.dev · Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) · [react.dev · useState](https://react.dev/reference/react/useState)

---

### 11. Error boundaries  `Medium`

**Pattern:** Resilience

**Problem.** What is an error boundary, what does it catch and not catch, and how do you use one?

**What it tests.** Knowing React's failure model and how to contain render-time crashes.

**Approach & answer.** An error boundary is a component that catches JavaScript errors thrown during rendering, in lifecycle methods, and in constructors of the tree BELOW it — then renders a fallback UI instead of letting the whole app unmount to a blank screen. It must be a class component implementing static getDerivedStateFromError (to flip to the fallback) and/or componentDidCatch (to log to a service). Crucially, it does NOT catch: errors in event handlers (use try/catch there — those aren't during render), asynchronous code (setTimeout, fetch callbacks), server-side rendering, or errors thrown in the boundary itself. Place boundaries strategically — one near the root for a global fallback, plus finer-grained boundaries around independent regions (a widget, a route, a dashboard panel) so one failing section doesn't take down the rest. There is no built-in hook version; teams use react-error-boundary, which also adds a reset mechanism to recover without a full reload. Pair boundaries with Suspense for a complete loading-and-error story.

**Use this technique when.** Wrapping routes, widgets, or third-party/render-risky subtrees so a crash degrades locally instead of blanking the app.

```jsx
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(error, info) { logToService(error, info); }
  render() {
    if (this.state.hasError) return <p>Something went wrong.</p>;
    return this.props.children;
  }
}
// <ErrorBoundary><Dashboard /></ErrorBoundary>  // event-handler & async errors NOT caught
```

**References.** [react.dev · Component (catching rendering errors)](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) · [react.dev · createRoot onCaughtError](https://react.dev/reference/react-dom/client/createRoot#displaying-a-dialog-for-recoverable-errors)

---

### 12. useReducer vs useState  `Medium`

**Pattern:** State Management

**Problem.** When should you reach for useReducer instead of useState?

**What it tests.** Recognizing when centralizing transition logic beats scattered setState calls.

**Approach & answer.** useState is ideal for independent, simple pieces of state. Reach for useReducer when the next state depends on the previous state through non-trivial transitions, when several values change together as part of one logical event, or when update logic is complex enough to be worth naming (a reducer gives each transition an action name and a single place to read them all). A reducer is a pure (state, action) => newState function, so it is trivially unit-testable in isolation and keeps the component's event handlers thin — they just dispatch intent ({ type: 'increment' }) rather than computing new state inline. It also stabilizes callbacks: dispatch has a stable identity across renders, so passing it deep through context or memoized children avoids the referential-identity churn that setState-derived callbacks can cause. Rule of thumb: multiple related fields, or state whose transitions you'd otherwise duplicate across handlers -> useReducer; one or two loosely-related values -> useState. For truly global state, lift the reducer into context or use a store library.

**Use this technique when.** Multi-field forms, wizards, undo/redo, or any component where transitions are complex or shared across handlers.

```jsx
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'reset': return { count: 0 };
    default: throw new Error('unknown action');
  }
}
function Counter() {
  const [state, dispatch] = React.useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}
```

**References.** [react.dev · useReducer](https://react.dev/reference/react/useReducer) · [react.dev · Extracting State Logic into a Reducer](https://react.dev/learn/extracting-state-logic-into-a-reducer)

---

### 13. useLayoutEffect vs useEffect  `Medium`

**Pattern:** Effects / Timing

**Problem.** When would you reach for useLayoutEffect instead of useEffect, and what's the cost of getting it wrong?

**What it tests.** Understanding the browser paint cycle and where each effect fires relative to it.

**Approach & answer.** Both run after render, but at different points relative to paint. useEffect fires asynchronously *after* the browser has painted — so if your effect mutates the DOM in a way the user can see (measuring an element then repositioning a tooltip, syncing scroll position), the user briefly sees the un-adjusted frame, i.e. a flicker. useLayoutEffect fires synchronously *after DOM mutations but before paint*, so you can read layout (getBoundingClientRect, offsetHeight) and write derived styles in the same frame, and the user never sees the intermediate state. The cost: useLayoutEffect blocks painting, so heavy work there freezes the UI — use it only for reads/writes that must happen before paint, and keep it cheap. Default to useEffect; escalate to useLayoutEffect only to kill a visible flicker caused by measuring-then-mutating layout. Note it also warns during SSR because there's no layout phase on the server — guard with a client check or use the useEffect fallback for isomorphic components.

**Use this technique when.** You measure the DOM then mutate it and see a flicker → useLayoutEffect (before paint). Otherwise useEffect.

```jsx
function Tooltip({ targetRef }) {
  const tipRef = React.useRef(null);
  const [pos, setPos] = React.useState({ top: 0, left: 0 });
  // Runs before paint: measure target, position tooltip in the same frame.
  React.useLayoutEffect(() => {
    const t = targetRef.current.getBoundingClientRect();
    const h = tipRef.current.offsetHeight;
    setPos({ top: t.top - h, left: t.left }); // no visible flicker
  }, [targetRef]);
  return <div ref={tipRef} style={{ position: 'fixed', ...pos }}>Hint</div>;
}
```

**References.** [react.dev · useLayoutEffect](https://react.dev/reference/react/useLayoutEffect) · [react.dev · useEffect](https://react.dev/reference/react/useEffect)

---

### 14. forwardRef and useImperativeHandle  `Medium`

**Pattern:** Refs & Imperative Handles

**Problem.** How do you let a parent call a method on a child component (e.g. focus an input inside a custom <TextField>), and when is that appropriate?

**What it tests.** Knowing the escape hatch from declarative data flow — and its guardrails.

**Approach & answer.** Refs don't pass through components by default — a ref on <TextField> would point at the component instance, not its inner <input>. forwardRef lets a component receive a ref and forward it onward. But you usually don't want to expose the raw DOM node; you want a narrow imperative API. useImperativeHandle customises what the ref exposes — you return an object with just the methods the parent should call (focus, scrollIntoView, clear), hiding everything else. This is the sanctioned escape hatch from React's declarative model, for the handful of things that are genuinely imperative: focus, text selection, media playback, scroll, triggering animations. The guardrail: reach for it only when the same result can't be expressed as props/state flowing down. If a parent wants to 'tell' a child something declaratively, that's a prop, not an imperative call. Note that in React 19, ref is passed as a regular prop to function components, so forwardRef is on its way out — but useImperativeHandle stays for shaping the exposed API. Overusing imperative handles recreates the tangled parent-reaches-into-child coupling that declarative React was designed to avoid.

**Use this technique when.** Parent must imperatively trigger focus/scroll/play on a child → forwardRef + useImperativeHandle exposing a narrow API.

```jsx
const TextField = React.forwardRef(function TextField(props, ref) {
  const inputRef = React.useRef(null);
  React.useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
  }), []); // expose only focus + clear, not the raw node
  return <input ref={inputRef} {...props} />;
});

// Parent:
function Form() {
  const field = React.useRef(null);
  return <><TextField ref={field} /><button onClick={() => field.current.focus()}>Focus</button></>;
}
```

**References.** [react.dev · useImperativeHandle](https://react.dev/reference/react/useImperativeHandle) · [react.dev · forwardRef](https://react.dev/reference/react/forwardRef)

---

### 15. React.lazy, Suspense & code splitting  `Medium`

**Pattern:** Code Splitting & Suspense

**Problem.** How do you split a large bundle so a rarely-used route (say, an admin dashboard) doesn't bloat the initial load, and how does Suspense fit in?

**What it tests.** Connecting bundle size to lazy loading and the declarative loading-state model.

**Approach & answer.** Everything imported at the top level ships in the initial bundle, even code the user may never hit. React.lazy defers a component's code to a separate chunk that the bundler (Webpack/Vite) emits and only fetches when the component first renders — `React.lazy(() => import('./AdminDashboard'))`. Because that import is async, React needs something to show while the chunk downloads: <Suspense fallback={...}> wraps the lazy component and declaratively renders the fallback (a spinner/skeleton) until it resolves. This is the same Suspense mechanism that data-fetching libraries and React Server Components hook into — a component 'suspends' by throwing a promise, and the nearest Suspense boundary catches it and shows the fallback until it settles. Place boundaries thoughtfully: too high and one slow chunk blanks a large region; too granular and you get spinner soup. Common wins: split by route, and lazy-load heavy below-the-fold or modal-only components. Pair with an error boundary since a chunk fetch can fail (network), and consider prefetching the chunk on hover/intent so the fallback rarely shows. Note React.lazy needs a default export (or wrap a named export).

**Use this technique when.** A route/feature's code shouldn't load until needed → React.lazy + a Suspense boundary with a fallback.

```jsx
const AdminDashboard = React.lazy(() => import('./AdminDashboard'));

function App() {
  return (
    <React.Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/admin" element={<AdminDashboard />} /> {/* chunk fetched on first visit */}
      </Routes>
    </React.Suspense>
  );
}
```

**References.** [react.dev · lazy](https://react.dev/reference/react/lazy) · [react.dev · Suspense](https://react.dev/reference/react/Suspense)

---

### 16. Compound components & render props  `Medium`

**Pattern:** Component Patterns

**Problem.** You're building a reusable <Tabs> (or <Accordion>) for a design system. Compare the compound-component pattern and render props for sharing state between a parent and its flexible children.

**What it tests.** Choosing a composition API that stays flexible without prop-drilling or leaking internals.

**Approach & answer.** The problem: a parent owns some state (which tab is active) and several children need to read/affect it, but you don't want the consumer to wire every child manually or drill props through markup they control. Two classic patterns. Compound components: the parent (<Tabs>) holds state and shares it with its children (<Tab>, <TabPanel>) implicitly via context, so the consumer writes natural, declarative markup and the pieces coordinate themselves — `<Tabs><Tab/><Tab/><TabPanels>...</TabPanels></Tabs>`. It reads cleanly and lets consumers reorder/wrap children freely; the cost is the implicit context coupling (a <Tab> only works inside <Tabs>). Render props (and its function-as-children variant): the component computes state and calls a function you pass, handing you the values to render however you like — `<Toggle>{({on, toggle}) => ...}</Toggle>`. Maximum flexibility and fully explicit, but nests awkwardly and can cause extra renders. In modern React, custom hooks have absorbed much of what render props/HOCs did for *logic* reuse — but render props still win when the shared thing is *rendering* control, and compound components remain the go-to for cohesive multi-part UI widgets in design systems. HOCs are the older wrapper approach (withRouter), now largely legacy.

**Use this technique when.** Reusable multi-part widget where children coordinate shared state → compound components (context); flexible render control → render props.

```jsx
const TabsContext = React.createContext(null);
function Tabs({ children, defaultIndex = 0 }) {
  const [active, setActive] = React.useState(defaultIndex);
  return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
function Tab({ index, children }) {
  const { active, setActive } = React.useContext(TabsContext);
  return <button aria-selected={active === index} onClick={() => setActive(index)}>{children}</button>;
}
// Consumer: <Tabs><Tab index={0}>One</Tab><Tab index={1}>Two</Tab></Tabs>
```

**References.** [react.dev · Passing Data Deeply with Context](https://react.dev/learn/passing-data-deeply-with-context) · [react.dev · Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks)

---

### 17. Automatic batching & StrictMode double-render  `Medium`

**Pattern:** Rendering Behavior

**Problem.** In React 18, how many re-renders do two setState calls inside a setTimeout trigger, and why does your effect/console.log appear to run twice in development?

**What it tests.** Two React 18 behaviors that surprise people: expanded batching and StrictMode's intentional double-invocation.

**Approach & answer.** Batching: React groups multiple state updates into a single re-render for performance. Before React 18, this only happened inside React event handlers — updates in a setTimeout, promise, or native event handler each triggered their own render. React 18's *automatic batching* extends it everywhere, so two setState calls inside a setTimeout now cause one re-render, not two. (If you ever need to opt out and force a synchronous render between updates, ReactDOM.flushSync wraps the update.) StrictMode double-invocation: in development only (never production), <StrictMode> intentionally double-invokes component function bodies, initializers, and — since React 18 — mounts each component twice (mount, unmount, remount), running your effects setup→cleanup→setup. This is a deliberate stress test that surfaces bugs: impure render logic (a component that renders differently the second time has a side effect it shouldn't), and effects missing cleanup (a subscription that isn't torn down leaks on the simulated remount). The fix is never to defeat it — it's to make render pure and every effect's cleanup exactly reverse its setup. Seeing your log twice in dev is the signal working as intended; if that *causes* a bug (double API call that isn't idempotent), the effect needs cleanup or an abort, not the removal of StrictMode.

**Use this technique when.** Reasoning about how many renders a batch of updates causes, or debugging dev-only double effects → automatic batching + StrictMode.

```jsx
function Example() {
  const [a, setA] = React.useState(0);
  const [b, setB] = React.useState(0);
  function onClick() {
    setTimeout(() => {
      setA(x => x + 1);
      setB(x => x + 1); // React 18: batched → ONE re-render (was two pre-18)
    }, 0);
  }
  return <button onClick={onClick}>{a}/{b}</button>;
}
// <StrictMode> in dev mounts this twice to surface impure renders & missing effect cleanup.
```

**References.** [react.dev · StrictMode](https://react.dev/reference/react/StrictMode) · [react.dev · flushSync](https://react.dev/reference/react-dom/flushSync)

---

### 18. Build a typeahead / autocomplete  `Hard`

**Pattern:** Machine Coding

**Problem.** Build an autocomplete: debounced input, async suggestions, loading/empty states, keyboard nav, no race conditions.

**What it tests.** The classic senior FE machine-coding round — combines debounce, async, a11y, and race handling.

**Approach & answer.** Compose the pieces: (1) debounce the query so you don't fetch on every keystroke; (2) fetch suggestions in an effect keyed on the debounced query, with an ignore flag to drop stale responses; (3) track an activeIndex for ArrowUp/ArrowDown/Enter/Escape keyboard navigation; (4) render loading, empty, and results states; (5) add ARIA roles (combobox/listbox/option, aria-activedescendant) for accessibility. Cache results per query if the same terms recur. This is the reusable, WCAG-compliant component work you did at Domo. The accessibility layer is where senior candidates separate themselves: follow the WAI-ARIA combobox pattern precisely — the input is role=combobox with aria-expanded, aria-controls pointing at the listbox id, and aria-activedescendant pointing at the active option's id (so focus stays in the input while arrow keys move a virtual highlight, which is what screen readers announce). Options are role=option with aria-selected. Beyond a11y and races, production-grade details are: minimum query length before firing, a request cache/in-flight dedupe, cancelling superseded requests with AbortController, clamping/ wrapping the active index, closing on outside-click and blur, and highlighting the matched substring. Structure the component so the data logic (debounce + fetch + cache) lives in a hook and the presentation is a dumb list — that's what makes it reusable across the app.

**Use this technique when.** Search bars, command palettes, tag pickers, address lookups — anywhere users pick from server-driven suggestions.

```jsx
function Autocomplete({ fetchSuggestions }) {
  const [input, setInput] = React.useState('');
  const [items, setItems] = React.useState([]);
  const [active, setActive] = React.useState(-1);
  const [loading, setLoading] = React.useState(false);
  const q = useDebouncedValue(input, 300);

  React.useEffect(() => {
    if (!q) { setItems([]); return; }
    let ignore = false;
    setLoading(true);
    fetchSuggestions(q).then(res => {
      if (!ignore) { setItems(res); setActive(-1); }
    }).finally(() => { if (!ignore) setLoading(false); });
    return () => { ignore = true; };   // drop stale results
  }, [q, fetchSuggestions]);

  function onKeyDown(e) {
    if (e.key === 'ArrowDown') setActive(a => Math.min(a + 1, items.length - 1));
    if (e.key === 'ArrowUp')   setActive(a => Math.max(a - 1, 0));
    if (e.key === 'Enter' && active >= 0) setInput(items[active].label);
    if (e.key === 'Escape') setItems([]);
  }

  return (
    <div role="combobox" aria-expanded={items.length > 0}>
      <input value={input} onChange={e => setInput(e.target.value)}
             onKeyDown={onKeyDown} aria-activedescendant={`opt-${active}`} />
      {loading && <div>Loading…</div>}
      {!loading && q && items.length === 0 && <div>No results</div>}
      <ul role="listbox">
        {items.map((it, i) => (
          <li id={`opt-${i}`} key={it.id} role="option"
              aria-selected={i === active}
              style={{ background: i === active ? '#eef' : '' }}>
            {it.label}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

**References.** [WAI-ARIA APG · Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) · [react.dev · Reacting to input with state](https://react.dev/learn/reacting-to-input-with-state)

---

### 19. Diagnose & fix a slow list  `Hard`

**Pattern:** Performance

**Problem.** A list of 10,000 rows scrolls badly and typing in a filter lags. How do you diagnose and fix it?

**What it tests.** Real performance methodology: measure first, then apply the right fix (virtualization, memo, keys).

**Approach & answer.** Measure first with the React Profiler and browser performance tab — don't guess. Typical fixes, in order of impact: (1) virtualize — render only the visible window (react-window/react-virtualized) so the DOM holds ~20 nodes not 10,000; (2) stable keys — use real ids, never the array index, or React remounts rows on reorder; (3) memoize rows with React.memo and stabilize the row callbacks/data with useCallback/useMemo so unchanged rows skip re-render; (4) debounce the filter input; (5) keep expensive derived data (sorting/filtering) in useMemo. Virtualization is almost always the big win for large lists. The methodology matters as much as the fixes: use the Profiler's flame chart to see WHICH components rendered and why (it flags 'why did this render'), and the browser Performance panel to separate a scripting bottleneck (too many renders / expensive render) from a layout/paint bottleneck (too many DOM nodes, forced reflow). That diagnosis picks the fix — if the DOM is huge, virtualize; if renders are frequent, memoize and stabilize identities; if a single render is slow, move work out of render or into useMemo. Watch for interaction cost too: with 10k rows even the filter's setState can jank, so debounce input and consider useDeferredValue/useTransition to keep typing responsive while the list updates at lower priority. Always re-measure after each change so you're optimizing the real bottleneck, not a guessed one.

**Use this technique when.** Large tables/feeds, data-grid components, anything rendering thousands of nodes — common in BI dashboards.

```jsx
import { FixedSizeList } from 'react-window';

const Row = React.memo(({ index, style, data }) => (
  <div style={style}>{data[index].label}</div>
));

function BigList({ rows }) {          // rows.length === 10000
  return (
    <FixedSizeList height={400} itemCount={rows.length}
      itemSize={32} width="100%" itemData={rows}>
      {Row}
    </FixedSizeList>   // only ~visible rows are in the DOM
  );
}
```

**References.** [react.dev · Rendering Lists (keys)](https://react.dev/learn/rendering-lists) · [react.dev · useDeferredValue](https://react.dev/reference/react/useDeferredValue)

---

### 20. useTransition & useDeferredValue  `Hard`

**Pattern:** Concurrent Rendering

**Problem.** A search input filters a large list and typing feels laggy. How do React 18's concurrent features fix this, and what's the difference between useTransition and useDeferredValue?

**What it tests.** Understanding priority-based rendering: keeping urgent updates responsive while deprioritising expensive ones.

**Approach & answer.** The lag comes from one render doing two things at once: updating the input (urgent — the user must see their keystroke immediately) and re-rendering a huge filtered list (expensive — can lag behind). Before concurrent rendering, both happened in one synchronous, non-interruptible pass, so the keystroke waited on the list. Concurrent rendering lets React mark the expensive update as low-priority and *interruptible* — it can pause the list render to process the next keystroke, then resume or restart. useTransition gives you `[isPending, startTransition]`: wrap the state update that triggers the expensive render in startTransition, and React keeps the input responsive while rendering the list in the background; isPending lets you show a subtle spinner. useDeferredValue is the same idea from the consumer side: you pass a value and get back a version that 'lags behind' during urgent updates — useful when you don't own the state setter (e.g. a value from props or context). Rule of thumb: useTransition when you control the update that causes the work; useDeferredValue when you only have the value. Neither makes the render faster — they make it *non-blocking*, so perceived responsiveness improves. Still memoize the expensive list (React.memo) so deferring actually skips work.

**Use this technique when.** An urgent update (typing) is blocked by an expensive re-render → mark the expensive update low-priority via startTransition / useDeferredValue.

```jsx
function Search({ items }) {
  const [query, setQuery] = React.useState('');
  const deferredQuery = React.useDeferredValue(query); // lags during typing
  const results = React.useMemo(
    () => items.filter(i => i.includes(deferredQuery)),
    [items, deferredQuery]
  );
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} /> {/* stays responsive */}
      <List rows={results} />
    </>
  );
}
```

**References.** [react.dev · useTransition](https://react.dev/reference/react/useTransition) · [react.dev · useDeferredValue](https://react.dev/reference/react/useDeferredValue)

---

### 21. Subscribing to external stores (useSyncExternalStore)  `Hard`

**Pattern:** External Stores

**Problem.** You need a component to re-render when a non-React store changes (a Redux-like store, a browser API like navigator.onLine, or a custom event emitter). Why not just useState + useEffect, and what does useSyncExternalStore solve?

**What it tests.** Awareness of tearing under concurrent rendering and the correct subscription primitive.

**Approach & answer.** The naive approach — subscribe in useEffect and copy the store value into local state — has two problems. First, there's a gap: the effect runs after render, so between the initial render and the subscription the store could change and you'd miss it. Second, and the real reason the hook exists: under concurrent rendering React can pause and resume renders, and different components (or different parts of one render) could read *different* values from a mutable external store mid-render — the UI 'tears', showing inconsistent data. useSyncExternalStore is the official primitive for subscribing to external mutable stores safely. You give it three things: a `subscribe(callback)` that registers a listener and returns an unsubscribe, a `getSnapshot()` that returns the current value, and (for SSR) a `getServerSnapshot()`. React uses it to read a consistent snapshot and to force a synchronous re-render when the store changes, avoiding tearing. This is what Redux, Zustand, and Jotai use internally. The snapshot must be referentially stable when unchanged (return the same object, don't build a new one each call) or you'll loop. For simple cases (online status, media queries, window size) it's cleaner than effect-based subscription and correct under concurrency.

**Use this technique when.** Re-render on changes to a store outside React (Redux-like, browser API, event emitter) → useSyncExternalStore, not useEffect copying.

```jsx
function useOnlineStatus() {
  return React.useSyncExternalStore(
    (callback) => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    () => navigator.onLine,   // client snapshot
    () => true                // server snapshot (SSR)
  );
}
```

**References.** [react.dev · useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)

---

### 22. Server Components vs Client Components  `Hard`

**Pattern:** Server Components

**Problem.** What are React Server Components (RSC), how do they differ from Client Components and from traditional SSR, and what are the rules for mixing them?

**What it tests.** Grasping the newest architectural shift: where a component runs and what that lets it do or forbids.

**Approach & answer.** Traditional SSR renders your whole app to an HTML string on the server, ships it, then *hydrates* — the same component code also runs on the client and all its JS is shipped. Server Components are a different axis: components that run *only* on the server and never ship their JS to the browser at all. Their rendered output is serialized and streamed to the client, which merges it into the tree. Benefits: zero bundle cost for server-only code, direct access to server resources (read a file, hit the database, use secrets) right inside the component with async/await, and less client JS overall. The constraints follow from 'this never runs in the browser': Server Components can't use state, effects, or event handlers (no useState/useEffect/onClick) and can't use browser-only APIs. Anything interactive must be a Client Component, marked with the `'use client'` directive at the top of the file, which opts that module (and its imports) into the client bundle. Composition rules: Server Components can import and render Client Components, but not vice versa — instead you pass Server Components *into* Client Components as children/props (a Client Component can render `{children}` that were produced on the server). Props crossing the boundary must be serializable (no functions). This is the model frameworks like Next.js App Router are built on; RSC and SSR are complementary, not competing.

**Use this technique when.** Static/data-heavy UI with server resource access and no interactivity → Server Component; anything with state/effects/handlers → Client Component ('use client').

```jsx
// app/page.jsx — Server Component (default, no directive). Runs on server only.
async function Page() {
  const posts = await db.query('SELECT * FROM posts'); // direct DB access, no API layer
  return <Feed posts={posts} />; // Feed can be a Client Component receiving serializable props
}

// Feed.jsx
'use client';                     // opts this module into the client bundle
export default function Feed({ posts }) {
  const [open, setOpen] = React.useState(false); // state is allowed here
  return <button onClick={() => setOpen(o => !o)}>{posts.length} posts</button>;
}
```

**References.** [react.dev · Server Components](https://react.dev/reference/rsc/server-components) · [react.dev · 'use client' directive](https://react.dev/reference/rsc/use-client)

---

### 23. React 19 Actions & form hooks  `Hard`

**Pattern:** React 19 / Actions

**Problem.** What are Actions in React 19? Explain useActionState, useFormStatus, useOptimistic, and the use() hook, and the problem they collectively solve.

**What it tests.** Whether you know React 19's built-in async/form primitives that replace hand-rolled pending/error/optimistic plumbing.

**Approach & answer.** The problem: for years, submitting a form meant hand-wiring the same boilerplate — an isPending state, a try/catch to capture errors, a manual reset, and often an optimistic update with manual rollback. React 19 folds this into 'Actions': an async function you hand to React (via a form's action prop or a transition) that React manages, automatically tracking pending state, errors, and sequencing. Four primitives sit on top. (1) useActionState(fn, initialState) returns [state, dispatch, isPending]: you pass an async action that receives the previous state (and FormData when used as a form action) and returns the next state; React gives you isPending for free and serializes concurrent submissions. (2) useFormStatus() reads the pending status of the nearest ANCESTOR form without any prop-drilling — so a deeply nested SubmitButton can disable itself while the parent form submits, which is the whole reason it exists. (3) useOptimistic(actualValue, updateFn) returns an optimistic value you can set instantly on submit; React shows it immediately and AUTOMATICALLY reverts to the real value when the action settles — no manual rollback. (4) use(promise) unwraps a promise (or context) DURING render: it suspends until the promise resolves and integrates with Suspense; unlike hooks, use() may be called conditionally and inside loops. Together they compose with Server Components and server actions: a <form action={serverAction}> works with progressive enhancement, and the client hooks layer pending/optimistic UI on top. The mental shift is declarative async: you describe the action and the optimistic result, and React owns the pending/error/reset lifecycle.

**Use this technique when.** Form submission with pending/error handling, optimistic UI without manual rollback, or reading form-pending state in a nested button.

```jsx
function NameForm() {
  const [error, submitAction, isPending] = React.useActionState(
    async (prevState, formData) => {
      const res = await save(formData.get('name'));   // the Action
      if (!res.ok) return 'Could not save';            // becomes the next state
      return null;
    },
    null                                               // initial state
  );
  return (
    <form action={submitAction}>
      <input name="name" />
      <SubmitButton />
      {error && <p role="alert">{error}</p>}
      {isPending && <p>Saving…</p>}
    </form>
  );
}

// Reads the PARENT <form>'s pending state — no props threaded down.
function SubmitButton() {
  const { pending } = ReactDOM.useFormStatus();
  return <button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>;
}
```

**References.** [react.dev · useActionState](https://react.dev/reference/react/useActionState) · [react.dev · useOptimistic](https://react.dev/reference/react/useOptimistic)

---

### 24. SSR hydration & hydration mismatches  `Hard`

**Pattern:** SSR / Hydration

**Problem.** What is hydration in SSR, why do 'hydration mismatch' errors happen, and how do you fix them?

**What it tests.** Understanding the two-pass render contract of SSR and the deterministic-first-render rule that keeps it intact.

**Approach & answer.** In server-side rendering the server runs your components once and emits inert HTML — real markup the browser paints immediately (fast first paint, SEO-friendly), but with no event handlers or state attached. Hydration is the client's second pass: React runs the SAME components again in the browser and, instead of recreating the DOM, walks the existing server HTML and attaches handlers and state to it (via hydrateRoot). The contract is strict: the client's FIRST render must produce markup identical to what the server sent. A 'hydration mismatch' is React discovering the trees disagree. Common causes are all forms of non-determinism between the two environments: Date.now()/new Date()/Math.random() (different values each run), locale/timezone/number formatting that differs server vs client, reading browser-only globals during render (window, localStorage, navigator, matchMedia — undefined on the server so the branch differs), and invalid HTML nesting the browser 'fixes' (a <div> inside a <p>) so the DOM no longer matches. The fixes follow from 'make the first client render match the server': (1) the two-pass pattern — render the deterministic/server version first, then read browser-only values in useEffect and set state, causing a SECOND render that safely diverges post-hydration; (2) gate browser-only UI behind a mounted flag (useState(false) → true in an effect) so it renders nothing until after hydration; (3) use useId() for ids that must be stable and matching across server and client instead of a random/counter id; (4) suppressHydrationWarning as a targeted last resort for genuinely unavoidable diffs like a timestamp. The anti-pattern is branching on window during render — always defer that to an effect.

**Use this technique when.** Diagnosing hydration-mismatch warnings, rendering browser-only or time/locale-dependent UI under SSR, and generating SSR-safe ids.

```jsx
// BAD: reads window during render — server has no window, so the
// first client render diverges from server HTML => hydration mismatch.
function BadWidth() {
  return <span>{window.innerWidth}px</span>;
}

// GOOD: deterministic first render (matches server), then a second
// render after mount reads the browser value safely.
function GoodWidth() {
  const [width, setWidth] = React.useState(null);   // same on server & first client render
  React.useEffect(() => setWidth(window.innerWidth), []);
  return <span>{width == null ? '…' : width + 'px'}</span>;
}

// SSR-safe id: stable and identical across server and client.
function Field() {
  const id = React.useId();
  return (<><label htmlFor={id}>Email</label><input id={id} /></>);
}
```

**References.** [react.dev · hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot) · [react.dev · useId](https://react.dev/reference/react/useId)

---

### 25. Reconciliation & the diffing algorithm  `Hard`

**Pattern:** Reconciliation

**Problem.** How does React's reconciliation (diffing) algorithm decide what to update? Why is it O(n) instead of O(n³), and what role do keys play?

**What it tests.** The two heuristics behind diffing and why element type and keys determine reuse-vs-remount.

**Approach & answer.** A general tree-diff (find the minimal set of edits between two trees) is O(n³) — far too slow for a UI. React makes it O(n) with two heuristics that trade theoretical minimality for speed. HEURISTIC 1 — element TYPE: React compares elements at the same position. If the type differs (a <div> became a <span>, or ComponentA became ComponentB), React does NOT try to diff their subtrees — it unmounts the old tree entirely (destroying its DOM and state) and builds the new one from scratch. If the type is the SAME, React keeps the DOM node, patches only the changed attributes/props, and recurses into children. This is why a conditional that swaps component type resets all state below it, and why deliberately CHANGING a key (or type) is the idiomatic way to force a remount/reset. HEURISTIC 2 — KEYS for lists: within a set of siblings, React needs to know which child is which across renders. Without keys it matches by index, so inserting or reordering makes every position 'change type-compatibly' and React patches the wrong nodes — state and DOM attach to the wrong item, causing the classic 'input value stuck on the wrong row' bug. A stable, unique key (a data id, not the array index) lets React match a child to its previous instance regardless of position, so it moves DOM nodes instead of rebuilding them. Index-as-key is only safe for a static list that never reorders, inserts, or deletes. Note keys must be unique among siblings, not globally. Fiber (React's architecture) splits this work into interruptible units so long renders don't block the main thread, but the MATCHING rules above are unchanged — Fiber changes WHEN the work happens, not WHAT counts as a match.

**Use this technique when.** Explaining why state resets on a type/key change, why index keys corrupt reorderable lists, and how to force a remount with key.

**Complexity.** General tree diff is O(n³); React's two heuristics make reconciliation O(n).

```jsx
// (1) TYPE change => unmount + rebuild subtree (state below is lost).
{editing ? <input defaultValue={name} /> : <span>{name}</span>}

// (2) Keys identify list children across renders.
todos.map(t => <Row key={t.id} todo={t} />);   // ✅ stable id: correct moves
todos.map((t, i) => <Row key={i} todo={t} />);  // ❌ index: breaks on reorder/insert

// (3) A changing key is the deliberate way to RESET a component.
<Profile key={userId} userId={userId} />;       // new userId => fresh state
```

**References.** [react.dev · Preserving and Resetting State](https://react.dev/learn/preserving-and-resetting-state) · [react.dev · Rendering Lists (keys)](https://react.dev/learn/rendering-lists)

---

## System Design

> Use the RADIO framework: Requirements (functional + non-functional, scope it), Architecture (component tree + responsibilities), Data model (what state lives where — server, client, URL, local), Interface (component props + API contracts), Optimizations (performance, a11y, network, error/empty/loading states). Frontend system design rewards breadth then a deep-dive on one hard part.

### 1. The RADIO framework itself  `Easy`

**Pattern:** RADIO Framework

**Problem.** You're asked to 'design a component/feature'. What structure do you use to answer?

**What it tests.** Whether you drive the interview with a repeatable structure instead of jumping straight to code.

**Approach & answer.** RADIO: Requirements — clarify scope, users, functional + non-functional needs (a11y, i18n, perf, devices); state assumptions. Architecture — break into components and data flow; draw the boxes. Data model — what state exists, who owns it (server cache vs local UI state), shape of it. Interface/API — component props/events AND the network API (endpoints, pagination, payloads). Optimizations — performance (virtualization, caching, code-split), accessibility, error/empty/loading states, edge cases. Spend the most time on Requirements and the part the interviewer probes. Why it works: front-end system design is deliberately open-ended, so a repeatable structure stops you rat-holing on one detail and signals seniority — you're driving the interview, not reacting to it. Announce the five letters up front, then timebox: spend the first chunk clarifying requirements, sketch the architecture quickly, then dive into whichever axis the interviewer leans on (usually the data model or optimizations). Distinguish functional needs (what it must do) from non-functional ones (a11y, performance budgets, i18n, offline, security) — naming the non-functional axes unprompted is a strong senior signal. State assumptions out loud so scope gets corrected early, before you design the wrong thing.

**Use this technique when.** EVERY frontend system-design prompt. Announce the framework up front so the interviewer can steer you.

```text
R  Requirements   — scope, users, functional + non-functional (a11y, perf, i18n)
A  Architecture    — component breakdown + data flow diagram
D  Data model      — state shape, ownership (server cache vs UI state)
I  Interface       — component props/events + network API (endpoints, pagination)
O  Optimizations   — perf, a11y, error/empty/loading, edge cases
```

**References.** [GreatFrontEnd · Front End System Design (RADIO)](https://www.greatfrontend.com/system-design) · [MDN · Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)

---

### 2. Design an optimistic like/save button  `Easy`

**Pattern:** Optimistic UI

**Problem.** Design a like (or save/follow) button that feels instant. Apply RADIO; cover rollback and rapid clicks.

**What it tests.** Optimistic updates, exact rollback, request dedup, and idempotency for high-frequency micro-interactions.

**Approach & answer.** Requirements: a toggle that feels instant, ends in the correct state, survives failure and rapid clicks, and is accessible. Architecture: on click, immediately update local UI state (optimistic), fire the request in the background, and reconcile when it resolves — on error, roll back to the previous value and surface a subtle notice. Data model: keep the displayed value plus the last server-confirmed value so rollback is exact; track an in-flight flag per item. Interface: POST /like { id, liked } returning the authoritative state/count. Optimizations: dedup or cancel concurrent requests for the same item; debounce rapid toggles or send only the final intent; keep the control clickable but reflect pending state; make it an accessible toggle (aria-pressed). Why optimistic: perceived latency dominates UX for micro-interactions (like, star, follow) — waiting a round-trip makes them feel broken. The correctness trap is reconciliation: store the pre-update value so a failed request restores exactly it, not a guessed value, and handle the race where the user toggles twice before the first response returns (sequence responses, or cancel superseded requests). Prefer sending the desired END-STATE rather than a delta so retries are idempotent. Roll back visibly but gently — revert the icon and show a toast — so the user knows it didn't persist.

**Use this technique when.** Like/star/follow/save toggles, and any high-frequency action where a round-trip wait would feel broken.

```jsx
function LikeButton({ id, liked, count, save }) {
  const [state, setState] = React.useState({ liked, count });

  async function toggle() {
    const prev = state;                                  // remember for rollback
    const next = { liked: !prev.liked,
                   count: prev.count + (prev.liked ? -1 : 1) };
    setState(next);                                      // optimistic
    try {
      const server = await save(id, next.liked);         // send end-state (idempotent)
      setState({ liked: server.liked, count: server.count });
    } catch {
      setState(prev);                                    // rollback to exact prior value
    }
  }

  return (
    <button aria-pressed={state.liked} onClick={toggle}>
      ♥ {state.count}
    </button>
  );
}
```

**References.** [TanStack Query · Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates) · [WAI-ARIA APG · Button (aria-pressed toggle)](https://www.w3.org/WAI/ARIA/apg/patterns/button/)

---

### 3. Design an in-app toast / notification system  `Easy`

**Pattern:** Notification Queue

**Problem.** Design a toast system any component can trigger. Apply RADIO; cover stacking, auto-dismiss, and a11y.

**What it tests.** Global cross-cutting UI, timer management, and accessible announcements via live regions.

**Approach & answer.** Requirements: transient messages (success/error/info) that stack, auto-dismiss, don't block the UI, are pausable, and are announced to screen readers. Architecture: a single ToastProvider owns a queue in context and exposes add(); a fixed-position container renders the list; a useToast() hook lets any component enqueue without prop-drilling. Data model: an array of { id, type, message, duration }; each toast owns a timer. Interface: toast.success('Saved'), toast.error(msg) — a tiny imperative API over the queue. Optimizations & a11y: render the container in an aria-live region (polite for info, assertive for errors) so messages are announced; pause auto-dismiss on hover/focus and resume on leave (someone reading a message shouldn't lose it); cap how many show and coalesce or drop the oldest to avoid a wall of toasts; give each an accessible close button; animate enter/exit but respect prefers-reduced-motion. Why a queue in one provider: toasts are global UI, so colocating them avoids z-index/stacking wars and lets any component fire one. The timer detail people miss: clear the timeout on unmount and on manual dismiss, and when pausing, store the REMAINING time so resume doesn't restart the full duration. Keep the provider render-cheap (split state from the dispatch API / memoize the context value) so a new toast doesn't re-render the whole app.

**Use this technique when.** Toasts, snackbars, inline alerts — any transient global feedback multiple components must trigger.

```jsx
const ToastCtx = React.createContext(null);

function ToastProvider({ children }) {
  const [toasts, setToasts] = React.useState([]);
  const remove = React.useCallback(
    (id) => setToasts((t) => t.filter((x) => x.id !== id)), []);
  const add = React.useCallback((message, type = 'info', duration = 4000) => {
    const id = crypto.randomUUID();
    setToasts((t) => [...t, { id, message, type }]);
    setTimeout(() => remove(id), duration);              // auto-dismiss
  }, [remove]);

  return (
    <ToastCtx.Provider value={add}>
      {children}
      <div className="toasts" role="region" aria-live="polite">
        {toasts.map((t) => (
          <div key={t.id} role="status">
            {t.message}
            <button onClick={() => remove(t.id)} aria-label="Dismiss">×</button>
          </div>
        ))}
      </div>
    </ToastCtx.Provider>
  );
}
const useToast = () => React.useContext(ToastCtx);
```

**References.** [MDN · ARIA live regions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) · [WAI-ARIA APG · Alert Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alert/)

---

### 4. Design light/dark theme switching  `Easy`

**Pattern:** Theming

**Problem.** Design theme switching (light/dark/system). Apply RADIO; cover FOUC, persistence, and prefers-color-scheme.

**What it tests.** Design-token strategy, avoiding the flash of wrong theme, and honoring the OS preference.

**Approach & answer.** Requirements: light/dark (and 'system') themes, no flash of the wrong theme on load (FOUC), persistence across visits, respect for the OS preference, and runtime switching. Architecture: express all colors as CSS custom properties (design tokens) scoped under a root attribute — [data-theme='dark'] { --bg: …; --fg: … } — so switching is a single attribute flip, not a re-render of styled components. Data model: a resolved theme ('light'|'dark') plus a mode ('light'|'dark'|'system'); persist the mode in localStorage. Interface: setTheme('dark'); a toggle in the UI. Optimizations: the critical trick is a tiny BLOCKING inline script in <head> that reads localStorage and sets data-theme BEFORE first paint — otherwise the page paints the default theme, then flips (FOUC). Honor prefers-color-scheme via matchMedia when the mode is 'system', and listen for changes so the app follows the OS live. Set color-scheme on the root so native controls (scrollbars, form widgets) match. Why CSS variables over a JS theme object: the browser recomputes cascaded values on one attribute change with zero React re-render, so theming is instant and also works for non-React CSS. Accessibility: keep both themes above WCAG contrast, and don't override the user's OS setting without an explicit choice — persist an explicit choice, but fall back to system when they haven't chosen.

**Use this technique when.** Any dark-mode / white-label theming; deciding tokens vs a JS theme object; killing the theme flash on load.

```js
// 1) Blocking script in <head> — runs before first paint, avoids FOUC:
(function () {
  var saved = localStorage.getItem('theme');            // 'light' | 'dark' | null
  var sys = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.dataset.theme = saved || sys;
})();

// 2) Runtime toggle:
function setTheme(mode) {                                 // 'light' | 'dark'
  document.documentElement.dataset.theme = mode;
  localStorage.setItem('theme', mode);
}

// 3) Follow the OS live when the user hasn't chosen:
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (!localStorage.getItem('theme'))
    document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
});

/* CSS:  :root { --bg:#fff; --fg:#111 }
         [data-theme="dark"] { --bg:#111; --fg:#eee; color-scheme:dark } */
```

**References.** [MDN · color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) · [MDN · prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)

---

### 5. Design a reusable component (e.g. Modal / Button)  `Medium`

**Pattern:** Reusable Component

**Problem.** Design a reusable Modal for a shared component library used by multiple teams. Apply RADIO.

**What it tests.** API design, accessibility, and composability — directly your Domo component-library work.

**Approach & answer.** Requirements: any content, controlled open/close, closes on Esc/backdrop, focus-trapped, accessible, themeable. Architecture: a Portal renders the modal at the document root (escapes overflow/z-index); Backdrop + Dialog + optional Header/Body/Footer via composition (children/slots), not a hundred boolean props. Data model: `isOpen` owned by the parent (controlled). Interface: props isOpen, onClose, children, ariaLabel, size; use composition for content. Optimizations/a11y: role=dialog + aria-modal, focus trap, restore focus to the trigger on close, lock body scroll, render nothing when closed, Esc + backdrop-click to close. Composition over configuration is the key senior signal for library work. Why a Portal: rendering into document.body escapes ancestor overflow:hidden, transform, and z-index stacking contexts that would otherwise clip or mis-layer the dialog. The accessibility details that separate a real answer from a toy: role=dialog + aria-modal=true, initial focus moved into the dialog, a focus trap that cycles Tab within it, and focus restored to the triggering element on close (save document.activeElement before opening). Prefer a controlled API (parent owns `isOpen`) so the modal composes with routing, forms, and confirmation flows; expose content via children/slots (Header/Body/Footer) rather than a title/body/footer/showClose prop explosion. Render nothing when closed to keep the tree light, and lock body scroll while open so the background doesn't scroll under the overlay.

**Use this technique when.** Any 'design a <widget>' prompt (dropdown, tooltip, tabs, date-picker) for a design system.

```jsx
// Composition over configuration + a Portal + a11y
function Modal({ isOpen, onClose, children, ariaLabel }) {
  React.useEffect(() => {
    if (!isOpen) return;
    const onKey = (e) => e.key === 'Escape' && onClose();
    document.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';        // lock scroll
    return () => {
      document.removeEventListener('keydown', onKey);
      document.body.style.overflow = '';
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;
  return ReactDOM.createPortal(
    <div className="backdrop" onClick={onClose}>
      <div role="dialog" aria-modal="true" aria-label={ariaLabel}
           onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  );
}
```

**References.** [react.dev · createPortal](https://react.dev/reference/react-dom/createPortal) · [WAI-ARIA APG · Dialog (Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)

---

### 6. Design an infinite-scroll feed  `Medium`

**Pattern:** Infinite Scroll / Feed

**Problem.** Design a news/social feed with infinite scroll. Apply RADIO; call out pagination and performance.

**What it tests.** Pagination strategy, list performance, and loading/error UX at scale.

**Approach & answer.** Requirements: endless list, fast scroll, resilient to slow network, accessible. Architecture: a Feed container fetches pages and renders a virtualized list of Post components; an IntersectionObserver sentinel near the bottom triggers the next page. Data model: items array + cursor/nextPageToken + isLoading/hasMore/error. Interface: cursor-based pagination (GET /feed?cursor=…&limit=20) — prefer cursors over offset (stable under inserts, no skipped/duplicated items). Optimizations: virtualize the list so the DOM stays small; cache pages; show skeletons while loading; handle error with a retry; debounce/guard so you don't fire duplicate page requests; preserve scroll position on back-navigation. Why IntersectionObserver over a scroll listener: it fires off the main thread and avoids the jank of high-frequency scroll + getBoundingClientRect measurement — place a sentinel div after the last item and load when it intersects. Cursor vs offset: offset pagination (?page=3) skips or duplicates rows when items are inserted or deleted between fetches; an opaque cursor/nextPageToken points at a stable position, so the feed stays consistent under live writes. Virtualization matters because a feed can grow to thousands of nodes — windowing (react-window / react-virtualized) keeps only the visible rows in the DOM so scrolling stays smooth and memory stays flat. Guard against duplicate in-flight requests with loading/hasMore flags, prefer skeletons over spinners to reduce layout shift, and always give errors a retry affordance.

**Use this technique when.** Feeds, search results, chat history, any long server-paginated list.

```jsx
function Feed({ fetchPage }) {
  const [items, setItems] = React.useState([]);
  const [cursor, setCursor] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [hasMore, setHasMore] = React.useState(true);
  const sentinel = React.useRef(null);

  const loadMore = React.useCallback(async () => {
    if (loading || !hasMore) return;           // guard duplicate requests
    setLoading(true);
    const { data, nextCursor } = await fetchPage(cursor);
    setItems(prev => [...prev, ...data]);
    setCursor(nextCursor);
    setHasMore(Boolean(nextCursor));
    setLoading(false);
  }, [cursor, loading, hasMore, fetchPage]);

  React.useEffect(() => {
    const io = new IntersectionObserver(
      ([e]) => e.isIntersecting && loadMore()
    );
    if (sentinel.current) io.observe(sentinel.current);
    return () => io.disconnect();
  }, [loadMore]);

  return (<>{items.map(p => <Post key={p.id} {...p} />)}
    <div ref={sentinel}>{loading ? 'Loading…' : ''}</div></>);
}
```

**References.** [MDN · Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) · [web.dev · Virtualize large lists](https://web.dev/articles/virtualize-long-lists-react-window)

---

### 7. Design a data table (sort, filter, pagination)  `Medium`

**Pattern:** RADIO

**Problem.** Design a reusable data-table component that supports column sorting, filtering, and pagination over large datasets.

**What it tests.** API design for a flexible component, server- vs client-side data strategy, and accessibility.

**Approach & answer.** Requirements: columns with per-column sort and filter, pagination, potentially tens of thousands of rows, reusable across teams, keyboard-and-screen-reader accessible. Architecture — separate three concerns: (1) a headless data layer that owns query state {sortBy, sortDir, filters, page, pageSize} and returns rows; (2) presentational cells/headers driven by a column config array ({ key, header, render, sortable, filterable }) so consumers declare columns as data rather than JSX; (3) the shell that wires them. Data strategy is the key fork: for small datasets do sort/filter/paginate on the client (instant, no network); past a few thousand rows push all three to the server (the query state becomes URL/request params) so you never ship the whole set — and debounce filter input to avoid a request per keystroke. Keep query state in the URL so a filtered/sorted view is shareable and survives reload. Performance: virtualize rows (windowing) when rendering large pages, memoize the column config and row renderers, and use stable keys. Accessibility is non-negotiable and frequently the differentiator: use a real <table> with <th scope='col'>, put aria-sort on the sorted header, make sort controls real buttons, and announce result counts via a live region. Edge cases: empty state, loading skeletons, error state, sticky header, and column resize.

**Use this technique when.** Any 'design a table/grid/list-with-controls' prompt; deciding client vs server data handling; building a design-system table.

**Complexity.** Client sort O(n log n); server mode shifts cost off the client. Virtualize to bound DOM nodes.

```jsx
// Columns as data -> the table is generic and declarative.
const columns = [
  { key: 'name',  header: 'Name',  sortable: true,  filterable: true },
  { key: 'email', header: 'Email', sortable: false, render: r => <a href={'mailto:' + r.email}>{r.email}</a> },
];

// Single query-state object; server mode sends it as params.
const [query, setQuery] = React.useState({ sortBy: 'name', sortDir: 'asc', filters: {}, page: 1, pageSize: 25 });

// Accessible header: <th aria-sort="ascending"><button onClick={toggleSort}>Name</button></th>
```

**References.** [WAI-ARIA APG · Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) · [MDN · aria-sort](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort)

---

### 8. Design an autocomplete / typeahead  `Medium`

**Pattern:** Autocomplete / Typeahead

**Problem.** Design a search autocomplete. Apply RADIO; call out debouncing, race handling, caching, and a11y.

**What it tests.** Debounce, request cancellation, out-of-order response handling, caching, and the combobox a11y pattern.

**Approach & answer.** Requirements: as-you-type suggestions that are fast and CURRENT (no stale result overwriting a newer one), keyboard-navigable, accessible, and resilient to a slow API. Architecture: the input drives a debounced query; each query cancels the previous in-flight request; results render in a listbox the input controls. Data model: { query, results, activeIndex, loading } plus a cache mapping query→results. Interface: GET /suggest?q=… returning ranked items. Optimizations: DEBOUNCE input (~150–300ms) so you fetch per pause, not per keystroke; CANCEL the superseded request (AbortController) AND guard against out-of-order responses by ignoring any response that isn't for the latest query — this race is the #1 autocomplete bug (an earlier, slower response clobbers a newer one); CACHE by query string (reuse prefixes) so backtracking is instant; cap and rank results; skip requests for empty or trivially short input. Accessibility is a spec, not a nicety — the combobox pattern: role=combobox on the input, aria-expanded, aria-activedescendant pointing at the highlighted option, ↑/↓ to move, Enter to select, Esc to close, each option role=option. Show loading/empty/error states. Why cancel AND a latest-query guard together: cancellation frees the network, but a cancel can land after a response has already started, so you still must gate on 'is this the current query?' — sequencing by the query value (or a request id) is what actually prevents flicker.

**Use this technique when.** Search boxes, @mention pickers, address/command palettes — any type-to-search with a network backend.

```jsx
function Autocomplete({ search }) {
  const [q, setQ] = React.useState('');
  const [results, setResults] = React.useState([]);
  const latest = React.useRef(0);

  React.useEffect(() => {
    if (!q) { setResults([]); return; }
    const id = ++latest.current;                    // sequence this request
    const ctrl = new AbortController();
    const t = setTimeout(async () => {              // debounce
      const data = await search(q, ctrl.signal);
      if (id === latest.current) setResults(data);  // ignore stale responses
    }, 200);
    return () => { clearTimeout(t); ctrl.abort(); };
  }, [q, search]);

  return (
    <div>
      <input role="combobox" aria-expanded={results.length > 0}
             value={q} onChange={(e) => setQ(e.target.value)} />
      <ul role="listbox">
        {results.map((r) => <li role="option" key={r.id}>{r.label}</li>)}
      </ul>
    </div>
  );
}
```

**References.** [WAI-ARIA APG · Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) · [MDN · AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)

---

### 9. Design a client-side SPA router  `Medium`

**Pattern:** Client-side Router

**Problem.** Design a client-side router for a single-page app. Apply RADIO; cover the History API, lazy routes, and scroll.

**What it tests.** History API usage, path matching, code-splitting routes, scroll restoration, and deep-link handling.

**Approach & answer.** Requirements: client-side navigation with no full reloads, deep-linkable URLs, back/forward support, lazy-loaded routes, scroll restoration, and per-route data loading. Architecture: a Router listens to history changes and matches the current path against a route table to pick a component; Links call history.pushState instead of navigating; a popstate listener re-renders on back/forward. Data model: the route table [{ path, component, loader }] plus current location; params parsed from the path. Interface: <Link to>, useParams(), useNavigate(). Optimizations: use the History API (pushState/replaceState + popstate) — intercept Link clicks, preventDefault, push the URL, swap the view; CODE-SPLIT routes with dynamic import() so each route's bundle loads on demand (a big first-load win); prefetch a route's chunk on link hover/focus; restore scroll to top on push but preserve position on back (save scrollY per history entry); handle a not-found route and nested/relative routes. Path matching: convert patterns like /users/:id into a regex capturing params, matching most-specific first. Why History API over hash routing: real paths (/users/1) are clean, server-renderable, and SEO-friendly — hash routing (#/users/1) is a fallback for static hosts that can't rewrite. Critically, the server must rewrite all unknown paths to index.html so a deep link or refresh doesn't 404. Accessibility: move focus to the new view's heading on navigation and announce route changes so screen-reader users aren't stranded after a silent DOM swap.

**Use this technique when.** Any SPA that needs deep links and back/forward without reloads; reasoning about history, lazy routes, and the index.html rewrite.

```jsx
function Router({ routes }) {
  const [path, setPath] = React.useState(location.pathname);
  React.useEffect(() => {
    const onPop = () => setPath(location.pathname);
    window.addEventListener('popstate', onPop);       // back/forward
    return () => window.removeEventListener('popstate', onPop);
  }, []);

  const match = routes.find((r) => matchPath(r.path, path));
  const Comp = match ? match.component : NotFound;
  return <Comp params={match ? match.params : {}} />;
}

function Link({ to, children }) {
  return (
    <a href={to} onClick={(e) => {
      e.preventDefault();
      history.pushState({}, '', to);                  // no reload
      dispatchEvent(new PopStateEvent('popstate'));   // trigger re-render
    }}>{children}</a>
  );
}
// Lazy route: { path: '/settings', component: React.lazy(() => import('./Settings')) }
```

**References.** [MDN · History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) · [web.dev · Reduce JS payloads with code-splitting](https://web.dev/articles/reduce-javascript-payloads-with-code-splitting)

---

### 10. Design a real-time BI dashboard  `Hard`

**Pattern:** Real-time Dashboard

**Problem.** Design a real-time analytics dashboard with live-updating D3 charts (à la Domo). Apply RADIO.

**What it tests.** Real-time transport choice, render performance with live data, and D3+React integration — your exact domain.

**Approach & answer.** Requirements: many widgets, live updates, large datasets, responsive, accessible, configurable layout. Architecture: a Dashboard grid of independent Widget components; a single data layer subscribes to updates and fans them out via a store (selectors so only affected widgets re-render). Transport: WebSocket for true push / high-frequency; SSE for one-way streams; polling as the simple fallback — pick per update frequency. Data model: normalized cache keyed by widget/metric + last-updated timestamps; throttle incoming ticks. D3+React: let React own the DOM/SVG structure and D3 own the math (scales, axes, layouts) — don't let both mutate the DOM. Optimizations: virtualize/lazy-render off-screen widgets, throttle/batch updates (rAF), memoize scales, downsample dense series, code-split heavy chart bundles, show per-widget loading/error, and keep charts accessible (labels, data tables as fallback). Transport tradeoffs in depth: polling is trivial but wastes requests and adds latency; SSE (EventSource) is a simple one-way server→client stream with built-in auto-reconnect, ideal for tickers; WebSocket is full-duplex for high-frequency or bidirectional needs but you own reconnection/backoff and heartbeats. The performance trap is fan-out — a naïve context holding all widget data re-renders every widget on every tick; instead normalize into a store and subscribe each widget to only its slice (selectors / useSyncExternalStore) so one metric update repaints one chart. Batch and throttle incoming ticks to animation frames (rAF) rather than calling setState per message. D3+React division of labor: D3 computes scales, axes, and layouts (the math) while React renders the resulting SVG/DOM — if both mutate the DOM you get double-render bugs and lost React state.

**Use this technique when.** Dashboards, monitoring, trading/analytics UIs, anything with live data and heavy visualization.

```jsx
// D3 for the math, React for the DOM. Throttle live updates.
function LineChart({ series, width, height }) {
  const x = React.useMemo(
    () => d3.scaleTime().domain(d3.extent(series, d => d.t)).range([0, width]),
    [series, width]
  );
  const y = React.useMemo(
    () => d3.scaleLinear().domain([0, d3.max(series, d => d.v)]).range([height, 0]),
    [series, height]
  );
  const line = d3.line().x(d => x(d.t)).y(d => y(d.v));
  return (
    <svg width={width} height={height} role="img" aria-label="Metric over time">
      <path d={line(series)} fill="none" stroke="currentColor" />
    </svg>
  );
}
// Data layer: const ws = new WebSocket(url);
// ws.onmessage = throttle(e => store.applyTick(JSON.parse(e.data)), 250);
```

**References.** [MDN · The WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) · [MDN · Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) · [react.dev · useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)

---

### 11. Design a shared component library  `Hard`

**Pattern:** Component Library

**Problem.** Design a component library adopted by multiple product teams. Apply RADIO; cover theming, versioning, a11y.

**What it tests.** Architecture at the org level — governance, DX, theming, and adoption. Your headline Domo achievement.

**Approach & answer.** Requirements: consistent UI across teams, themeable/brandable, accessible by default, great DX, backward-compatible releases. Architecture: design tokens (color/space/type as CSS variables) at the base; primitive components consume tokens; composite components build on primitives; ship as a versioned package. Data model: components are stateless/controlled where possible; tokens are the single source of truth for theming. Interface: minimal, composable prop APIs (composition over boolean explosion), TypeScript types as the contract, documented with Storybook. Optimizations & governance: WCAG-compliant primitives (keyboard, ARIA, contrast) so every team inherits a11y; tree-shakeable exports; semantic versioning + changelog + deprecation path; visual regression + unit tests in CI; a contribution/review process. The org-level win is that accessibility and consistency are solved once and inherited by all — exactly the duplication-cutting outcome you drove at Domo. Design tokens as the single source of truth: express color/space/type/radius as CSS custom properties so theming and white-labeling become a token swap, not a component fork — primitives read tokens, composites read primitives. DX is a first-class requirement: minimal composable prop APIs, TypeScript types as the enforced contract, and Storybook as living docs so teams discover components instead of rebuilding them. Governance is what makes adoption stick: WCAG-compliant primitives so every consumer inherits accessibility for free; semantic versioning with a changelog and a deprecation path (deprecate → warn → remove across releases) so upgrades don't break teams; visual-regression + unit tests in CI; and a lightweight contribution/review process so the library scales beyond its original authors.

**Use this technique when.** Design-system / platform prompts, and 'how would you cut duplicated UI across teams' questions.

```text
Design tokens (CSS vars: --color-primary, --space-2, --font-body)
        │  single source of truth for theming
        ▼
Primitives (Button, Input, Text)  ── stateless, controlled, WCAG built-in
        ▼
Composites (Modal, Card, DataTable) ── composed from primitives
        ▼
Package  ── TS types = contract · Storybook docs · tree-shakeable
        ── semver + changelog + deprecations · visual + unit tests in CI
```

**References.** [WAI-ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/) · [Storybook · Documentation](https://storybook.js.org/docs) · [Semantic Versioning 2.0.0](https://semver.org/)

---

### 12. Design a resilient file uploader  `Hard`

**Pattern:** RADIO

**Problem.** Design a file uploader that handles large files with progress, retries, concurrency limits, and resilience to flaky networks.

**What it tests.** Chunking strategy, concurrency control, retry/backoff, and progress aggregation under real network conditions.

**Approach & answer.** Requirements: upload large files (hundreds of MB+), show accurate progress, survive transient failures, don't saturate the network, and ideally resume. Architecture — chunking is the core idea: slice each file with Blob.slice() into fixed-size chunks (e.g. 5MB) and upload them independently. This unlocks everything else: per-chunk retry (a failed chunk re-sends alone, not the whole file), resumability (ask the server which chunks it already has and skip them), and parallelism. Concurrency: run a bounded worker pool (e.g. 3-4 in-flight chunks) rather than firing all at once — too many parallel requests hurt throughput and hit browser connection limits; a simple queue drains work as slots free. Retry with exponential backoff + jitter on 5xx/network errors, capped at N attempts, so a blip self-heals without hammering the server. Progress: track bytes-sent per chunk and sum across chunks for whole-file percentage — use XHR's upload.onprogress (fetch lacks upload progress without streams) or a stream-based approach. Resilience extras: on final failure surface a retry affordance; pause/resume by stopping and restarting the queue; use an upload-session id so the server can assemble chunks and detect duplicates idempotently. Validate type/size client-side before starting, and compute a hash for integrity if the backend supports dedupe. Edge cases: user navigates away (warn/beforeunload), duplicate submissions, and very small files (skip chunking).

**Use this technique when.** Any 'design an uploader / resilient network transfer' prompt; reasoning about chunking, backoff, and concurrency caps.

**Complexity.** Bounded concurrency c keeps memory/connections O(c·chunkSize); total transfer parallelized across the pool.

```js
async function uploadFile(file, { chunkSize = 5 * 1024 * 1024, concurrency = 4 } = {}) {
  const chunks = [];
  for (let start = 0; start < file.size; start += chunkSize)
    chunks.push(file.slice(start, start + chunkSize));

  let next = 0;
  async function worker() {
    while (next < chunks.length) {
      const i = next++;
      await withRetry(() => putChunk(i, chunks[i])); // backoff + jitter inside
    }
  }
  await Promise.all(Array.from({ length: concurrency }, worker)); // bounded pool
}
```

**References.** [MDN · Using files from web applications](https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications) · [MDN · XMLHttpRequest: progress event](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/progress_event)

---

### 13. Design a notifications delivery system  `Hard`

**Pattern:** Real-time Transport

**Problem.** Design real-time notification delivery to the client. Apply RADIO; compare transports and cover reconnect, ordering, and dedup.

**What it tests.** Transport tradeoffs (poll vs long-poll vs SSE vs WebSocket), reconnect/backoff, dedup, and resume.

**Approach & answer.** Requirements: deliver server-originated notifications in near-real-time, survive reconnects, avoid duplicates, keep ordering where it matters, and scale to many clients. Architecture: a single connection manager owns the transport and fans messages out to subscribers; the server pushes events; the client tracks a cursor so it can resume. Transport tradeoffs (the core of the answer): POLLING (setInterval GET) is trivial and firewall-proof but high-latency and wasteful; LONG-POLLING holds the request open until data arrives — near-real-time over plain HTTP but connection-churny; SSE (EventSource) is a one-way server→client stream with automatic reconnection and Last-Event-ID resumption BUILT IN — ideal for notifications/feeds; WEBSOCKET is full-duplex for chat/collaboration but YOU own reconnection, heartbeats, and backpressure. Pick SSE for one-way notifications; WebSocket only when the client must also push. Resilience: reconnect with EXPONENTIAL BACKOFF + JITTER (don't stampede the server after an outage); send heartbeats/pings to detect dead connections; DEDUPE with a monotonic event id and drop ids already seen; RESUME from the last-seen id on reconnect so nothing is missed; buffer while disconnected. Ordering: a per-stream sequence number lets the client detect gaps and reorder. Also handle multi-tab (a shared worker or leader election so N tabs share one connection) and coalesce bursts. Degrade gracefully: try WebSocket/SSE, fall back to long-poll where blocked.

**Use this technique when.** Notification bells, live feeds, presence, chat — choosing a transport and owning reconnect/dedup/resume.

```js
// WebSocket: YOU own reconnection, heartbeat, dedup, and resume.
function connect(url, onMsg) {
  let delay = 1000, lastId = 0;
  const seen = new Set();

  function open() {
    const ws = new WebSocket(url + '?since=' + lastId);
    ws.onopen = () => { delay = 1000; };                 // reset backoff
    ws.onmessage = (e) => {
      const m = JSON.parse(e.data);
      if (seen.has(m.id)) return;                        // dedup
      seen.add(m.id); lastId = m.id;                     // resume cursor
      onMsg(m);
    };
    ws.onclose = () => {
      delay = Math.min(delay * 2, 30000);                // exponential
      setTimeout(open, delay + Math.random() * 1000);    // + jitter
    };
  }
  open();
}
// SSE (EventSource) is simpler for one-way: reconnect + Last-Event-ID are built in.
```

**References.** [MDN · Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) · [MDN · The WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)

---

### 14. Design a client data-fetching cache (SWR)  `Hard`

**Pattern:** Client Data Cache

**Problem.** Design a data-fetching layer like React Query / SWR. Apply RADIO; cover dedup, stale-while-revalidate, invalidation, and GC.

**What it tests.** Cache keying, request deduplication, background revalidation, optimistic mutation, and garbage collection.

**Approach & answer.** Requirements: a data layer that dedupes concurrent requests, serves cached data instantly, refreshes in the background, invalidates on mutation, and supports optimistic updates — essentially 'build your own React Query'. Architecture: a global cache keyed by a serialized query key; a useQuery(key, fetcher) hook reads the cache and subscribes to it; a coordinator ensures only ONE network request per key is in flight. Data model per key: { data, error, status, updatedAt, subscribers }. Interface: useQuery(key, fn), mutate(key), invalidate(key). Optimizations: STALE-WHILE-REVALIDATE — return cached (possibly stale) data immediately so the UI is instant, then refetch in the background and update subscribers when fresh data lands; DEDUPE — if three components mount with the same key, share ONE promise instead of three requests; INVALIDATE on mutation so dependent queries refetch; background refetch on window focus / reconnect / interval; OPTIMISTIC mutations — write the expected result into the cache, fire the request, roll back on error (the same pattern as optimistic UI). GARBAGE-COLLECT entries with no subscribers after a TTL so the cache doesn't grow unbounded. Why SWR beats fetch-in-useEffect: it removes duplicate requests, kills loading spinners on revisits (cache-first), and centralizes invalidation so the whole app stays consistent after a write. The subtle parts: a STABLE serialized key (sort object params), reference-stable reads so components don't re-render needlessly, and treating the cache as the single source of truth that components merely subscribe to.

**Use this technique when.** Building or reasoning about a data-fetching layer; explaining why React Query/SWR exist over raw useEffect fetches.

**Complexity.** Cache read O(1) by key; dedup makes N concurrent mounts cost 1 request. Memory bounded by GC of unsubscribed keys.

```js
const cache = new Map(); // key -> { data, promise, ts, subs:Set }

function useQuery(key, fetcher, { staleMs = 30000 } = {}) {
  const [, force] = React.useReducer((x) => x + 1, 0);
  let entry = cache.get(key);
  if (!entry) cache.set(key, (entry = { data: undefined, promise: null, ts: 0, subs: new Set() }));

  React.useEffect(() => {
    entry.subs.add(force);
    const stale = Date.now() - entry.ts > staleMs;
    if (stale && !entry.promise) {                  // dedup: one request per key
      entry.promise = fetcher(key).then((d) => {
        entry.data = d; entry.ts = Date.now(); entry.promise = null;
        entry.subs.forEach((fn) => fn());           // revalidate subscribers
      });
    }
    return () => entry.subs.delete(force);          // GC hook: drop key when subs empty
  }, [key]);

  return { data: entry.data };                       // stale-while-revalidate
}
```

**References.** [SWR · Getting Started (stale-while-revalidate)](https://swr.vercel.app/docs/getting-started) · [TanStack Query · Caching](https://tanstack.com/query/latest/docs/framework/react/guides/caching)

---

### 15. Design a real-time collaborative editor  `Hard`

**Pattern:** Collaborative Editing

**Problem.** Design a collaborative document editor (à la Google Docs). Apply RADIO; cover presence, conflict resolution, and offline.

**What it tests.** Concurrent-edit convergence (OT vs CRDT), presence/cursors, offline reconciliation, and latency compensation.

**Approach & answer.** Requirements: multiple users edit one document simultaneously, see each other's cursors/presence, converge to an identical state, and keep working offline. This is the hardest front-end consistency problem: concurrent edits must merge with no central lock. Architecture: each client holds a local replica and applies edits instantly (optimistic); edits are sent to a server that relays them to peers; a merge algorithm guarantees convergence. The core fork is CONFLICT RESOLUTION. OPERATIONAL TRANSFORMATION (OT) sends operations (insert@5, delete@3) and TRANSFORMS incoming ops against ones applied since, so concurrent inserts don't corrupt positions — powerful but the transform functions are notoriously hard to get right and usually need a central server to order ops. CRDTs (conflict-free replicated data types) give each character a unique, ordered id so operations are commutative and merge deterministically with no transform and no central authority — simpler correctness and true offline/peer-to-peer, at the cost of metadata overhead (tombstones for deletes). Modern editors lean CRDT (Yjs, Automerge). Presence: broadcast lightweight EPHEMERAL state (cursor position, selection, name/color) out-of-band from the document ops — it needn't persist. Hard parts: LATENCY COMPENSATION (apply locally first, reconcile remote ops as they arrive); OFFLINE (queue local ops, merge on reconnect — where CRDTs shine); intention preservation so a merge keeps what each user meant; and transport is usually WebSocket for low-latency bidirectional sync. Bound memory by garbage-collecting CRDT tombstones.

**Use this technique when.** Collaborative docs/whiteboards/design tools; explaining OT vs CRDT, presence, and offline merge.

```text
Client A ─ local replica (edit applied instantly, optimistic)
   │  ops
   ▼
Server ── orders / relays ops ──► broadcast to peers
   │
   ▼
Client B ─ merge incoming ops into local replica

Merge strategy:
  OT   — send operations, TRANSFORM against concurrent ops
         (needs a central server to order; transforms are hard)
  CRDT — unique ordered id per char -> ops commute, merge with no
         transform, works offline / p2p (cost: tombstone metadata)

Presence (cursor, selection, name) — ephemeral, sent out-of-band,
never persisted. Transport: WebSocket (low-latency, bidirectional).
```

**References.** [Yjs · Shared editing (CRDT)](https://docs.yjs.dev/) · [Wikipedia · Operational transformation](https://en.wikipedia.org/wiki/Operational_transformation)

---

## Accessibility

> Accessibility is where interview candidates most often reveal whether they build for real users or just for the happy path. These questions walk from the foundations — semantic HTML, the accessibility tree, alt text, keyboard navigation, focus indicators, contrast, and labelled forms — up through ARIA's rules, accessible errors, SVG and tables, into the hard custom widgets (modal dialogs, comboboxes, menus, tabs), live regions, keyboard-accessible drag-and-drop, and a realistic automated-plus-manual testing strategy. The throughline: prefer native semantics, keep ARIA state in sync with reality, make everything keyboard-operable, and remember that a passing automated scan is a floor, not proof.

### 1. Semantic HTML & landmark regions  `Easy`

**Pattern:** Semantic HTML

**Problem.** Why prefer <nav>, <main>, <button> over <div>s? What do landmarks give a screen-reader user?

**What it tests.** Whether you reach for the element that carries built-in semantics before adding ARIA.

**Approach & answer.** Semantic elements come with a role, keyboard behaviour, and state for free — a <button> is focusable, fires on Enter/Space, and exposes the 'button' role; a <div> you turn into a button gives you none of that until you add tabindex, key handlers, role, and aria-pressed by hand. Landmark elements — <header>, <nav>, <main>, <aside>, <footer>, <section> with a label, <form> — build a structural map of the page. Screen readers expose a landmarks rotor so a user can jump straight to 'main' or 'navigation' instead of tabbing through everything, the way a sighted user's eye skips to the content. The guiding principle is 'the first rule of ARIA': if a native element with the semantics and behaviour you need exists, use it rather than repurposing a generic element with an ARIA role. There should be exactly one <main>, and landmarks that repeat (multiple <nav>s) should be distinguished with aria-label so they read as 'primary navigation' vs 'footer navigation'. Semantic markup is also the substrate everything else stands on: alt text, headings, and labels only help because the element they describe already has a meaningful role.

**Use this technique when.** Choosing markup for any component; justifying why native elements beat div+ARIA reconstructions.

```html
<!-- Reconstructed from divs: no role, no focus, no keyboard, no landmarks -->
<div class="btn" onclick="save()">Save</div>
<div class="top-bar">...</div>

<!-- Semantic: roles, focus, keyboard, and a landmark map for free -->
<header>...</header>
<nav aria-label="Primary">...</nav>
<main>
  <button type="button" onclick="save()">Save</button>
</main>
<footer>...</footer>
```

**References.** [MDN · HTML: A good basis for accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML) · [MDN · ARIA landmark roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/landmark_role)

---

### 2. The accessibility tree & accessible names  `Easy`

**Pattern:** Accessibility Tree

**Problem.** What is the accessibility tree, and how does an element get its 'accessible name'?

**What it tests.** Understanding that assistive tech reads a parallel tree, and how name computation works.

**Approach & answer.** The browser builds a second tree alongside the DOM — the accessibility tree — where each node is reduced to what assistive technology needs: a role (what it is), a name (what to call it), a value/description, and states (checked, expanded, disabled). Screen readers, voice control, and testing tools read THIS tree, not your CSS. So an element that looks like a button on screen but is a bare <div> is, to a screen reader, an unnamed generic node it cannot describe or operate. The 'accessible name' is computed by a defined algorithm (accname) that walks a priority order: aria-labelledby (point at other elements' text) wins, then aria-label (a string you supply), then the element's own content or native labelling — a <label> for a form control, alt for an image, the text between a button's tags. If none of those yield text, the element is nameless and a screen reader announces just its role ('button') or nothing useful. The practical rules that fall out: give every control a name via its visible text where possible (so the name matches what a voice-control user says), reserve aria-label for icon-only controls, and remember content-based naming means <button>Delete</button> is already named — no ARIA required. display:none and aria-hidden prune a node from the tree entirely.

**Use this technique when.** Debugging why a control is announced wrong, and deciding between visible text, aria-label, and aria-labelledby.

```html
<!-- Name from content (best: visible text = accessible name) -->
<button>Delete</button>

<!-- Icon-only: no text content, so supply a name with aria-label -->
<button aria-label="Delete"><svg aria-hidden="true">…</svg></button>

<!-- Name from another element's text (labelledby wins over label/content) -->
<h2 id="sec-title">Billing</h2>
<section aria-labelledby="sec-title">…</section>
```

**References.** [MDN · The accessibility tree](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Accessibility_tree) · [W3C · Accessible Name and Description Computation](https://www.w3.org/TR/accname-1.2/)

---

### 3. Alt text: decorative vs informative images  `Easy`

**Pattern:** Images & Alt Text

**Problem.** How do you decide what alt text an image needs? When should alt be empty?

**What it tests.** Judging an image's PURPOSE, not describing its pixels — including when the right alt is none.

**Approach & answer.** Alt text conveys the image's FUNCTION in context, not a literal description of its pixels. Ask: if the image vanished, what information or action would be lost? That answer is the alt. An informative image (a chart, a product photo, a diagram) needs alt that carries its meaning — for a chart, summarise the takeaway, not 'chart'. A functional image (an icon inside a link or button) takes its alt from the action: a magnifying-glass link gets alt='Search', not 'magnifying glass'. A DECORATIVE image that adds nothing — a background flourish, a divider, an icon sitting next to text that already says the same thing — must get alt="" (empty, not missing). An empty alt tells the screen reader to skip it; a MISSING alt attribute makes many screen readers fall back to reading the file name ('IMG_2043.jpg'), which is noise. Never start alt with 'image of' — the role already says it's an image. Keep it concise; if the image needs a long description (a complex infographic), keep alt short and provide the detail in adjacent text or via a longer description. CSS background images are invisible to assistive tech entirely, so anything meaningful must be a real <img> with alt (or have its meaning conveyed in text).

**Use this technique when.** Writing alt for any image; deciding when empty alt is correct vs describing the content.

```html
<!-- Informative: alt carries the meaning -->
<img src="chart.png" alt="Sales doubled from Q1 to Q2">

<!-- Functional: alt = the action, not the picture -->
<a href="/search"><img src="lens.svg" alt="Search"></a>

<!-- Decorative: empty alt so the screen reader skips it -->
<img src="divider.svg" alt="">

<!-- WRONG: missing alt -> screen reader may read the filename -->
<img src="IMG_2043.jpg">
```

**References.** [W3C · WAI Images Tutorial (alt decision tree)](https://www.w3.org/WAI/tutorials/images/decision-tree/) · [MDN · <img> alt attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#alt)

---

### 4. Keyboard navigation & focus order  `Easy`

**Pattern:** Keyboard Navigation

**Problem.** What makes a UI keyboard-accessible? What does tabindex do, and why is a positive tabindex a smell?

**What it tests.** Whether every interaction works without a mouse, and how the tab order is really determined.

**Approach & answer.** A huge share of users navigate by keyboard alone — screen-reader users, people with motor impairments, power users. The baseline: everything you can do with a mouse must be doable with the keyboard, and the focus order must follow the visual/reading order. Native interactive elements (<a href>, <button>, <input>, <select>) are in the tab order automatically and handle their keys (Enter/Space to activate, arrows within selects); this is the #1 reason to use them instead of clickable <div>s, which are unreachable by Tab and deaf to Enter/Space until you wire it all up. tabindex controls focusability: tabindex="0" puts a normally non-focusable element (a custom widget root) into the natural tab order at its DOM position; tabindex="-1" makes an element focusable by script (element.focus()) but NOT by Tab — essential for moving focus to a heading or dialog programmatically. A POSITIVE tabindex (1, 2, 3…) is an anti-pattern: it yanks those elements to the FRONT of the tab order regardless of DOM position, creating a confusing sequence that's fragile and hard to maintain — fix the DOM order instead. Also never remove focusability from real controls, and ensure custom widgets implement their expected keys (Escape closes, arrows move within a composite). Focus order bugs usually trace to DOM order not matching visual order (e.g. CSS fl/grid reordering) — the fix is source order, not tabindex.

**Use this technique when.** Auditing a feature for keyboard support; explaining tabindex 0 vs -1 vs positive values.

```html
<!-- Unreachable by keyboard, deaf to Enter/Space -->
<div class="btn" onclick="go()">Go</div>

<!-- Native: in tab order + keyboard-operable for free -->
<button onclick="go()">Go</button>

<!-- tabindex 0: add a custom widget root to the natural order -->
<div role="slider" tabindex="0">…</div>

<!-- tabindex -1: focusable by script only (e.g. move focus here) -->
<h1 tabindex="-1" id="page-title">Results</h1>

<!-- SMELL: positive tabindex jumps ahead of DOM order -->
<input tabindex="3">
```

**References.** [MDN · Keyboard-navigable JavaScript widgets](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets) · [MDN · tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex)

---

### 5. Visible focus indicators & :focus-visible  `Easy`

**Pattern:** Focus Indicators

**Problem.** Why is `outline: none` a bug? What does :focus-visible solve?

**What it tests.** That the focus ring is a required affordance, and how to satisfy both mouse and keyboard users.

**Approach & answer.** The focus ring is how a keyboard user knows where they are — remove it and the page becomes unusable without a mouse, which is why a bare `*:focus { outline: none }` is one of the most common and most damaging accessibility bugs (and a WCAG 2.4.7 failure). It's usually done because the default outline looks ugly on mouse click. :focus-visible resolves the tension: the browser applies it only when focus arrives via keyboard (or other non-pointer means) using a heuristic, so you can show a strong ring for keyboard users and suppress it on mouse click. The correct pattern is never to kill the outline outright, but to REPLACE it with a clearly visible custom indicator scoped to :focus-visible. The indicator must meet contrast requirements against the background (WCAG 2.4.11 in 2.2 sets a non-text contrast bar and a minimum area), so a faint 1px light-grey line isn't enough. If you must support older browsers, keep :focus as a fallback and layer :focus-visible on top. Related: never rely on focus styles alone to convey selected/active state to screen-reader users — that's what aria-current, aria-selected, and roles are for; focus-visible is a purely visual affordance.

**Use this technique when.** Any time a designer wants to hide the focus ring; giving keyboard users a visible, high-contrast indicator.

```html
<style>
  /* WRONG: strips the ring for everyone, keyboard users included */
  button:focus { outline: none; }

  /* RIGHT: strong ring only when focus came from the keyboard */
  button:focus-visible {
    outline: 3px solid #2563eb;
    outline-offset: 2px;
  }
  /* Fallback for browsers without :focus-visible */
  button:focus { outline: 3px solid #2563eb; }
  button:focus:not(:focus-visible) { outline: none; }
</style>
<button>Save</button>
```

**References.** [MDN · :focus-visible](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) · [WCAG · 2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html)

---

### 6. Heading structure & the document outline  `Easy`

**Pattern:** Document Structure

**Problem.** Why do heading levels matter for screen readers? What are the rules for h1–h6?

**What it tests.** That headings are navigation, not font sizes — and the common level-skipping / styling bugs.

**Approach & answer.** Headings are the primary way screen-reader users navigate a page: they pull up a headings list (a rotor) and jump between sections, and they press keys to move to the next heading of a given level — exactly how a sighted reader scans bold section titles. For that to work the levels must describe a logical OUTLINE, not visual size. Rules: use one <h1> that names the page/main content; don't skip levels going down (an <h2> section's subsections are <h3>, never jumping h2→h4); and never pick a heading level for its default font size — style with CSS, choose the level by meaning. The two classic bugs are (1) using <div class="heading"> or a styled <p> that looks like a heading but has no heading role, so it's invisible to the rotor, and (2) skipping/misordering levels so the outline is nonsensical. A correct heading tree lets a user grasp the page's structure and skip to what they want in seconds; a flat or broken one forces linear reading of everything. Note the once-hoped-for HTML5 'document outline algorithm' (auto-computing levels from <section> nesting) was never implemented by browsers or AT — so explicit h1–h6 levels are still required.

**Use this technique when.** Structuring any page or component; catching styled-div 'headings' and skipped levels in review.

```html
<!-- Logical outline: one h1, no skipped levels -->
<h1>Account settings</h1>
  <h2>Profile</h2>
    <h3>Avatar</h3>
    <h3>Display name</h3>
  <h2>Security</h2>
    <h3>Password</h3>

<!-- WRONG: looks like a heading, but no heading role -> invisible to the rotor -->
<div class="h2-style">Security</div>

<!-- WRONG: skips h2 -> h4, breaking the outline -->
<h1>Title</h1>
<h4>Subsection</h4>
```

**References.** [W3C · WAI Headings tutorial](https://www.w3.org/WAI/tutorials/page-structure/headings/) · [MDN · Heading elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements)

---

### 7. Color contrast & not relying on color alone  `Easy`

**Pattern:** Color & Contrast

**Problem.** What are the WCAG contrast requirements, and why is 'color alone' a failure even when contrast passes?

**What it tests.** The AA ratio thresholds and the separate 'use of color' requirement most teams miss.

**Approach & answer.** Two distinct requirements, often conflated. (1) CONTRAST (WCAG 1.4.3, AA): normal text needs a contrast ratio of at least 4.5:1 against its background; large text (≈18.66px bold or 24px regular) needs 3:1; and non-text UI — icons, input borders, focus rings, the boundary of a control — needs 3:1 (1.4.11). AAA raises text to 7:1. The ratio is computed from relative luminance, so light-grey-on-white placeholder text is a frequent failure. (2) USE OF COLOR (1.4.1): color must not be the ONLY means of conveying information. A form field that turns red to signal an error, a required field marked only by red text, a chart whose series are distinguished only by hue, a link identified only by color inside body text — all fail, because colour-blind users and screen-reader users get nothing. The fix is a redundant cue: an error icon and text ('Email is required'), an asterisk plus a legend, patterns or direct labels on chart series, underlines on inline links. So a design can pass contrast and still fail on colour-alone, and vice versa — you must check both. Test with a contrast checker on real foreground/background pairs (including hover/disabled states), not just the base theme.

**Use this technique when.** Reviewing a color palette; catching red-only errors and hue-only charts even when contrast is fine.

```html
<!-- Contrast: light grey on white fails 4.5:1 for normal text -->
<p style="color:#bbb; background:#fff">Hard to read</p>

<!-- Use of color: red alone conveys 'error' -> fails 1.4.1 -->
<input class="error" aria-label="Email">   <!-- only a red border -->

<!-- Fixed: redundant text + icon, not color alone -->
<label for="email">Email</label>
<input id="email" aria-invalid="true" aria-describedby="email-err">
<p id="email-err">⚠ Enter a valid email address</p>
```

**References.** [WCAG · 1.4.3 Contrast (Minimum)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) · [WCAG · 1.4.1 Use of Color](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html)

---

### 8. Labeling form controls  `Easy`

**Pattern:** Accessible Forms

**Problem.** How do you correctly label an input? Why isn't a placeholder a label, and what's wrong with a bare aria-label?

**What it tests.** Programmatic label association and the common placeholder-as-label mistake.

**Approach & answer.** Every form control needs a programmatically associated label so its accessible name is announced on focus and its hit target includes the label text. The primary tool is <label>: either wrap the control (<label>Email <input></label>) or, better for styling, use for/id (<label for="email">…</label><input id="email">). A correctly associated label also lets sighted users click the label to focus the control — a usability win for everyone. A PLACEHOLDER is not a label: it disappears the moment the user types (so they lose the field's name mid-entry), it's typically low-contrast (a contrast failure), and support for exposing it as a name is inconsistent — never use placeholder as the only label. aria-label and aria-labelledby can supply a name when a visible label genuinely isn't possible (a search field with only an icon), but they have a cost: aria-label has NO visible text, so voice-control users can't say the label to target it, and sighted users get no caption — prefer a real visible <label> and reserve ARIA labelling for icon-only or space-constrained cases. Group related controls (radio sets, a set of checkboxes) in a <fieldset> with a <legend> so the group's purpose is announced. Required, format hints, and errors are conveyed with required/aria-required and aria-describedby, not baked into the placeholder.

**Use this technique when.** Building any form; killing placeholder-as-label and choosing label vs aria-label.

```html
<!-- Best: visible label associated via for/id -->
<label for="email">Email</label>
<input id="email" type="email" required>

<!-- Also valid: wrapping label -->
<label>Email <input type="email"></label>

<!-- WRONG: placeholder is not a label (vanishes on typing, low contrast) -->
<input placeholder="Email">

<!-- Grouped controls need a fieldset + legend -->
<fieldset>
  <legend>Notify me by</legend>
  <label><input type="checkbox" name="n" value="email"> Email</label>
  <label><input type="checkbox" name="n" value="sms"> SMS</label>
</fieldset>
```

**References.** [MDN · <label>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) · [W3C · WAI Forms: Labeling Controls](https://www.w3.org/WAI/tutorials/forms/labels/)

---

### 9. ARIA roles, states, properties — and the first rule of ARIA  `Medium`

**Pattern:** ARIA

**Problem.** What do ARIA roles, states, and properties actually do? Why is 'no ARIA better than bad ARIA'?

**What it tests.** That ARIA changes only the accessibility tree — never behaviour — and is easy to get dangerously wrong.

**Approach & answer.** ARIA (Accessible Rich Internet Applications) is a set of attributes that override or supplement what the accessibility tree exposes. Three kinds: ROLES say what a thing is (role="tablist", role="dialog"); STATES are dynamic and change (aria-expanded, aria-checked, aria-selected, aria-disabled); PROPERTIES are more static relationships/config (aria-label, aria-labelledby, aria-describedby, aria-controls, aria-haspopup). The critical mental model: ARIA changes ONLY the semantics reported to assistive tech. It adds no behaviour, no focusability, no keyboard handling, no styling. Slapping role="button" on a <div> makes a screen reader announce 'button' but the div still isn't focusable and still ignores Enter/Space until you add tabindex and key handlers yourself. This is why 'no ARIA is better than bad ARIA': incorrect ARIA actively lies to users — a role="checkbox" with no aria-checked, or an aria-expanded you forget to update, is worse than a plain element, because the user is told a state that doesn't match reality. The rules of thumb (the ARIA Authoring Practices): (1) prefer a native element with the semantics you need over ARIA; (2) don't change native semantics unless you must (don't put role="heading" on a <button>); (3) all interactive ARIA widgets must be keyboard-operable; (4) don't use role="presentation" or aria-hidden="true" on a focusable element (you'd hide something a keyboard user can still reach). Keep aria-* states in sync with the DOM on every change — that's the recurring bug.

**Use this technique when.** Reaching for a role/aria-* attribute; auditing custom widgets for state that drifts out of sync.

```html
<!-- BAD ARIA: announces 'button' but not focusable, no keys, worse than nothing -->
<div role="button">Menu</div>

<!-- If you MUST use a div, you own all of it -->
<div role="button" tabindex="0"
     onclick="toggle()" onkeydown="if(event.key==='Enter'||event.key===' ')toggle()">
  Menu
</div>

<!-- State must track reality: update aria-expanded on every toggle -->
<button aria-expanded="false" aria-controls="menu" onclick="toggle()">Menu</button>
<ul id="menu" hidden>…</ul>

<!-- Best: a native <button> gives role + focus + keyboard for free -->
```

**References.** [MDN · ARIA basics](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) · [W3C · Using ARIA (rules)](https://www.w3.org/TR/using-aria/)

---

### 10. Accessible form errors & validation  `Medium`

**Pattern:** Accessible Forms

**Problem.** How do you announce a validation error to a screen-reader user and tie it to the field?

**What it tests.** Programmatic error association (aria-describedby/aria-invalid) and announcing errors, not just coloring them red.

**Approach & answer.** A visible red border and message help sighted users but are invisible to screen readers unless wired up. Three pieces. (1) ASSOCIATE the error text with the field using aria-describedby pointing at the error element's id, so on focus the screen reader reads the label, then the value, then the error/hint. aria-describedby can list multiple ids (a format hint AND an error). (2) MARK the field invalid with aria-invalid="true" (and remove it when fixed) so the state is exposed programmatically, not just via colour. (3) ANNOUNCE on submit: move focus to the first invalid field (so the user lands on the problem and hears its error via describedby), or render an error SUMMARY at the top of the form inside a container that receives focus or is a live region, listing each error as a link to its field — the pattern GOV.UK popularised. Don't rely on the field turning red (fails use-of-color) and don't use placeholder for hints (vanishes). For inline/live validation, be careful: validating and announcing on every keystroke is noisy and can interrupt — validate on blur or submit, and if you use aria-live for a status, keep it polite. required/aria-required communicates the requirement up front. The recurring senior insight: the error must be perceivable (announced), programmatically related to its field (describedby), and reachable (focus moves to it) — not merely visible.

**Use this technique when.** Wiring up form validation; making errors announced and reachable, not just red.

```html
<label for="pw">Password</label>
<input id="pw" type="password"
       aria-describedby="pw-hint pw-err"
       aria-invalid="true" required>

<p id="pw-hint">At least 12 characters.</p>
<p id="pw-err">Password is too short.</p>

<!-- On submit, move focus to the first invalid field: -->
<script>document.getElementById('pw').focus();</script>

<!-- Or an error summary at the top, focused on submit, linking to each field -->
<div role="alert" tabindex="-1">
  <h2>There is a problem</h2>
  <ul><li><a href="#pw">Password is too short</a></li></ul>
</div>
```

**References.** [W3C · WAI Forms: User Notifications](https://www.w3.org/WAI/tutorials/forms/notifications/) · [MDN · aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby)

---

### 11. Accessible SVG, icons & complex images  `Medium`

**Pattern:** Images & SVG

**Problem.** How do you make inline SVG icons and complex graphics accessible? What about icon-only buttons?

**What it tests.** Naming/hiding inline SVG correctly and handling icon-only controls and complex images.

**Approach & answer.** Inline SVG has no alt attribute, and browser/AT support for its internal semantics is inconsistent, so you name or hide it explicitly. DECORATIVE svg (an icon next to text that already says the same thing) should be removed from the tree: aria-hidden="true" plus focusable="false" (the latter stops old IE/Edge from tabbing into it). MEANINGFUL standalone svg should get role="img" and a name via aria-label (or a <title> element referenced by aria-labelledby) — role="img" collapses the SVG's internal shapes so the name is announced as one graphic rather than reading every <path>. ICON-ONLY BUTTONS are the highest-frequency bug: <button><svg…></button> with no text is a nameless button. Fix by naming the BUTTON (aria-label="Close") and hiding the decorative icon inside it (aria-hidden), or include visually-hidden text. For a COMPLEX image (an infographic, a data-dense chart), a short alt/label can't carry it — provide a longer text alternative nearby (a caption, an adjacent description, or a data table conveying the same information), and keep the short name as an entry point. General rule: the icon is decoration; the CONTROL or the graphic's MEANING is what needs the name. And don't forget: an SVG conveying state (a filled vs empty star rating) needs that state in text/ARIA, since colour and shape alone won't reach everyone.

**Use this technique when.** Adding inline SVG icons; naming icon-only buttons; giving complex graphics a real text alternative.

```html
<!-- Decorative icon beside text: hide it from the tree -->
<button>
  <svg aria-hidden="true" focusable="false">…</svg>
  Delete
</button>

<!-- Icon-only button: name the BUTTON, hide the icon -->
<button aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false">…</svg>
</button>

<!-- Meaningful standalone graphic: role=img + name -->
<svg role="img" aria-label="Company logo">…</svg>

<!-- Complex image: short name + longer description nearby -->
<img src="flow.png" alt="Signup funnel (described below)">
<div id="flow-desc"><p>Visitors → 40% sign up → 12% activate…</p></div>
```

**References.** [MDN · SVG accessibility](https://developer.mozilla.org/en-US/docs/Web/SVG/Guides/SVG_as_an_image) · [W3C · WAI Complex Images](https://www.w3.org/WAI/tutorials/images/complex/)

---

### 12. Skip links & focus management in SPAs  `Medium`

**Pattern:** Focus Management

**Problem.** What is a skip link and why does it matter? How do you handle focus when a client-side route changes?

**What it tests.** Bypassing repeated content and the SPA-specific problem of route changes not moving focus.

**Approach & answer.** A SKIP LINK is a link, first in the tab order, that jumps past repeated blocks (nav, header) straight to <main>. Keyboard and screen-reader users otherwise Tab through the same 30 nav links on every page. Convention: it's visually hidden until focused, then appears; it targets an id on the main region (href="#main"), which should have tabindex="-1" so focus actually lands there in all browsers. This satisfies WCAG 2.4.1 (Bypass Blocks). The SPA problem is subtler and widely broken: with a real page navigation the browser resets focus to the top and screen readers announce the new page. With CLIENT-SIDE routing, the URL and DOM change but focus stays on the link the user clicked and NOTHING is announced — a screen-reader user has no idea the page changed, and a keyboard user's next Tab resumes from a stale location. The fix on each route change: move focus to a sensible target — typically the new page's <h1> (given tabindex="-1") or the main container — and/or announce the new page title via a polite live region (a visually-hidden aria-live region updated with the route name). Also update document.title so the tab/announcement reflects the page. Frameworks don't do this automatically; it's the app's responsibility. The senior framing: an SPA must recreate the two things a full navigation gave for free — focus reset and a page-change announcement.

**Use this technique when.** Adding a skip link; fixing SPA route changes that leave focus stranded and unannounced.

```jsx
// Skip link (first focusable element on the page)
// <a class="skip-link" href="#main">Skip to content</a>
// <main id="main" tabindex="-1">…</main>

// On every client-side route change, move focus + announce:
function onRouteChange(pageTitle) {
  document.title = pageTitle;                 // reflect the new page
  const h1 = document.querySelector('main h1');
  if (h1) { h1.setAttribute('tabindex', '-1'); h1.focus(); }  // focus lands on new content
  announce(pageTitle);                        // update a polite aria-live region
}

// Visually-hidden live region kept in the DOM:
// <div aria-live="polite" class="sr-only" id="route-status"></div>
function announce(msg){ document.getElementById('route-status').textContent = msg; }
```

**References.** [WCAG · 2.4.1 Bypass Blocks](https://www.w3.org/WAI/WCAG21/Understanding/bypass-blocks.html) · [MDN · Client-side routing and accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Operable)

---

### 13. Accessible data tables  `Medium`

**Pattern:** Data Tables

**Problem.** How do you make a data table accessible? What do <th scope>, <caption>, and headers/id do — and why never tables for layout?

**What it tests.** Associating headers with cells so a screen reader can announce a cell in context.

**Approach & answer.** A data table's accessibility comes from the header–cell relationships that let a screen reader announce, at any cell, which row and column it belongs to — the equivalent of a sighted user glancing up and left. Mark header cells as <th> (not <td>), and give each a scope: scope="col" for column headers, scope="row" for row headers. With scope set, when the user navigates to a data cell the screen reader reads the associated column (and row) header before the value — 'Revenue, Q2, $4M' — instead of a bare '$4M'. Add a <caption> as the table's accessible name/title so users know what the table is before entering it. Use <thead>/<tbody> to structure it. For COMPLEX tables with split or multi-level headers where simple scope is ambiguous, use the headers/id mechanism: give each <th> an id and each <td> a headers attribute listing the ids that describe it — explicit but verbose, so prefer restructuring into simpler tables when you can. Two anti-patterns: (1) using <table> for visual LAYOUT — it forces AT into a data-table reading mode and announces meaningless row/column counts; use CSS grid/flex for layout instead (or role="presentation" only as a last resort on a genuine layout table). (2) A 'table' built from <div>s with no semantics — if you must, apply role="table/row/columnheader/cell", but a real <table> is far less error-prone. Empty header cells and merged cells are the usual real-world pain points.

**Use this technique when.** Rendering tabular data; associating headers with cells and keeping tables out of layout.

```html
<table>
  <caption>Quarterly revenue by region</caption>
  <thead>
    <tr>
      <th scope="col">Region</th>
      <th scope="col">Q1</th>
      <th scope="col">Q2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">EMEA</th>   <!-- row header -->
      <td>$3M</td>
      <td>$4M</td>              <!-- announced as "EMEA, Q2, $4M" -->
    </tr>
  </tbody>
</table>

<!-- Never use <table> to lay out a page; use CSS grid/flex instead. -->
```

**References.** [W3C · WAI Tables tutorial](https://www.w3.org/WAI/tutorials/tables/) · [MDN · <th> scope](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th#scope)

---

### 14. Building an accessible modal dialog  `Hard`

**Pattern:** Dialogs & Focus Trap

**Problem.** What does a modal dialog need to be accessible? Walk through the focus trap, aria-modal, and restoring focus.

**What it tests.** The full modal contract: role, labelling, focus trap, Escape, background inert, and focus restoration.

**Approach & answer.** An accessible modal has a precise contract. (1) SEMANTICS: role="dialog" (or role="alertdialog" for a confirm/error that demands a response) plus aria-modal="true", and a name via aria-labelledby pointing at the dialog title (aria-describedby for the body if useful). (2) FOCUS IN: when it opens, move focus INTO the dialog — to the first focusable control, or the dialog container/heading if there isn't an obvious one. (3) FOCUS TRAP: while open, Tab and Shift+Tab must cycle only within the dialog; Tab from the last element wraps to the first and vice versa, so focus can't escape to the page behind. (4) BACKGROUND INERT: content behind the dialog must be unreachable AND unread — set inert (or aria-hidden="true") on the rest of the page so screen-reader virtual-cursor browsing and Tab both stay contained; aria-modal helps but the inert background is what actually prevents 'reading behind'. (5) ESCAPE + dismiss: Escape closes it; clicking the backdrop typically closes (but not for alertdialog). (6) FOCUS RESTORATION: on close, return focus to the element that opened it (store it on open) — otherwise the user is dumped at the top of the document. The modern shortcut is the native <dialog> element with showModal(), which gives you the top layer, a real backdrop (::backdrop), Escape-to-close, and background inertness for free — you still supply the label and focus restoration, but it removes most of the trap boilerplate and its bugs. Rolling your own is where teams get it wrong: forgetting the trap, forgetting to restore focus, or hiding the background from sighted users (CSS) but not from AT.

**Use this technique when.** Building any modal/overlay; auditing one for trap, inert background, Escape, and focus restore.

```jsx
function Modal({ open, onClose, title, children }) {
  const ref = React.useRef(null);
  const opener = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    opener.current = document.activeElement;          // remember what to restore
    const dlg = ref.current;
    dlg.showModal();                                   // native: top layer + backdrop + inert bg + Esc
    const first = dlg.querySelector('button, [href], input, select, textarea, [tabindex]');
    (first || dlg).focus();
    return () => { dlg.close(); opener.current && opener.current.focus(); }; // restore focus on close
  }, [open]);

  return (
    <dialog ref={ref} aria-labelledby="dlg-title" onCancel={onClose} onClose={onClose}>
      <h2 id="dlg-title">{title}</h2>
      {children}
      <button onClick={onClose}>Close</button>
    </dialog>
  );
}
```

**References.** [W3C · APG Dialog (Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) · [MDN · <dialog>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)

---

### 15. Accessible combobox / autocomplete (APG)  `Hard`

**Pattern:** ARIA Widgets

**Problem.** Design an accessible autocomplete. What roles, ARIA state, and keyboard model does the combobox pattern require?

**What it tests.** The combobox/listbox wiring: aria-activedescendant vs focus, expanded state, and the full key map.

**Approach & answer.** A combobox is an input paired with a popup (usually a listbox of suggestions) — one of the hardest widgets to get right. Structure per the APG: a text <input> with role="combobox" (native input already, but the role/attrs make the popup relationship explicit), aria-expanded reflecting whether the popup is open, aria-controls pointing at the listbox id, and aria-autocomplete="list" (or 'both' when it also inline-completes). The popup is role="listbox" containing role="option" items, each with a unique id and aria-selected on the active one. The key decision is HOW focus works: keep DOM focus on the INPUT (so the user can keep typing) and track the highlighted option with aria-activedescendant on the input, set to the active option's id — the screen reader announces that option while real focus never leaves the input. (The alternative, moving real focus into the list, breaks typing.) Keyboard model: Down/Up arrows move the active option (opening the popup if closed) and update aria-activedescendant; Enter selects the active option and closes; Escape closes the popup (and on a second press may clear the input); Home/End jump within the list; typing filters. On selection, put the value in the input, collapse the popup (aria-expanded="false"), and clear aria-activedescendant. Announce result counts via a polite live region ('5 results available') so screen-reader users know suggestions appeared. Because this is so error-prone, most teams should use a vetted library or the emerging native options — but you must be able to reason about the roles, the activedescendant model, and the key map.

**Use this technique when.** Building autocomplete/typeahead/select-with-search; explaining activedescendant vs moving focus.

```html
<label for="city">City</label>
<input id="city" role="combobox"
       aria-expanded="true"
       aria-controls="city-list"
       aria-autocomplete="list"
       aria-activedescendant="city-opt-2">   <!-- focus stays here; this id = highlighted option -->

<ul id="city-list" role="listbox">
  <li id="city-opt-1" role="option">London</li>
  <li id="city-opt-2" role="option" aria-selected="true">Lisbon</li>
  <li id="city-opt-3" role="option">Lima</li>
</ul>

<div aria-live="polite" class="sr-only">3 results available</div>
<!-- Keys: ↓/↑ move activedescendant, Enter selects, Esc closes, typing filters -->
```

**References.** [W3C · APG Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) · [MDN · aria-activedescendant](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-activedescendant)

---

### 16. Live regions: aria-live, status vs alert  `Hard`

**Pattern:** Live Regions

**Problem.** How do you announce dynamic content (toasts, async results, errors) to a screen reader? polite vs assertive?

**What it tests.** When and how AT announces DOM changes, the politeness levels, and the timing pitfalls.

**Approach & answer.** Screen readers announce the focused element; content that changes somewhere ELSE on the page — a toast, 'saved', search-result counts, an async error — is silent unless it's in a LIVE REGION. A live region is a container the AT watches; when its contents change, the AT queues an announcement without moving focus. Politeness: aria-live="polite" waits until the user is idle (doesn't interrupt typing) — the default choice for status updates; aria-live="assertive" interrupts immediately — reserve for urgent, time-critical messages (a session-expiry warning, a submission error) because it's disruptive. Two convenience roles bundle this: role="status" ≈ polite (also implies aria-atomic behaviour for status), and role="alert" ≈ assertive. The single biggest pitfall: the live region MUST already exist in the DOM (empty) BEFORE you put text in it. If you inject the region and its message together, many screen readers miss the change because they only announce mutations to regions they were already observing — so render an empty <div aria-live="polite"> up front and update its textContent later. Other controls: aria-atomic="true" makes the AT read the WHOLE region on any change (vs just the changed node) — use for a message that only makes sense as a unit; aria-relevant tunes which mutation types announce. Keep messages short, don't stuff a live region with a whole page of content, avoid multiple assertive regions competing, and clear/replace text so the same message announced twice actually re-announces (some AT needs the text to change).

**Use this technique when.** Toasts, async status, form errors, result counts — announcing changes without moving focus.

```jsx
// The region must exist (empty) BEFORE the message is written into it.
function StatusRegion({ message }) {
  return (
    <div aria-live="polite" aria-atomic="true" className="sr-only">
      {message}          {/* update state later -> AT announces politely */}
    </div>
  );
}

// Urgent, interrupts: role="alert" ≈ assertive
function ErrorBanner({ error }) {
  return error ? <div role="alert">{error}</div> : null;
}

// WRONG: injecting the region + text together often isn't announced
// container.innerHTML = '<div aria-live="polite">Saved</div>';
```

**References.** [MDN · ARIA live regions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) · [W3C · APG Alert Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alert/)

---

### 17. Menu / menubar & roving tabindex  `Hard`

**Pattern:** ARIA Widgets

**Problem.** How does an application menu differ from a nav list? What is roving tabindex and why use it?

**What it tests.** The composite-widget model: one tab stop, arrow-key navigation, and roving vs activedescendant.

**Approach & answer.** First, a distinction interviewers probe: a site NAVIGATION (links to pages) is NOT an ARIA menu — it's a <nav> with a list of links, and you should not slap role="menu" on it. role="menu"/"menubar" is for an APPLICATION menu of commands/actions (like a desktop app's menu bar), with role="menuitem" (and menuitemcheckbox/menuitemradio) children. Getting that wrong makes screen readers announce 'menu' and impose command-menu key expectations on what are really links. The core interaction model for menus — and for tabs, listboxes, radio groups, tree grids, any COMPOSITE widget — is that the whole widget is a SINGLE tab stop: Tab moves INTO and OUT of it as one unit, and ARROW keys move between the items inside. That's the WAI-ARIA convention users expect. Two ways to implement 'arrows move the active item': (1) ROVING TABINDEX — exactly one item has tabindex="0" (the current one) and all others have tabindex="-1"; on arrow key you move real DOM focus to the new item and swap the tabindex values so it becomes the new tab stop. Real focus moves, so focus styles and getElementById-free focus work naturally. (2) aria-activedescendant — focus stays on the container and you point activedescendant at the active child's id (the combobox approach). Roving tabindex suits menus/toolbars/radio groups where items are real focusable elements; activedescendant suits inputs that must keep focus (comboboxes). Either way: Tab is one stop, arrows navigate, Home/End jump, Escape closes a popup menu and returns focus to its trigger, and typeahead (type a letter to jump) is expected in menus. The bug to avoid is making every item its own tab stop — that's the sign someone reconstructed a menu from plain buttons without the composite model.

**Use this technique when.** Building menus, toolbars, tab sets, radio groups; choosing roving tabindex vs activedescendant.

```jsx
// Roving tabindex: one item is the tab stop (0), the rest are -1.
function Menu({ items }) {
  const [active, setActive] = React.useState(0);
  const refs = React.useRef([]);

  function onKeyDown(e) {
    let next = active;
    if (e.key === 'ArrowDown') next = (active + 1) % items.length;
    else if (e.key === 'ArrowUp') next = (active - 1 + items.length) % items.length;
    else if (e.key === 'Home') next = 0;
    else if (e.key === 'End') next = items.length - 1;
    else return;
    e.preventDefault();
    setActive(next);
    refs.current[next].focus();          // move REAL focus to the new item
  }

  return (
    <ul role="menu" onKeyDown={onKeyDown}>
      {items.map((label, i) => (
        <li key={label} role="menuitem"
            tabIndex={i === active ? 0 : -1}   // exactly one 0 -> single tab stop
            ref={el => (refs.current[i] = el)}>
          {label}
        </li>
      ))}
    </ul>
  );
}
```

**References.** [W3C · APG Menu & Menubar Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) · [W3C · APG Keyboard: roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/)

---

### 18. Accessible tabs (tablist / tab / tabpanel)  `Hard`

**Pattern:** ARIA Widgets

**Problem.** Build an accessible tabs widget. What roles and ARIA link a tab to its panel, and what's the keyboard model?

**What it tests.** The tabs roles/relationships, the single-tab-stop arrow model, and automatic vs manual activation.

**Approach & answer.** Roles and relationships: a container with role="tablist" holds the tabs; each tab is role="tab" with aria-selected (true on the active one, false on the rest) and aria-controls pointing at the id of its panel; each panel is role="tabpanel" with aria-labelledby pointing back at its tab's id. That two-way wiring lets a screen reader announce 'tab, 2 of 3, selected' and, on entering the panel, name it from its tab. Keyboard model (composite widget, so a single tab stop via roving tabindex): Tab moves focus INTO the active tab and, again, OUT to the tabpanel — not between tabs. LEFT/RIGHT arrows (or Up/Down for vertical tabs) move between tabs, wrapping at the ends; Home/End jump to first/last. The panel itself is often given tabindex="0" so that if it has no focusable content the user can still Tab to it and read it. A key design choice: AUTOMATIC activation (selecting a tab the instant arrow keys land on it) vs MANUAL activation (arrow keys move focus, Enter/Space activates). Automatic is fine and slightly faster when switching panels is cheap and instant; MANUAL is required when activating a tab is expensive (loads data, heavy render) so arrowing through tabs doesn't fire a load per keystroke. Only ONE panel is visible at a time; hide the inactive panels with hidden (not just CSS) so their content is out of the tab order and the accessibility tree. Don't reconstruct tabs from plain links/buttons without these roles — the relationships and the arrow-key model are exactly what AT users expect from something announced as 'tab'.

**Use this technique when.** Building a tabbed interface; wiring tab↔panel relationships and choosing automatic vs manual activation.

```jsx
function Tabs({ tabs }) {
  const [sel, setSel] = React.useState(0);
  function onKeyDown(e) {
    if (e.key === 'ArrowRight') setSel((sel + 1) % tabs.length);
    else if (e.key === 'ArrowLeft') setSel((sel - 1 + tabs.length) % tabs.length);
  }
  return (
    <div>
      <div role="tablist" onKeyDown={onKeyDown}>
        {tabs.map((t, i) => (
          <button key={t.id} role="tab" id={'tab-' + t.id}
                  aria-selected={i === sel}
                  aria-controls={'panel-' + t.id}
                  tabIndex={i === sel ? 0 : -1}       // roving: one tab stop
                  onClick={() => setSel(i)}>{t.label}</button>
        ))}
      </div>
      {tabs.map((t, i) => (
        <div key={t.id} role="tabpanel" id={'panel-' + t.id}
             aria-labelledby={'tab-' + t.id}
             hidden={i !== sel} tabIndex={0}>{t.content}</div>
      ))}
    </div>
  );
}
```

**References.** [W3C · APG Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) · [MDN · ARIA: tab role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tab_role)

---

### 19. Keyboard-accessible drag and drop  `Hard`

**Pattern:** Keyboard Interaction

**Problem.** Native HTML drag-and-drop is inaccessible. How do you make a reorder/DnD interaction work for keyboard and screen-reader users?

**What it tests.** Providing a non-pointer alternative for an inherently pointer-based interaction and announcing it.

**Approach & answer.** The HTML5 Drag and Drop API is effectively unusable by keyboard and poorly supported by screen readers, so an accessible reorder cannot rely on it alone — you must provide an equivalent keyboard operation and announce what happens. The established pattern (WAI-ARIA APG's 'keyboard' approach, as popularised by libraries like dnd-kit and react-aria) has a few parts. (1) Each draggable item is focusable and has a clear accessible name and instructions (e.g. via aria-describedby: 'Press Space to pick up, arrow keys to move, Space to drop, Escape to cancel'). (2) A GRABBED state: on Space/Enter the item enters 'picked up' mode; arrow keys then move it among positions; a second Space drops it; Escape cancels and returns it to its origin. (While grabbing, aria-grabbed / aria-dropeffect existed in ARIA 1.0 but are deprecated — modern implementations manage state and announcements manually rather than relying on them.) (3) ANNOUNCE every step through a polite live region: 'Item 3 grabbed, position 3 of 5', 'moved to position 2 of 5', 'dropped at position 2' — without this a screen-reader user has no feedback that anything moved. (4) Move real FOCUS with the item so the keyboard user follows it. (5) Provide a NON-DnD fallback control too where practical (up/down buttons, a 'move to' menu) since keyboard DnD is still cognitively heavy. Also honour prefers-reduced-motion for any drag animation. The senior point: DnD is a pointer gesture; accessibility means offering a parallel, fully-announced keyboard model — not trying to make mouse dragging itself keyboard-driven.

**Use this technique when.** Any sortable list, kanban, or reorder UI; giving drag-and-drop a keyboard + announced alternative.

```jsx
function SortableItem({ label, index, total, onMove, announce }) {
  const [grabbed, setGrabbed] = React.useState(false);
  function onKeyDown(e) {
    if (e.key === ' ' || e.key === 'Enter') {
      e.preventDefault();
      setGrabbed(g => !g);
      announce(grabbed ? 'Dropped at ' + (index + 1) : 'Grabbed, ' + (index + 1) + ' of ' + total);
    } else if (grabbed && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
      e.preventDefault();
      const to = index + (e.key === 'ArrowDown' ? 1 : -1);
      if (to >= 0 && to < total) { onMove(index, to); announce('Moved to ' + (to + 1) + ' of ' + total); }
    } else if (e.key === 'Escape' && grabbed) { setGrabbed(false); announce('Cancelled'); }
  }
  return (
    <li tabIndex={0} aria-describedby="dnd-help" onKeyDown={onKeyDown}
        style={{ outline: grabbed ? '2px solid #2563eb' : undefined }}>
      {label}
    </li>
  );
}
// <p id="dnd-help" class="sr-only">Space to pick up, arrows to move, Space to drop, Escape to cancel</p>
// <div aria-live="polite" class="sr-only">{message}</div>
```

**References.** [W3C · APG Practices (keyboard interaction)](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/) · [MDN · HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)

---

### 20. Testing accessibility: automated vs manual  `Hard`

**Pattern:** Testing

**Problem.** How do you actually verify a feature is accessible? What can automated tools catch, and what can they never catch?

**What it tests.** A realistic testing strategy — knowing automated scans cover only a fraction and what manual testing adds.

**Approach & answer.** A credible strategy layers automated and manual testing, because each catches what the other can't. AUTOMATED tools — axe-core (via jest-axe, Playwright, or the axe DevTools/Lighthouse UIs), or eslint-plugin-jsx-a11y at lint time — are fast, cheap, and belong in CI on every PR. But by axe's own estimate they detect only ~30–50% of WCAG issues: they reliably catch machine-checkable things — missing alt, missing form labels, insufficient color contrast, duplicate ids, invalid ARIA attribute/role combinations, missing document language, empty buttons/links. What they FUNDAMENTALLY CANNOT judge is meaning and experience: whether alt text is actually MEANINGFUL (not just present), whether the focus ORDER is logical, whether a custom widget's keyboard interactions work, whether an announcement makes sense in context, whether focus is managed on route change, whether content is understandable. Those require MANUAL testing: (1) KEYBOARD-only — unplug the mouse and Tab through the whole flow (reach everything, visible focus, logical order, no traps, Escape works). (2) SCREEN READER — exercise the feature with a real one (VoiceOver on macOS/iOS, NVDA or JAWS on Windows), which is the only way to hear what's actually announced. (3) ZOOM/REFLOW to 200%–400% and 320px width, and check with reduced-motion and forced-colors/high-contrast. (4) Include people with disabilities in usability testing where possible — the ground truth. So: automate to catch regressions cheaply and gate PRs, but never treat a green axe run as 'accessible' — the human tests are where real accessibility is confirmed.

**Use this technique when.** Defining an a11y QA process; explaining why a passing axe scan isn't proof of accessibility.

```js
// Automated: cheap regression gate in CI (catches ~30-50% of issues)
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';

test('dialog has no automatically-detectable a11y violations', async () => {
  const { container } = render(<Modal open title="Confirm">Body</Modal>);
  expect(await axe(container)).toHaveNoViolations();
});

// What axe CANNOT verify (must be tested by a human):
//  - is the alt text actually meaningful?
//  - is the focus ORDER logical? does the focus trap work?
//  - does the screen reader announce something that makes sense?
//  - keyboard-only walkthrough, 200-400% zoom, reduced-motion, forced-colors
```

**References.** [Deque · axe-core (what it can/can't test)](https://github.com/dequelabs/axe-core) · [W3C · WAI Easy Checks & evaluation](https://www.w3.org/WAI/test-evaluate/)

---

## Web Performance

> Web performance is where good intentions meet real devices and real networks. These questions walk from the foundations — the critical rendering path, the Core Web Vitals (LCP, INP, CLS), how scripts and CSS block first paint, image and font delivery, caching, compression, and debounce vs. throttle — up through the working techniques teams actually reach for: code-splitting, breaking up long tasks to protect INP, avoiding layout thrash, tree-shaking, and the lab-vs-field distinction; and into the hard end — eliminating layout shift at the source, optimizing LCP by its sub-parts, list virtualization, Web Workers, IntersectionObserver, hunting SPA memory leaks, and using resource hints as a scalpel rather than a blanket. The throughline: measure before you optimize, spend the main thread and the network on the critical path first, reserve space so nothing jumps, and remember every kilobyte of JavaScript is paid for twice — once to download and again to run.

### 1. Critical rendering path: what blocks first paint  `Easy`

**Pattern:** Critical Rendering Path

**Problem.** Walk through what the browser does between receiving HTML and painting the first pixel. What blocks that first paint?

**What it tests.** Whether you understand rendering as a pipeline with specific blocking points, not a black box.

**Approach & answer.** The browser turns bytes into pixels through a pipeline. It parses HTML into the DOM; as it hits <link rel=stylesheet> and <script>, it fetches those. CSS is parsed into the CSSOM. DOM + CSSOM combine into the render tree (only visible nodes, with computed styles). Then layout (a.k.a. reflow) computes the geometry of every box, and paint fills in pixels, composited into layers on screen. Two things block the FIRST paint. (1) CSS is render-blocking: the browser will not paint until the CSSOM is ready, because painting with no styles then restyling would flash unstyled content — so a slow stylesheet in <head> delays everything. (2) A synchronous <script> (no defer/async) is parser-blocking: when the parser reaches it, it stops building the DOM, downloads and executes the script, and only then continues — and because scripts can read styles, a script also waits for any pending CSS above it. The practical levers all target this path: keep critical CSS small and inline it, load non-critical CSS asynchronously, and get scripts off the parser with defer/async. First paint is gated by the slowest render-blocking resource in the <head>, so minimizing that set is the whole game for a fast start.

**Use this technique when.** Diagnosing slow first paint; explaining why a stylesheet or script in <head> delays render.

```html
<!-- Render-blocking: paint waits for this CSS to download + parse -->
<link rel="stylesheet" href="/app.css">

<!-- Parser-blocking: DOM build stops here until the script runs -->
<script src="/app.js"></script>

<!-- Better: inline the critical CSS, defer the rest -->
<style>/* above-the-fold rules only */</style>
<link rel="stylesheet" href="/rest.css" media="print" onload="this.media='all'">
<script src="/app.js" defer></script>
```

**References.** [MDN · Critical rendering path](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path) · [web.dev · Understanding the critical path](https://web.dev/articles/critical-rendering-path)

---

### 2. Core Web Vitals: LCP, INP, CLS  `Easy`

**Pattern:** Core Web Vitals

**Problem.** Name the three Core Web Vitals, what each measures, and the 'good' threshold for each.

**What it tests.** Knowing the user-centric metrics Google standardized on and their target values.

**Approach & answer.** Core Web Vitals are three field metrics, each capturing a distinct part of the experience. LCP — Largest Contentful Paint — measures loading: the time until the largest visible element (usually the hero image or headline block) is rendered. Good is ≤ 2.5s (at the 75th percentile of real users). INP — Interaction to Next Paint — measures responsiveness: across the whole visit it takes the worst (near-worst) latency from a user interaction (tap, click, keypress) to the next frame the browser paints in response. Good is ≤ 200ms. INP replaced FID (First Input Delay) in March 2024 because FID only measured the delay of the FIRST interaction and only its input delay, whereas INP measures every interaction end-to-end. CLS — Cumulative Layout Shift — measures visual stability: a unitless score summing how much visible content unexpectedly jumps around (an image loading with no reserved space, an ad pushing text down). Good is ≤ 0.1. The threshold to remember for each: 2.5s / 200ms / 0.1, all at p75 of real-user data. Each maps to a different fix: LCP → prioritize the hero resource and cut render-blocking; INP → break up long tasks and yield to the main thread; CLS → reserve space for anything that loads or moves.

**Use this technique when.** Setting performance budgets; interpreting a Lighthouse or CrUX report.

```js
// Measure the vitals in real users with the web-vitals library idea,
// or directly with PerformanceObserver:
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // entry.startTime is the LCP render time (ms)
    console.log('LCP candidate:', entry.startTime, entry.element);
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });

// CLS: sum layout-shift entries that weren't caused by recent input
let cls = 0;
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) if (!e.hadRecentInput) cls += e.value;
}).observe({ type: 'layout-shift', buffered: true });
```

**References.** [web.dev · Core Web Vitals](https://web.dev/articles/vitals) · [web.dev · INP](https://web.dev/articles/inp)

---

### 3. Render-blocking scripts: defer vs async  `Easy`

**Pattern:** Script Loading

**Problem.** Compare a plain <script>, one with defer, and one with async. When would you reach for each?

**What it tests.** Understanding how each attribute changes download timing, execution timing, and order.

**Approach & answer.** All three download the same file; they differ in WHEN download happens and WHEN execution happens relative to HTML parsing. A plain <script> is parser-blocking: the parser stops, downloads, executes, then resumes — so it delays DOM construction and first paint, and scripts run in document order. `defer` downloads the script in parallel while parsing continues, then executes it only AFTER the DOM is fully parsed (just before DOMContentLoaded), and deferred scripts run in order. This is the default choice for app code: it never blocks the parser and it can safely touch the DOM because parsing is done. `async` also downloads in parallel, but executes as soon as it arrives — which can be mid-parse — and async scripts run in no guaranteed order (whoever downloads first runs first). Use `async` for independent, order-insensitive scripts that don't touch your DOM: analytics, an ad tag, an isolated widget. Rule of thumb: reach for `defer` for anything that's part of your app or depends on the DOM or on other scripts; reach for `async` for fire-and-forget third parties; avoid plain synchronous scripts in <head> entirely. Note both attributes only apply to external scripts (with src); on an inline script they're ignored. Also: type=module scripts are deferred by default.

**Use this technique when.** Deciding how to include any script; fixing a script that blocks paint or runs before the DOM exists.

```html
<!-- Blocks parsing until downloaded + executed -->
<script src="a.js"></script>

<!-- Downloads in parallel, runs after DOM is parsed, in order. Default for app code. -->
<script src="framework.js" defer></script>
<script src="app.js" defer></script>

<!-- Downloads in parallel, runs ASAP, no order guarantee. For independent 3rd parties. -->
<script src="analytics.js" async></script>
```

**References.** [MDN · <script> defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer) · [web.dev · Efficiently load third-party JS](https://web.dev/articles/efficiently-load-third-party-javascript)

---

### 4. Image optimization essentials  `Easy`

**Pattern:** Images

**Problem.** You have a page dominated by images. What are the highest-leverage optimizations, and how do images cause layout shift?

**What it tests.** Practical image tuning — format, responsive sizing, lazy-loading — and the CLS connection.

**Approach & answer.** Images are usually the largest bytes on a page and the LCP element, so they pay back optimization the most. (1) Format: prefer modern codecs — AVIF, then WebP — which are far smaller than JPEG/PNG at equal quality; serve them with a fallback via <picture>. (2) Responsive sizing: never ship a 2000px image into a 400px slot. Use srcset + sizes so the browser picks a resolution appropriate to the viewport and DPR, so phones don't download desktop-sized files. (3) Lazy-load below-the-fold images with loading="lazy" so off-screen images don't compete for bandwidth during initial load — but NEVER lazy-load the LCP/hero image (that delays your key metric); instead give it fetchpriority="high". (4) Compress appropriately; strip metadata. Now the CLS connection: an <img> with no width/height (and no CSS aspect-ratio) has zero height until the bytes arrive, then suddenly takes up space and shoves everything below it down — a layout shift. ALWAYS set width and height attributes (or aspect-ratio in CSS); modern browsers use them to reserve the correct box before the image loads, even in responsive layouts. So the checklist is: right format, right size, lazy the non-critical ones, prioritize the hero, and always reserve dimensions.

**Use this technique when.** Cutting page weight; fixing a slow LCP or a jumpy layout caused by images.

```html
<!-- Modern format with fallback, responsive sizes, dimensions reserved -->
<picture>
  <source type="image/avif" srcset="hero-480.avif 480w, hero-960.avif 960w">
  <source type="image/webp" srcset="hero-480.webp 480w, hero-960.webp 960w">
  <img src="hero-960.jpg"
       srcset="hero-480.jpg 480w, hero-960.jpg 960w"
       sizes="(max-width: 600px) 480px, 960px"
       width="960" height="540"      <!-- reserves space => no CLS -->
       fetchpriority="high" alt="…"> <!-- hero: do NOT lazy-load -->
</picture>

<!-- Below the fold: lazy-load -->
<img src="thumb.webp" width="200" height="150" loading="lazy" alt="…">
```

**References.** [web.dev · Optimize images](https://web.dev/articles/fast#optimize-your-images) · [MDN · Responsive images](https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images)

---

### 5. Minification, compression, and bundling  `Easy`

**Pattern:** Transfer Size

**Problem.** Distinguish minification, compression (gzip/brotli), and bundling. Do they overlap, and what does each save?

**What it tests.** Separating three often-conflated build/transfer optimizations and what layer each operates at.

**Approach & answer.** They're three different reductions at different layers, and they stack. MINIFICATION rewrites the source itself: strip whitespace and comments, shorten local variable names, drop dead code — producing smaller but still-valid JS/CSS. It happens at build time and reduces the raw bytes before anything else touches them. COMPRESSION is applied by the server over the wire: gzip or (better) Brotli encode the response body, the browser sends Accept-Encoding and decompresses on arrival. Text compresses extremely well — often 70–90% smaller — because code is repetitive; Brotli generally beats gzip, especially with a high static-precompression level for assets you ship repeatedly. Minification and compression are complementary: minify first (removes structure a compressor can't recover), then compress the result. BUNDLING concatenates many modules into fewer files. Its historical win was cutting the number of HTTP requests, which mattered a lot under HTTP/1.1's connection limits; under HTTP/2+ (multiplexed requests) that matters less, so today bundling is more about enabling tree-shaking and avoiding waterfalls than about request count — and you balance it against code-splitting so you don't ship one giant bundle. Net: minify + Brotli are almost always pure wins; bundling is a tuning decision. Don't forget images/fonts are already compressed — re-gzipping them wastes CPU for ~0 gain, so exclude them.

**Use this technique when.** Explaining a build pipeline; deciding what actually shrinks a bundle vs. what's server config.

```text
Source:      function addNumbers(first, second) { return first + second; } // 60B
Minified:    function a(b,c){return b+c}                                    // ~24B
+ Brotli:    (binary, ~repetition-encoded across the whole file)            // often 5-10x smaller

Layers, and where each runs:
  Minification  -> build time,   rewrites the code
  Compression   -> server/CDN,   encodes the response body (gzip/brotli)
  Bundling      -> build time,   fewer files -> enables tree-shaking

Rule: minify THEN compress. Don't gzip already-compressed images/fonts.
```

**References.** [web.dev · Reduce payloads with compression](https://web.dev/articles/reduce-network-payloads-using-text-compression) · [MDN · Content-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding)

---

### 6. Browser caching & Cache-Control  `Easy`

**Pattern:** Caching

**Problem.** How would you cache static assets so repeat visits are fast, without ever serving a stale file after a deploy?

**What it tests.** The hashed-filename + immutable pattern that lets you cache aggressively AND deploy safely.

**Approach & answer.** The tension is: you want long cache lifetimes (so repeat visits skip the network), but you also must guarantee users get the new file after a deploy. The standard solution decouples the two with content hashing. Give each built asset a filename derived from its content — app.9f3a2c.js. Because the name changes only when the content changes, you can serve it with Cache-Control: max-age=31536000, immutable — cache for a year, and 'immutable' tells the browser not to even revalidate on reload. On the next deploy the content changes → the hash changes → the filename changes → it's a cache MISS → the browser fetches the new file. The old cached file is simply never referenced again. The one file you must NOT cache long is the HTML that references these assets, because it carries the current hashed names; serve HTML with a short or no-cache policy (e.g., no-cache, which means 'revalidate every time' via ETag) so a deploy is picked up promptly. Under the hood the mechanics are: max-age sets freshness lifetime; ETag/Last-Modified enable cheap 304 Not Modified revalidation once stale; 'immutable' skips revalidation entirely. So the recipe is: hashed asset names + max-age=1yr, immutable for JS/CSS/images/fonts, and no-cache (or very short max-age) for the HTML entry point.

**Use this technique when.** Configuring a CDN/static host; explaining why users still see old assets after a deploy.

```text
# Hashed asset: safe to cache forever (name changes when content changes)
GET /assets/app.9f3a2c.js
Cache-Control: public, max-age=31536000, immutable

# HTML entry point: must reflect latest hashed names -> revalidate every time
GET /index.html
Cache-Control: no-cache        # store, but revalidate with ETag before use
ETag: "v42"

# Repeat request when still fresh  -> served from cache, no network
# After max-age expires           -> conditional GET, server may reply 304
```

**References.** [MDN · Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) · [web.dev · HTTP caching](https://web.dev/articles/http-cache)

---

### 7. Debounce vs. throttle  `Easy`

**Pattern:** Event Handling

**Problem.** A scroll/resize/input handler is firing far too often and janking the page. When do you debounce and when do you throttle? Implement both.

**What it tests.** Choosing the right rate-limiting strategy and being able to write each from scratch.

**Approach & answer.** Both limit how often an expensive handler runs, but with opposite timing semantics. DEBOUNCE waits for a pause: it fires only after events STOP for N ms, collapsing a burst into one call at the end. Use it when you only care about the final state — a search-as-you-type input (query the API once the user stops typing), resize (relayout once they finish dragging), validating a field after typing ends. THROTTLE guarantees a steady cadence: it fires at most once every N ms DURING a continuous stream, giving you regular updates while the burst is ongoing. Use it when you need periodic feedback mid-stream — scroll position (update a progress bar/parallax as they scroll), mousemove drawing, firing analytics at a bounded rate. Mnemonic: debounce = 'wait until they're done', throttle = 'at most once per interval'. Both slash the number of times your costly work (layout reads, network calls, React state updates) runs, which is often the difference between a smooth 60fps and a janky handler. For scroll specifically, IntersectionObserver or a passive listener + rAF is often better than throttle; but for input, debounce remains the go-to. Watch the leading/trailing edge option — a leading-edge debounce fires immediately then suppresses, which feels more responsive for some UIs.

**Use this technique when.** Rate-limiting input, scroll, resize, or mousemove handlers to stop excessive work.

**Complexity.** O(1) per event; work runs at most once per pause (debounce) or per window (throttle)

```js
function debounce(fn, wait) {
  let t;
  return function (...args) {
    clearTimeout(t);
    t = setTimeout(() => fn.apply(this, args), wait);
  };
}

function throttle(fn, wait) {
  let last = 0, timer;
  return function (...args) {
    const now = Date.now();
    const remaining = wait - (now - last);
    if (remaining <= 0) { last = now; fn.apply(this, args); }
    else { clearTimeout(timer); timer = setTimeout(() => { last = Date.now(); fn.apply(this, args); }, remaining); }
  };
}

// Debounce: one call after the burst ends
const search = debounce((q) => console.log('query:', q), 300);
search('a'); search('ab'); search('abc'); // -> only 'abc' fires, 300ms later

// Throttle: at most one call per window during a burst
const onScroll = throttle((y) => console.log('scrollY:', y), 200);
[0,10,20,30,40].forEach((y, i) => setTimeout(() => onScroll(y), i * 50));
```

**References.** [MDN · Passive event listeners (scroll perf)](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners) · [CSS-Tricks · Debouncing and throttling](https://css-tricks.com/debouncing-throttling-explained-examples/)

---

### 8. Lab vs. field data (Lighthouse vs. RUM)  `Easy`

**Pattern:** Measurement

**Problem.** Lighthouse says your site scores 95, but users complain it's slow. How can both be true?

**What it tests.** Understanding the lab/field distinction and why a lab score isn't the user's experience.

**Approach & answer.** Because Lighthouse and real-user monitoring measure fundamentally different things. Lighthouse is LAB data: a single synthetic run on one machine, under a simulated network/CPU throttle, from your location, with a cold cache and no real interactions. It's reproducible and great for debugging and catching regressions in CI — you control every variable — but it's ONE sample of ONE environment. FIELD data (Real User Monitoring, and Google's CrUX dataset) is collected from actual visitors: every device (including cheap phones), every network (including 3G), every geography, every cache state, and real interaction patterns. So a 95 in the lab and unhappy users are perfectly consistent — your lab machine is a fast laptop on fast wifi near the server; your users might be on mid-tier Android over spotty mobile, far from your origin, hitting slow third-party scripts that only fire on real interaction. Key consequences: (1) Core Web Vitals for ranking/assessment use FIELD data at the 75th percentile, not your lab score. (2) Some metrics essentially don't exist in the lab — INP needs real interactions to measure. (3) The right workflow is both: use lab (Lighthouse/DevTools) to find and fix causes reproducibly, and use field (RUM/CrUX) to know what real users actually experience and to prioritize. Trust the field for 'are users happy'; trust the lab for 'what specifically is slow and did my fix work'.

**Use this technique when.** Reconciling a good Lighthouse score with real complaints; deciding which tool answers which question.

```text
LAB (Lighthouse, DevTools)          FIELD (RUM, CrUX)
--------------------------          -----------------------------
1 synthetic run                     millions of real sessions
your machine + simulated throttle   every device / network / geo
cold cache, no interactions         real caches, real interactions
reproducible -> debug & CI          representative -> "are users happy?"
INP not measurable (no input)       INP measured end-to-end
p ~ single sample                   Core Web Vitals judged at p75

Use BOTH: lab to find & verify fixes, field to measure reality.
```

**References.** [web.dev · Lab and field data](https://web.dev/articles/lab-and-field-data-differences) · [Chrome · CrUX](https://developer.chrome.com/docs/crux)

---

### 9. Code-splitting and lazy-loading routes  `Medium`

**Pattern:** Code Splitting

**Problem.** Your app ships one 800KB JS bundle and time-to-interactive is poor. How does code-splitting help, and where do you split?

**What it tests.** Understanding that you should ship only the code a given screen needs, and the mechanics of doing so.

**Approach & answer.** A single bundle forces every visitor to download, parse, and execute ALL your code before anything is interactive — even code for routes they'll never visit. Parsing and executing JS is CPU work that blocks the main thread, so a big bundle hurts time-to-interactive and INP especially on cheap phones. Code-splitting breaks the bundle into chunks loaded on demand, so the initial download is just what the first screen needs. The natural seams: (1) Per route — the single highest-leverage split. Lazy-load each route's component so visiting /settings fetches settings code only when navigated to. (2) Below-the-fold or interaction-gated components — a heavy chart, a rich editor, a modal — load when they're about to be shown, not on initial load. (3) Large third-party libraries used in one place — dynamically import the date-picker or the markdown renderer at the point of use. The mechanism is the dynamic `import()` expression, which returns a promise and tells the bundler to emit a separate chunk. Frameworks wrap this: React.lazy + Suspense render a fallback while the chunk loads. Two caveats: don't over-split (each chunk is a request and a potential waterfall — splitting a 2KB component is pure overhead), and prefetch likely-next chunks during idle time (e.g., <link rel=prefetch> or on hover) so the lazy load doesn't cost a visible delay when the user actually navigates. The goal is a small, fast initial bundle plus just-in-time loading of the rest.

**Use this technique when.** Cutting initial bundle size; improving TTI/INP on a JS-heavy app.

```jsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Each route becomes its own chunk, fetched only when visited
const Dashboard = lazy(() => import('./routes/Dashboard'));
const Settings  = lazy(() => import('./routes/Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// Interaction-gated: load a heavy editor only when opened
async function openEditor() {
  const { RichEditor } = await import('./RichEditor'); // separate chunk
  mount(RichEditor);
}
```

**References.** [web.dev · Reduce JavaScript payloads with code splitting](https://web.dev/articles/reduce-javascript-payloads-with-code-splitting) · [React · lazy](https://react.dev/reference/react/lazy)

---

### 10. Long tasks and yielding to the main thread  `Medium`

**Pattern:** Main-Thread / INP

**Problem.** What is a 'long task', why does it wreck INP, and how do you break one up so the UI stays responsive?

**What it tests.** Understanding the single main thread, the 50ms long-task threshold, and yielding strategies.

**Approach & answer.** The browser runs your JS, styling, layout, paint, and event handling on ONE main thread. While a task holds that thread, nothing else can happen — a click can't be processed, a frame can't paint. A 'long task' is any task that occupies the main thread for more than 50ms; anything longer and the browser can't respond to input within that window, so a tap that lands during a long task waits for it to finish. That waiting IS the input-delay portion of INP, so a few long tasks (a big JSON parse, an expensive render, a heavy loop) are the classic cause of poor responsiveness even on an otherwise 'fast' page. The fix is to break long work into small chunks and YIELD between them, letting the browser process pending input and paint. Options, roughly in order: (1) scheduler.yield() — the modern primitive; await it to yield and continue after the browser handles higher-priority work. (2) A setTimeout(0) / MessageChannel yield as a fallback. (3) requestIdleCallback for truly non-urgent work, which runs only when the thread is idle. (4) Move the work off-thread entirely to a Web Worker when it's pure computation. The mental model: prefer many short tasks over one long one, and yield right after handling input so the interaction paints quickly, then continue the heavy work. Frameworks add their own scheduling (React's concurrent renderer time-slices for the same reason). Measure long tasks with the Long Tasks API / PerformanceObserver, and target keeping tasks well under 50ms.

**Use this technique when.** Fixing poor INP / janky interactions caused by heavy synchronous JS on the main thread.

```js
// A long loop blocks the main thread -> input can't be handled -> bad INP
async function processAll(items) {
  for (let i = 0; i < items.length; i++) {
    doExpensiveWork(items[i]);

    // Yield periodically so the browser can paint & handle clicks
    if (i % 100 === 0) {
      if ('scheduler' in window && scheduler.yield) {
        await scheduler.yield();                       // modern
      } else {
        await new Promise((r) => setTimeout(r, 0));    // fallback
      }
    }
  }
}

// Detect long tasks in the field
new PerformanceObserver((list) => {
  for (const t of list.getEntries()) {
    if (t.duration > 50) console.warn('long task', t.duration.toFixed(0), 'ms');
  }
}).observe({ type: 'longtask', buffered: true });
```

**References.** [web.dev · Optimize long tasks](https://web.dev/articles/optimize-long-tasks) · [MDN · scheduler.yield()](https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/yield)

---

### 11. Avoiding layout thrash  `Medium`

**Pattern:** Layout / Reflow

**Problem.** This loop that reads offsetHeight and then sets style each iteration is slow. What is 'layout thrashing' and how do you fix it?

**What it tests.** Understanding forced synchronous layout and how interleaved reads/writes trigger it repeatedly.

**Approach & answer.** Layout thrashing is repeatedly forcing the browser to recompute layout inside a loop by interleaving DOM reads and writes. Normally the browser batches style changes and does layout once, lazily, before the next paint. But certain property reads — offsetHeight, offsetTop, getBoundingClientRect(), scrollTop, getComputedStyle, clientWidth, etc. — require an up-to-date layout to answer. If you've written to the DOM since the last layout, reading one of these forces a SYNCHRONOUS layout right now (a 'forced reflow') to flush pending changes. So a loop that goes read → write → read → write invalidates layout on every write and forces a full recompute on every read — O(n) layouts instead of one. The fix is to BATCH: do all your reads first (measure everything), then do all your writes (mutate everything). That way layout is computed at most once for the reads and once (deferred to next frame) for the writes. Concretely: read all offsetHeights into an array, then apply all the new styles. For animations, do the writing inside requestAnimationFrame so it aligns with the frame and reads happen before writes. Libraries formalize this as 'read/write phases' (e.g., FastDOM). Also prefer properties that don't trigger layout at all when animating — transform and opacity are composited and skip layout/paint — over animating top/left/width/height which reflow every frame. The signature symptom in DevTools is a 'Forced reflow' warning or a Performance panel full of purple layout bars inside a script call.

**Use this technique when.** Fixing a slow loop that measures and mutates the DOM; smoothing scroll/resize handlers.

**Complexity.** Batching turns O(n) forced layouts into O(1)

```js
// BAD: read forces layout, write invalidates it -> reflow every iteration
boxes.forEach((box) => {
  const h = box.offsetHeight;      // READ -> forced synchronous layout
  box.style.height = h * 2 + 'px'; // WRITE -> invalidates layout
});

// GOOD: batch all reads, then all writes -> layout computed once
const heights = boxes.map((box) => box.offsetHeight); // all READS
boxes.forEach((box, i) => {                            // all WRITES
  box.style.height = heights[i] * 2 + 'px';
});

// Animate composited props (no layout/paint) instead of top/left/width:
el.style.transform = 'translateX(100px)'; // GPU, skips layout
// el.style.left = '100px';               // reflows every frame
```

**References.** [web.dev · Avoid large, complex layouts and layout thrashing](https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing) · [MDN · Reflow / forced synchronous layout](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/How_browsers_work)

---

### 12. Font loading: FOIT, FOUT, and font-display  `Medium`

**Pattern:** Fonts

**Problem.** Text is invisible for a second while a web font loads, then it pops in. What's happening and how do you control it?

**What it tests.** Understanding the invisible-text problem, the font-display swap period, and preloading.

**Approach & answer.** When text uses a web font that hasn't downloaded yet, the browser must decide what to show meanwhile, and the default is bad for perceived performance. FOIT — Flash Of Invisible Text — is when the browser hides the text entirely while waiting for the font (historically up to 3s), so the user stares at blank space; if the font is your LCP text, this delays LCP directly. FOUT — Flash Of Unstyled Text — is when the browser shows a fallback system font immediately, then swaps to the web font when it arrives, causing a visible reflow/restyle. The `font-display` descriptor in @font-face lets you choose the tradeoff: `swap` shows the fallback immediately and swaps in the web font whenever it loads (favor content visibility, accept the swap — good default for body text); `optional` gives a tiny block period and, if the font isn't cached/fast, just keeps the fallback for this visit (best for CLS/LCP, the font may not appear at all first load); `fallback` is a compromise; `block` is the FOIT behavior. Beyond font-display: (1) `<link rel=preload as=font crossorigin>` the critical font so it starts downloading early instead of being discovered late (fonts are referenced from CSS, so they're found only after CSSOM). (2) Self-host and subset the font to the glyphs you use to cut bytes. (3) Use WOFF2. (4) Reduce the swap's layout shift by choosing a fallback with similar metrics (size-adjust / ascent-override, or the 'f-mods'). The combination — preload + font-display: swap (or optional) + WOFF2 subset — gives fast visible text with minimal shift.

**Use this technique when.** Fixing invisible or shifting text during load; tuning web-font delivery.

```html
<!-- Discover the critical font early instead of after CSS parses -->
<link rel="preload" href="/fonts/inter.woff2" as="font"
      type="font/woff2" crossorigin>

<style>
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;   /* show fallback now, swap when ready (no FOIT) */
  /* Reduce the swap's layout shift by matching fallback metrics: */
  size-adjust: 105%;
  ascent-override: 90%;
}
body { font-family: 'Inter', system-ui, sans-serif; }
</style>
```

**References.** [web.dev · Prevent invisible text with font-display](https://web.dev/articles/avoid-invisible-text) · [MDN · font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)

---

### 13. Tree-shaking and shipping less JavaScript  `Medium`

**Pattern:** Tree-shaking

**Problem.** You import one function from a utility library but the whole thing ends up in your bundle. Why, and how does tree-shaking prevent it?

**What it tests.** Understanding dead-code elimination, why it depends on ESM and side effects, and common defeats.

**Approach & answer.** Tree-shaking is dead-code elimination for JavaScript modules: the bundler keeps only the exports you actually use and drops the rest. It relies on ES modules (import/export) being STATICALLY analyzable — the bundler can see at build time exactly which named exports are reached from your entry point and prune everything unreachable. It fundamentally cannot work the same way on CommonJS (require) because require is dynamic (you can require a computed string, reassign exports), so a CJS-only library often pulls in wholesale. So the first reason 'one function drags in the whole library' is that the library ships CommonJS, or you imported it in a way that isn't shakeable. The second reason is SIDE EFFECTS: if a module runs code at import time (patches a global, registers something), the bundler must keep it even if you use none of its exports — unless the package declares `"sideEffects": false` (or lists the few files that do have them) in package.json, which grants the bundler permission to drop unused modules. Practical rules: (1) prefer libraries that ship ESM and are marked side-effect-free; (2) import only what you need with named imports (import { debounce } from 'lib') rather than a namespace import of everything, and avoid re-export barrel files that accidentally force-load siblings; (3) make sure minification/production mode is on, since tree-shaking's removal is finalized by the minifier; (4) verify with a bundle analyzer — the honest measure is what actually ends up in the output, not what you intended. The payoff is fewer bytes to download AND less JS to parse/execute, which helps TTI and INP.

**Use this technique when.** Explaining why a bundle is bigger than expected; choosing/importing libraries for shakeability.

```js
// Shakeable: named import from an ESM, side-effect-free library
import { debounce } from 'lodash-es';   // only debounce (+deps) is bundled

// Often NOT shakeable: default/namespace import pulls the whole thing
import _ from 'lodash';                  // entire library
_.debounce(fn, 200);

// package.json of a library declaring it's safe to drop unused modules:
// {
//   "sideEffects": false,        // or ["./src/polyfill.js"]
//   "module": "dist/index.esm.js" // ships an ESM build
// }
```

**References.** [MDN · Tree shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking) · [web.dev · Remove unused code](https://web.dev/articles/remove-unused-code)

---

### 14. Eliminating layout shift at the source  `Hard`

**Pattern:** Layout Stability (CLS)

**Problem.** CLS is 0.35 and content jumps as the page loads. Enumerate the root causes and the fix for each.

**What it tests.** A systematic account of what shifts layout and how to reserve space or avoid the shift entirely.

**Approach & answer.** CLS accumulates every time visible content moves without a user action; each shift is scored by how much of the viewport moved and how far. The causes are a short, fixable list. (1) Media without dimensions — images, videos, iframes, embeds that have no reserved box collapse to zero then push content down when they load. Fix: set width/height attributes or CSS aspect-ratio so the box is reserved before bytes arrive. (2) Web fonts — the swap from fallback to web font changes text metrics and reflows. Fix: font-display: optional, or match fallback metrics with size-adjust/ascent-override, and preload the font. (3) Dynamically injected content above existing content — banners, cookie notices, ad slots, 'you have new messages' bars inserted at the top shove everything down. Fix: reserve space for known slots (min-height on the ad container), or inject below the fold / overlay instead of in-flow. (4) Actions that resize without space reserved — an accordion or 'read more' that expands is user-initiated (within 500ms of input it's excluded from CLS), but async expansions are not. (5) Animating layout properties — animating top/left/height moves surrounding content; animate transform/opacity instead (composited, no reflow, no shift). (6) Late-arriving data that changes sizes — skeletons should match the final content's dimensions so the real content drops in without resizing. The unifying principle: reserve the final space up front and never insert or grow in-flow content above what the user is looking at. Measure which element shifted using the LayoutShift entries' `sources` in DevTools to target the actual culprit rather than guessing.

**Use this technique when.** Driving CLS below 0.1; auditing a page that visibly jumps during load.

```html
<!-- Reserve the media box so nothing jumps when it loads -->
<img src="hero.webp" width="800" height="450" alt="…">
<div style="aspect-ratio: 16/9;"><iframe src="…"></iframe></div>

<!-- Reserve space for an async ad/slot instead of letting it push content -->
<div class="ad-slot" style="min-height:250px"></div>

<style>
  /* Animate composited props: moves the element, not its neighbors */
  .toast { transition: transform .2s, opacity .2s; }
  /* NOT: top / height / margin, which reflow surrounding content */
</style>
```

**References.** [web.dev · Optimize CLS](https://web.dev/articles/optimize-cls) · [web.dev · Debug layout shifts](https://web.dev/articles/debug-layout-shifts)

---

### 15. Optimizing LCP end-to-end  `Hard`

**Pattern:** Loading (LCP)

**Problem.** LCP is 4.5s. Break the LCP timeline into its sub-parts and give the optimization for each.

**What it tests.** Decomposing LCP into TTFB, load delay, load time, and render delay — and fixing the dominant part.

**Approach & answer.** LCP has four sequential sub-parts, and you optimize whichever dominates — guessing wastes effort. (1) TTFB (time to first byte): how long until the HTML starts arriving. Slow TTFB caps everything downstream. Fixes: CDN/edge caching, faster server or SSR caching, fewer redirects, early hints (103). (2) Resource load delay: the gap between TTFB and when the LCP resource STARTS downloading — usually because the browser discovered it late (it's in CSS as a background-image, or injected by JS, or behind render-blocking resources) or it was deprioritized. Fixes: make the LCP image a real <img> (discoverable by the preload scanner), preload it, set fetchpriority="high", and don't lazy-load it. (3) Resource load time: how long the LCP resource itself takes to download. Fixes: compress and right-size the image (AVIF/WebP, correct dimensions), preconnect to its origin. (4) Element render delay: the gap between the resource finishing and it actually painting — caused by render-blocking CSS/JS still pending, or the element waiting on the framework to hydrate/mount. Fixes: cut render-blocking resources (inline critical CSS, defer JS), reduce hydration cost, avoid client-side-only rendering of the hero. The workflow: in DevTools or the web-vitals attribution build, read the four sub-parts, find the biggest, and target it. Commonly the win is (2)+(3): the hero image is discovered late and unoptimized — so preload it, mark it high priority, serve a modern compressed format at the right size, and ensure nothing render-blocking sits in front of it. Also confirm the LCP element is what you think; sometimes it's a large text block gated by a web font (→ font optimization) rather than an image.

**Use this technique when.** Systematically driving down a slow LCP; deciding whether the bottleneck is server, network, or render.

```html
<!-- The 4 LCP sub-parts: TTFB | load delay | load time | render delay -->

<!-- Cut load delay: warm the connection + start the hero early -->
<link rel="preconnect" href="https://img.cdn.example">
<link rel="preload" as="image" href="https://img.cdn.example/hero.avif"
      fetchpriority="high">

<!-- Cut load time + delay: real <img>, high priority, NOT lazy, right size -->
<img src="https://img.cdn.example/hero.avif"
     width="1200" height="675" fetchpriority="high" alt="…">

<!-- Cut render delay: inline critical CSS, defer the rest of JS -->
<style>/* above-the-fold rules */</style>
<script src="/app.js" defer></script>
```

**References.** [web.dev · Optimize LCP](https://web.dev/articles/optimize-lcp) · [web.dev · LCP breakdown](https://web.dev/articles/optimize-lcp#lcp-breakdown)

---

### 16. Virtualizing long lists (windowing)  `Hard`

**Pattern:** List Virtualization

**Problem.** Rendering 10,000 rows freezes the page. Explain windowing, what it costs, and the tricky parts.

**What it tests.** Understanding why huge DOMs are slow and how to render only what's visible while preserving scroll.

**Approach & answer.** Rendering 10,000 rows creates 10,000+ DOM nodes: that's slow to build, expensive in memory, and every style/layout pass has to consider all of them — so initial render, scrolling, and updates all jank. Windowing (virtualization) renders only the rows currently in (or near) the viewport — maybe 20 — plus a small overscan buffer, and recycles them as you scroll. The core mechanics: (1) A tall spacer establishes the full scrollable height so the scrollbar behaves as if all rows exist (height = rowCount × rowHeight for fixed rows). (2) On scroll, compute which index range is visible from scrollTop and rowHeight, and render only that slice, absolutely positioned (or offset with translateY/padding) at the right place. (3) As the user scrolls, the visible slice shifts and you render different rows into roughly the same handful of nodes. The costs and tricky parts: FIXED-height rows are easy; VARIABLE heights require either measuring rows and caching their offsets (a prefix-sum you update as measurements come in) or estimating then correcting, which can cause scroll jumps if estimates are off. Accessibility and find-in-page break because off-screen rows aren't in the DOM — mitigate with appropriate ARIA (row/rowcount) and by not virtualizing when the list is short. Anchoring/jumpiness on prepend needs scroll-position compensation. Sticky headers, keyboard focus on a recycled node, and horizontal + vertical grids add complexity. In practice you reach for a battle-tested library (react-window / TanStack Virtual) rather than hand-rolling, but you must understand the model to debug it. An adjacent modern option for pure show/hide cost is CSS `content-visibility: auto` with `contain-intrinsic-size`, which lets the browser skip rendering off-screen subtrees while keeping them in the DOM — cheaper to adopt, though it doesn't cut node count or memory the way true windowing does.

**Use this technique when.** Rendering very long lists/tables/feeds without freezing; choosing windowing vs. content-visibility.

**Complexity.** Renders O(visible) nodes instead of O(total); scroll math O(1) for fixed-height rows

```jsx
function VirtualList({ items, rowHeight = 40, height = 400, overscan = 5 }) {
  const [scrollTop, setScrollTop] = React.useState(0);
  const total = items.length * rowHeight;

  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const visible = Math.ceil(height / rowHeight) + 2 * overscan;
  const end = Math.min(items.length, start + visible);
  const slice = items.slice(start, end);

  return (
    <div style={{ height, overflow: 'auto' }}
         onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}>
      {/* spacer gives the real scroll height */}
      <div style={{ height: total, position: 'relative' }}>
        {slice.map((item, i) => (
          <div key={start + i}
               style={{ position: 'absolute', top: (start + i) * rowHeight,
                        height: rowHeight, width: '100%' }}>
            {item.label}
          </div>
        ))}
      </div>
    </div>
  );
}
```

**References.** [web.dev · Virtualize large lists](https://web.dev/articles/virtualize-long-lists-react-window) · [web.dev · content-visibility](https://web.dev/articles/content-visibility)

---

### 17. Offloading work to a Web Worker  `Hard`

**Pattern:** Web Workers

**Problem.** A CPU-heavy computation freezes the UI. When does a Web Worker help, what are its constraints, and how do you communicate with it?

**What it tests.** Knowing workers run off the main thread, their isolation model, messaging cost, and structured clone.

**Approach & answer.** A Web Worker runs JavaScript on a SEPARATE thread, so long computation there doesn't block the main thread that handles input, layout, and paint — the fix for UI freezes caused by pure CPU work (parsing big files, image processing, crypto, heavy data transforms, running a WASM module). When it helps: the work is CPU-bound and self-contained. When it does NOT help: work that's I/O-bound (already async on the main thread) or that must touch the DOM — workers have NO access to the DOM, window, or most page APIs; they get their own global (self), plus fetch, timers, IndexedDB, WebSockets, and importScripts/ESM. Communication is by message passing: postMessage on one side, onmessage on the other. Crucially the data is COPIED via the structured clone algorithm, not shared — so sending a huge object has a real serialization cost that can eat the savings. Two escape hatches: (1) Transferable objects — pass an ArrayBuffer (or OffscreenCanvas, etc.) with a transfer list so ownership MOVES to the worker with zero copy (the sender can no longer use it). (2) SharedArrayBuffer for genuinely shared memory across threads (gated behind cross-origin isolation / COOP+COEP headers, and you must coordinate with Atomics). Practical patterns: keep messages coarse (batch work, don't chat per item), consider a worker pool for parallelism across cores, and use a small RPC/comlink-style wrapper so the async boundary reads like normal calls. Also note OffscreenCanvas lets you do rendering work in a worker. The mental test: 'is this pure computation that's janking the thread, and can I ship its inputs/outputs cheaply?' — if yes, a worker; if it needs the DOM or the transfer cost dominates, keep it on-thread and instead break it into yielding chunks.

**Use this technique when.** Moving CPU-bound work off the main thread to keep the UI responsive; deciding worker vs. yielding.

```js
// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });

worker.onmessage = (e) => console.log('result:', e.data);

// Zero-copy transfer of a big buffer (ownership moves to the worker)
const buf = new ArrayBuffer(50 * 1024 * 1024);
worker.postMessage({ cmd: 'process', buf }, [buf]); // buf now unusable here

// worker.js  (no DOM/window here; own global 'self')
self.onmessage = (e) => {
  const { cmd, buf } = e.data;
  if (cmd === 'process') {
    const view = new Uint8Array(buf);
    let sum = 0;
    for (let i = 0; i < view.length; i++) sum += view[i]; // heavy work, off-thread
    self.postMessage(sum);
  }
};
```

**References.** [MDN · Using Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) · [MDN · Transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)

---

### 18. IntersectionObserver for lazy work and infinite scroll  `Hard`

**Pattern:** IntersectionObserver

**Problem.** Why is IntersectionObserver better than scroll listeners for lazy-loading and infinite scroll? Implement an infinite-scroll sentinel.

**What it tests.** Understanding async, off-main-thread visibility detection vs. throttled scroll + getBoundingClientRect.

**Approach & answer.** The old way — a scroll listener that calls getBoundingClientRect() on candidate elements to see if they're near the viewport — has two problems: scroll fires very frequently (so you must throttle and still do work often), and getBoundingClientRect forces a synchronous layout each call, so you're doing layout-thrashing work on the hot scroll path. IntersectionObserver solves both: you register elements and the browser tells you ASYNCHRONOUSLY, off the main thread, when their intersection with a root (the viewport or a scroll container) crosses thresholds you specify — no polling, no forced layout, no scroll handler. It's the right tool for: lazy-loading images/components as they approach the viewport, firing analytics when a section becomes visible, pausing offscreen work, and infinite scroll. Key options: `root` (the scroll container, default viewport), `rootMargin` (grow the root's box so you can start loading BEFORE the element is actually visible — e.g., '200px' preloads just off-screen), and `threshold` (fire at 0%, 50%, fully visible, etc.). For infinite scroll the clean pattern is a SENTINEL: an empty element after the last item; when it intersects (with a rootMargin so it triggers a bit early), fetch the next page and append — then the sentinel moves down and re-triggers. This avoids attaching/detaching scroll math and naturally batches. Gotchas: unobserve/disconnect when done to avoid leaks and duplicate fetches; guard against firing while a fetch is already in flight; and note it reports visibility, not pixels-scrolled, so for a scroll-progress bar you still want rAF + scroll. Native `loading=lazy` on <img>/<iframe> covers the image case without any JS, so reserve IntersectionObserver for components, analytics, and pagination.

**Use this technique when.** Lazy-loading components, visibility analytics, or infinite scroll without janky scroll handlers.

**Complexity.** No per-scroll work; callbacks fire only on threshold crossings, off the main thread

```js
// Infinite scroll via a sentinel element after the list
const sentinel = document.querySelector('#load-more');
let loading = false;

const io = new IntersectionObserver(async (entries) => {
  const entry = entries[0];
  if (!entry.isIntersecting || loading) return;
  loading = true;
  const page = await fetchNextPage();     // append rows
  render(page);
  loading = false;
  if (page.isLast) io.disconnect();       // stop observing when done
}, {
  root: null,            // viewport
  rootMargin: '400px',   // start loading before the sentinel is visible
  threshold: 0,
});

io.observe(sentinel);
```

**References.** [MDN · Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) · [web.dev · Lazy-loading with IntersectionObserver](https://web.dev/articles/lazy-loading-images)

---

### 19. Finding and fixing memory leaks in SPAs  `Hard`

**Pattern:** Memory Leaks

**Problem.** A single-page app gets slower the longer it runs and eventually crashes a tab. What causes leaks in SPAs and how do you find them?

**What it tests.** Knowing the common retain-paths in long-lived JS apps and the heap-snapshot workflow to locate them.

**Approach & answer.** In an SPA the page never fully reloads, so anything you fail to release accumulates across navigations until the tab bloats and GCs constantly (jank) or crashes. A memory leak in JS means objects are still REACHABLE from a root (so the garbage collector can't free them) even though your code will never use them again. The usual retain-paths: (1) Event listeners not removed — addEventListener on window/document/a shared bus from a component that later unmounts keeps the handler (and its closed-over component state and DOM) alive. Remove them on teardown (or use AbortController's signal). (2) Timers/intervals — a setInterval never cleared keeps its callback and closure forever. (3) Detached DOM nodes — you removed a node from the document but still hold a reference to it (in an array, a closure, a cache), so the whole subtree is retained. (4) Closures capturing large objects — a long-lived callback that closes over a big array pins it. (5) Growing caches/maps keyed by things that never get evicted — use WeakMap/WeakRef so entries can be collected when the key is gone, or bound the cache (LRU). (6) Framework-specific: subscriptions/observables/stores not unsubscribed on unmount. Finding them: in DevTools Memory panel, take a heap snapshot, exercise the suspected flow (navigate in and out several times), take another snapshot, and use the 'Comparison' view to see what grew; sort by retained size and look at the retainers path to find WHO is holding the object — detached nodes show up flagged. The Performance panel's memory timeline showing a sawtooth that trends UP across repeated actions is the signature. The 'take 3 snapshots' technique (baseline → do action → snapshot → undo action → snapshot) isolates objects that should have been freed but weren't. Fix is almost always: pair every subscribe/addListener/setInterval/retain with its teardown in the component's cleanup.

**Use this technique when.** Diagnosing an SPA that degrades over time; auditing cleanup in components with subscriptions/timers.

```jsx
// LEAK: listener + interval survive unmount, pinning state and DOM
useEffect(() => {
  window.addEventListener('resize', onResize);
  const id = setInterval(poll, 1000);
  bus.subscribe(onEvent);
  // no cleanup -> accumulates on every mount/unmount
});

// FIXED: tear everything down; AbortController removes all its listeners at once
useEffect(() => {
  const ctrl = new AbortController();
  window.addEventListener('resize', onResize, { signal: ctrl.signal });
  const id = setInterval(poll, 1000);
  const unsub = bus.subscribe(onEvent);
  return () => { ctrl.abort(); clearInterval(id); unsub(); };
}, []);

// Cache that lets entries be GC'd when the key object is gone
const cache = new WeakMap(); // vs. Map, which retains keys forever
```

**References.** [Chrome · Fix memory problems](https://developer.chrome.com/docs/devtools/memory-problems) · [MDN · Memory management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management)

---

### 20. Resource hints and priority  `Hard`

**Pattern:** Resource Hints

**Problem.** Distinguish preconnect, dns-prefetch, preload, prefetch, and fetchpriority. When does each help, and how can they hurt?

**What it tests.** Precise understanding of each hint's effect on discovery/connection/priority and the cost of misuse.

**Approach & answer.** These control WHEN and at WHAT PRIORITY the browser does network work, filling gaps the default discovery order leaves. `dns-prefetch` resolves a domain's DNS ahead of time — cheap, good for origins you'll likely use but not immediately. `preconnect` goes further: DNS + TCP + TLS handshake, so when the real request fires the connection is already warm — use for a few critical cross-origin origins (your image CDN, fonts host, API), but each open connection has cost so limit it to 2–4 and only ones you'll use very soon (an unused preconnect wastes a connection). `preload` (<link rel=preload as=...>) tells the browser to fetch a resource NOW at high priority that it would otherwise discover late — the classic use is a font (referenced from CSS, found late) or the LCP image or a critical script; you MUST set `as` correctly (and crossorigin for fonts) or the browser can't match it and may double-fetch. `prefetch` is the opposite intent: fetch at LOW priority something for a FUTURE navigation (the next likely route's chunk), stored for later — great for perceived-instant navigations, done during idle. `fetchpriority` (high/low/auto) tunes the relative priority of a resource the browser already knows about — e.g., fetchpriority="high" on the LCP <img> to jump it ahead of other images, or low on things that can wait. How they hurt: preload/preconnect are bandwidth and connection you spend UP FRONT, competing with genuinely critical resources — preloading too much (or the wrong thing) DELAYS your LCP by contending for the pipe; an unused preload is flagged by the browser as wasted; over-preconnecting opens idle connections. So the discipline is: hint only the few resources on the critical path, verify with DevTools' priority column and the 'preload not used' warnings, and prefer the native signals (fetchpriority on the hero image, defer on scripts) before scattering <link> hints. Think of it as a scalpel for the critical path, not a blanket 'load everything sooner'.

**Use this technique when.** Shaving the critical path with the right hint; auditing hints that hurt by over-fetching.

```html
<!-- Warm a connection you'll use very soon (limit to a few) -->
<link rel="preconnect" href="https://cdn.example" crossorigin>
<link rel="dns-prefetch" href="https://cdn.example">  <!-- cheaper, weaker -->

<!-- Fetch NOW at high priority something discovered late (font, LCP img) -->
<link rel="preload" as="font" type="font/woff2"
      href="/fonts/inter.woff2" crossorigin>

<!-- Low-priority fetch for the NEXT navigation, during idle -->
<link rel="prefetch" href="/routes/settings.[hash].js">

<!-- Reprioritize a known resource -->
<img src="hero.avif" fetchpriority="high" width="1200" height="675" alt="…">
<img src="below.avif" fetchpriority="low" loading="lazy" alt="…">
```

**References.** [web.dev · Preload critical assets & establish connections](https://web.dev/articles/preload-critical-assets) · [MDN · rel=preload](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload)

---

## Testing

> Tests are how you buy the confidence to change code without fear. The craft is less about knowing a framework's API and more about judgment: what to test, at which layer, and — just as importantly — what to leave alone. Good tests describe behavior the way a user experiences it, so they survive refactors and fail only when something that matters actually breaks; bad tests couple to internals, rot into noise, and get updated on reflex until they prove nothing. This section works up from the fundamentals — the pyramid, the anatomy of a solid unit test, test doubles, and what coverage really means — through practical component and async testing with Testing Library, and into the hard, opinion-shaped questions: eliminating flakiness, choosing when end-to-end is worth its cost, testing accessibility, taming snapshots and visual diffs, making a slow suite fast, and spending limited testing effort where the return is highest. The recurring theme: the more your tests resemble how the software is actually used, the more confidence they give you — and the less they cost you every time you change the code.

### 1. The testing pyramid  `Easy`

**Pattern:** Test Strategy

**Problem.** Explain the testing pyramid. What goes in each layer, and why is the shape a pyramid rather than a rectangle?

**What it tests.** Whether you understand the cost/speed/confidence tradeoff across test types and how to balance them.

**Approach & answer.** The testing pyramid is a heuristic for how to distribute tests across three layers by cost and speed. At the wide BASE are UNIT tests: they exercise a single function/module in isolation, run in milliseconds, need no browser or network, and pinpoint failures precisely. You have the most of these because they're cheap to write and run and give fast feedback. The MIDDLE is INTEGRATION tests: several units working together — a component with its hooks and store, a service with a real (or in-memory) database, a form that validates and submits. They catch the bugs unit tests miss (wrong wiring between correct parts) but are slower and a bit more brittle, so you have fewer. The narrow TOP is END-TO-END tests: drive the whole app through a real browser like a user (Playwright/Cypress) — click, type, assert on rendered UI. They give the highest confidence that the system actually works, but they're the slowest, flakiest, and most expensive to maintain, so you keep only a handful covering critical user journeys (login, checkout). The shape is a pyramid, not a rectangle, precisely because of that gradient: push as much coverage as low as possible where tests are fast, cheap, and stable, and reserve the expensive high-confidence tests for the few flows that most need them. The anti-pattern is the 'ice-cream cone' (mostly slow e2e, few unit) — a slow, flaky suite. A common modern refinement is the 'testing trophy', which fattens the integration layer because that's where user-facing confidence per dollar is often highest for UI apps.

**Use this technique when.** Deciding how many of each test type to write; diagnosing a slow, flaky suite.

```text
        /\        E2E        few, slow, high-confidence, flaky
       /--\                   (Playwright/Cypress: real browser)
      /----\     Integration  some, moderate speed
     /------\                 (components+hooks, service+db)
    /--------\   Unit         many, fast, precise, cheap
   /----------\               (one function/module in isolation)

Push coverage DOWN where tests are fast & stable.
Anti-pattern: the "ice-cream cone" (mostly slow E2E).
```

**References.** [Martin Fowler · The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html) · [Testing Library · Guiding Principles](https://testing-library.com/docs/guiding-principles/)

---

### 2. What makes a good unit test  `Easy`

**Pattern:** Unit Test Quality

**Problem.** What properties distinguish a good unit test from a bad one? What is the AAA structure?

**What it tests.** Recognizing the qualities — focused, deterministic, isolated, readable — that make tests worth having.

**Approach & answer.** A good unit test is FAST, ISOLATED, DETERMINISTIC, and tests ONE behavior with a clear name. Fast: it runs in milliseconds with no network, filesystem, or timers, so you can run thousands on every save. Isolated: it doesn't depend on other tests, execution order, or shared mutable state — each test sets up and tears down its own world, so a failure points at one thing. Deterministic: same input, same result, every run — no reliance on the current time, random values, network, or race conditions (those cause flakiness, the thing that destroys trust in a suite). Focused: it asserts one behavior, so its name can say exactly what broke ('returns 0 for an empty cart') and a failure is diagnostic rather than 'something in this 200-line test'. Readable: the test doubles as documentation of what the code should do. The AAA structure organizes each test into three visual phases: ARRANGE (set up inputs, state, and any doubles), ACT (invoke the one thing under test), ASSERT (check the outcome). Keeping those phases distinct — and having a single Act — keeps tests honest: multiple Acts usually means you're testing multiple behaviors and should split. Good tests also avoid logic (loops/conditionals in the test are a smell — they can hide bugs in the test itself) and test PUBLIC behavior, not private internals, so they don't break on every refactor. The payoff of these properties compounds: a fast, deterministic, well-named suite is one people actually run and trust; a slow, flaky, tangled one gets ignored or deleted.

**Use this technique when.** Reviewing test quality; structuring a new test; explaining why a test is brittle.

```js
// AAA: Arrange, Act, Assert — one behavior, clear name
test('applies a 10% discount to orders over $100', () => {
  // Arrange
  const cart = { subtotal: 150 };

  // Act
  const total = applyDiscount(cart);

  // Assert
  expect(total).toBe(135);
});

// Smell: two Acts / two behaviors in one test -> split it.
// Smell: relies on Date.now(), Math.random(), or a previous test's state.
```

**References.** [Testing Library · Guiding Principles](https://testing-library.com/docs/guiding-principles/) · [Kent C. Dodds · Write tests. Not too many. Mostly integration.](https://kentcdodds.com/blog/write-tests)

---

### 3. Testing behavior, not implementation  `Easy`

**Pattern:** Behavior vs. Implementation

**Problem.** What does it mean to test behavior rather than implementation details? Why does it matter?

**What it tests.** The single most important habit for tests that survive refactors and actually catch regressions.

**Approach & answer.** Testing BEHAVIOR means asserting on what the code does from the outside — its observable outputs and effects for given inputs — rather than HOW it does it internally. Implementation details are the internals a consumer shouldn't care about: private methods, internal state variable names, which helper was called, the exact DOM structure or CSS class, how many times a function ran. A behavior test says 'given this input, the user sees this result'; an implementation test says 'the component called setState twice and has a state field named _count'. Why it matters: tests coupled to implementation break when you REFACTOR — change the internals while keeping behavior identical and the test fails, even though nothing a user cares about changed. That's a false alarm that trains people to distrust and ignore the suite, and it makes refactoring painful, discouraging the very cleanup that keeps code healthy. Worse, implementation tests can PASS while the feature is broken (you asserted a method was called, but its result was wrong). The rule of thumb: 'the more your tests resemble the way your software is used, the more confidence they give you' — so query the UI the way a user does (by role, label, visible text), assert on rendered output and side effects, and avoid reaching into private state or spying on internal calls unless the call itself IS the contract (e.g., 'it must call the payment API exactly once'). A practical test: if I rewrite the internals but keep the same public behavior, should this test still pass? If yes, it's a behavior test; if it would break, it's testing implementation.

**Use this technique when.** Deciding what to assert; explaining why a test broke on a refactor that changed no behavior.

```jsx
// Implementation detail: couples to internal state -> breaks on refactor
expect(wrapper.state('isOpen')).toBe(true);
expect(instance._handleClick).toHaveBeenCalled();

// Behavior: what the user actually observes -> survives refactors
await user.click(screen.getByRole('button', { name: /open menu/i }));
expect(screen.getByRole('menu')).toBeVisible();

// Ask: if I rewrite the internals but keep behavior,
// should this test still pass? If no -> it's testing implementation.
```

**References.** [Testing Library · Guiding Principles](https://testing-library.com/docs/guiding-principles/) · [Kent C. Dodds · Testing Implementation Details](https://kentcdodds.com/blog/testing-implementation-details)

---

### 4. Assertions and matchers: toBe vs. toEqual  `Easy`

**Pattern:** Assertions

**Problem.** What's the difference between toBe and toEqual? When do you reach for each, and what other matchers matter?

**What it tests.** Understanding reference vs. structural equality in assertions — a classic source of confusing failures.

**Approach & answer.** toBe checks REFERENCE / primitive identity — it's Object.is, essentially ===. Use it for primitives (numbers, strings, booleans) and when you specifically want to assert two variables point to the SAME object instance. toEqual checks STRUCTURAL / deep equality — it recursively compares the contents of objects and arrays, so two different objects with the same shape and values pass. The classic bug: expect({a:1}).toBe({a:1}) FAILS because they're two distinct objects with different references, even though their contents match; you wanted toEqual. Conversely, using toEqual where you meant to assert identity can hide a bug where a function returned a fresh copy instead of the same reference. A few related nuances: toEqual ignores undefined properties and array holes; toStrictEqual is stricter (checks undefined props and that types/classes match), useful when the exact shape matters. Beyond equality, the matchers worth knowing keep tests readable and failures diagnostic: toContain (array/string membership), toMatch (regex on strings), toThrow (a function throws, optionally matching a message), toHaveBeenCalledWith (a mock/spy was called with given args), toBeCloseTo (floating-point comparison, since 0.1+0.2 !== 0.3), and truthiness helpers toBeTruthy/toBeNull/toBeDefined. For DOM there are jest-dom matchers like toBeVisible/toHaveTextContent that produce far better failure messages than poking at properties. Choosing the RIGHT matcher matters beyond correctness: a precise matcher gives a precise failure message ('expected 3 to be 4', not 'expected true to be false'), which is half the value of a test.

**Use this technique when.** Choosing the correct assertion; debugging a 'they look equal but toBe fails' surprise.

```js
expect(2 + 2).toBe(4);              // primitives: reference/=== is fine

expect({ a: 1 }).toBe({ a: 1 });   // FAILS: different object references
expect({ a: 1 }).toEqual({ a: 1 });// passes: deep structural equality

expect([1, 2, 3]).toContain(2);
expect(() => parse('')).toThrow(/empty/);
expect(0.1 + 0.2).toBeCloseTo(0.3); // floats: never toBe(0.3)
expect(mockFn).toHaveBeenCalledWith('id-42');
```

**References.** [Jest · Expect / matchers](https://jestjs.io/docs/expect) · [MDN · Object.is()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)

---

### 5. Setup, teardown, and test isolation  `Easy`

**Pattern:** Isolation

**Problem.** What do beforeEach/afterEach do, and why is shared state between tests dangerous?

**What it tests.** Understanding why tests must not leak state into each other and how lifecycle hooks enforce isolation.

**Approach & answer.** Lifecycle hooks run code around your tests: beforeEach/afterEach run before and after EVERY test in scope; beforeAll/afterAll run ONCE before and after the whole group. Their main job is to give each test a fresh, known starting world and to clean up after it — create a new instance, reset a store, mount a component, seed a fixture in beforeEach; unmount, clear mocks, restore globals in afterEach. The reason this matters is ISOLATION: tests must be independent, so that they pass or fail the same way whether run alone, together, or in any order. Shared mutable state between tests breaks that. If test A mutates a module-level object, a singleton, the DOM, localStorage, or a mock's call history, and test B silently depends on or is polluted by that, you get ORDER-DEPENDENT tests: they pass when run in one order and fail in another, or one test's failure cascades into others. That's a nightmare to debug because the failing test isn't the buggy one. The fixes: put fresh setup in beforeEach (not beforeAll, unless the resource is genuinely immutable and expensive) so nothing carries over; reset all mocks between tests (jest's clearMocks/resetMocks or afterEach(() => jest.clearAllMocks())); and reset shared browser state (localStorage, document body) if you touched it. Prefer creating new local objects inside each test over sharing a top-level `let` that tests mutate. A good litmus test: run the suite with randomized order (test runners support it) — if that surfaces failures, you have hidden shared state. The discipline pays off as the ability to run one test in isolation and trust its result, and to parallelize the suite safely.

**Use this technique when.** Structuring test setup; debugging tests that only fail in a certain order or when run together.

```js
let cart;

beforeEach(() => {
  cart = createCart();      // fresh state per test -> no leakage
});

afterEach(() => {
  jest.clearAllMocks();     // reset spy call history
  localStorage.clear();     // reset shared browser state you touched
});

test('starts empty', () => expect(cart.items).toHaveLength(0));
test('adds an item', () => { cart.add(item); expect(cart.items).toHaveLength(1); });
// Because cart is rebuilt each time, order can't make one test affect another.
```

**References.** [Jest · Setup and Teardown](https://jestjs.io/docs/setup-teardown) · [Vitest · Test API (lifecycle)](https://vitest.dev/api/#setup-and-teardown)

---

### 6. Test doubles: stub, mock, spy, fake  `Easy`

**Pattern:** Test Doubles

**Problem.** Define stub, spy, mock, and fake. When would you use each, and what's the risk of over-using them?

**What it tests.** Distinguishing the kinds of test doubles and the tradeoff between isolation and realism.

**Approach & answer.** 'Test double' is the umbrella term (like a stunt double) for any stand-in you swap for a real dependency in a test. The four common kinds differ by what they replace and what you assert. A STUB provides canned answers: it returns preset values so the code under test can run without the real dependency — 'when getUser is called, return {id:1}'. You use it to control inputs and avoid slow/nondeterministic dependencies; you don't assert on the stub itself. A SPY wraps a real (or empty) function and RECORDS how it was called — arguments, call count — while optionally letting the real one still run. You use it to verify an interaction happened ('the logger was called once with this error') without changing behavior. A MOCK is a double with pre-programmed EXPECTATIONS about how it should be called; the assertion is baked in — the test fails if the mock isn't called as specified. It's for verifying interactions are the contract. A FAKE is a working lightweight implementation — an in-memory database, a fake clock, an in-memory version of a repository — real enough to behave correctly but not production-grade. You use it when you need realistic behavior across many calls, not just canned returns. In practice the libraries blur these (jest.fn() can act as stub, spy, and mock), so the useful distinction is intent: am I controlling input (stub/fake) or verifying an interaction (spy/mock)? The over-use risk: MOCKING TOO MUCH couples tests to implementation (you assert internal calls, so refactors break tests) and reduces confidence (you tested against your assumptions of the dependency, which may be wrong — 'all your mocks pass but production is broken'). Prefer faking at real boundaries (network via MSW, time via fake timers) and using real collaborators inside the unit where cheap.

**Use this technique when.** Choosing how to replace a dependency; explaining why heavy mocking makes a suite brittle.

```js
// Stub: canned return, controls input to the code under test
const getRate = jest.fn().mockReturnValue(1.1);

// Spy: records calls, can keep real behavior
const spy = jest.spyOn(logger, 'error');
doThing();
expect(spy).toHaveBeenCalledWith(expect.stringContaining('failed'));

// Mock: assert the interaction is the contract
const pay = jest.fn();
checkout(cart, pay);
expect(pay).toHaveBeenCalledTimes(1);

// Fake: a real, lightweight implementation
const db = new InMemoryUserRepo(); // behaves like the real repo, no network
```

**References.** [Martin Fowler · Test Double](https://martinfowler.com/bliki/TestDouble.html) · [Jest · Mock Functions](https://jestjs.io/docs/mock-functions)

---

### 7. Testing pure functions vs. side effects  `Easy`

**Pattern:** Side Effects

**Problem.** Why are pure functions the easiest thing to test, and how do you make code with side effects testable?

**What it tests.** Understanding how purity affects testability and the design moves that isolate side effects.

**Approach & answer.** A PURE function's output depends only on its inputs and it causes no observable side effects (no I/O, no mutation of external state, no reading the clock or random). That makes it trivially testable: give inputs, assert the output, done — no setup, no doubles, no teardown, fully deterministic, and fast. This is why pushing logic into pure functions is a testability superpower: the hard-to-test parts shrink. SIDE-EFFECTFUL code — hits the network, writes a file, reads Date.now()/Math.random(), mutates a global, logs, touches the DOM — is harder because the result isn't determined by inputs alone and running it does something to the world. Three moves make it testable. (1) SEPARATE the effect from the decision: extract a pure 'calculate what to do' function and keep a thin impure shell that performs the effect. You unit-test the pure core exhaustively and only lightly test the shell. (2) INJECT dependencies rather than reaching for them: pass the clock, the fetch function, the random source, or the repository as arguments (or via a constructor), so a test can pass a fake — e.g., accept `now = () => Date.now()` so tests pass a fixed time. (3) Use CONTROLLED doubles at the real boundaries for the effects you can't remove: fake timers for time, MSW for network, an in-memory repo for the database. The design payoff is broader than tests — code that's easy to test (small pure functions + injected dependencies + effects at the edges) is also easier to reason about and reuse. So 'this is hard to test' is usually a design signal: the logic and the effect are tangled and want to be pulled apart.

**Use this technique when.** Refactoring hard-to-test code; deciding where to put logic vs. effects.

```js
// Hard to test: reads the clock + performs the effect inline
function greet() {
  const h = new Date().getHours();               // hidden input
  document.title = h < 12 ? 'Morning' : 'Hello'; // effect
}

// Testable: pure decision (inject the hour) + thin effectful shell
function greetingFor(hour) {                      // PURE, trivial to test
  return hour < 12 ? 'Morning' : 'Hello';
}
function applyGreeting(now = () => new Date()) {  // shell, inject clock
  document.title = greetingFor(now().getHours());
}

expect(greetingFor(9)).toBe('Morning');
expect(greetingFor(15)).toBe('Hello');
```

**References.** [MDN · Pure functions (First-class / functional concepts)](https://developer.mozilla.org/en-US/docs/Glossary/Pure_function) · [Kent C. Dodds · Avoid the Test User (design for testability)](https://kentcdodds.com/blog/avoid-the-test-user)

---

### 8. Code coverage: what it does and doesn't tell you  `Easy`

**Pattern:** Coverage

**Problem.** What does code coverage measure, and why is '100% coverage' not the same as 'well tested'?

**What it tests.** Reading coverage as a diagnostic for untested code, not as a proxy for test quality.

**Approach & answer.** Code coverage measures which parts of your code were EXECUTED while the tests ran. The common flavors: line coverage (which lines ran), statement coverage (similar), branch coverage (were both sides of each if/ternary/switch taken), and function coverage (which functions were called). Branch coverage is the most informative because it catches untested paths that line coverage hides — a line with `a && b` can be 100% line-covered while one branch never executed. Coverage is genuinely useful as a DIAGNOSTIC: it reliably tells you what is definitely NOT tested — uncovered lines are code no test touched, which is a real gap and a good place to look. What it does NOT tell you is whether the code is WELL tested, for a simple reason: coverage records that a line RAN, not that you ASSERTED anything meaningful about it. You can execute every line with zero assertions (or weak ones) and hit 100% while testing nothing — a test that calls a function and never checks its result 'covers' it. Coverage also can't see the cases you forgot: the empty array, the null, the boundary, the error path that your inputs never triggered; nor whether your assertions check the right thing. That's why chasing 100% is a trap — it drives people to write assertion-free tests for trivial getters just to hit a number, spending effort where risk is low and creating a false sense of safety. Use coverage the right way: as a floor and a spotlight (fail CI if it drops sharply, and review uncovered critical paths), not as a target that proves quality. Mutation testing is the tool that actually measures assertion strength — it changes your code and checks whether tests fail — but it's heavier. The honest summary: high coverage with weak assertions is worse than moderate coverage with sharp ones.

**Use this technique when.** Interpreting a coverage report; pushing back on a blanket '100% coverage' mandate.

```text
Coverage TELLS you:            Coverage does NOT tell you:
- which lines/branches ran     - whether you asserted anything useful
- what is definitely UNtested  - whether you covered the edge cases
                               - whether the assertions are correct

# A "100% covered" test that proves nothing:
test('runs', () => { calculateTotal(cart); }); // no expect(...) at all!

# Prefer branch coverage; use it as a spotlight on gaps, not a target.
# Mutation testing measures assertion STRENGTH; coverage measures execution.
```

**References.** [Martin Fowler · Test Coverage](https://martinfowler.com/bliki/TestCoverage.html) · [Istanbul · Code coverage](https://istanbul.js.org/)

---

### 9. Testing React components the user's way  `Medium`

**Pattern:** Component Testing

**Problem.** How should you query and assert on a React component with Testing Library? Why 'by role' over test IDs or class names?

**What it tests.** Whether you test components through the accessibility tree the way a user perceives them, not through internals.

**Approach & answer.** Testing Library's core idea is to test a component the way a USER interacts with it, so your tests give confidence that the real thing works and don't break on refactors. Concretely: render the component, find elements the way a person (or assistive tech) would, interact, and assert on what's visible. That means preferring queries in this priority order: getByRole (with an accessible name, e.g. getByRole('button', {name: /submit/i})) is best because it mirrors how users and screen readers find things and doubles as an accessibility check; then getByLabelText for form fields (you find inputs by their label, as a user does); then getByPlaceholderText, getByText, getByDisplayValue; and only as a last resort getByTestId, an escape hatch for elements with no accessible handle. You explicitly AVOID querying by CSS class or DOM structure (container.querySelector('.btn-primary')) because those are implementation details — rename a class or restructure a div and the test breaks though nothing user-facing changed. Query variants matter too: getBy throws if not found (assert presence), queryBy returns null (assert ABSENCE — the only one for 'should not be there'), findBy returns a promise and retries (for elements that appear after async work). Assert with jest-dom matchers (toBeInTheDocument, toBeVisible, toBeDisabled, toHaveTextContent) which read well and fail with helpful messages. A nice side effect: if you can't query your component by role/label, that's often a real accessibility gap — the test is telling you a screen-reader user would struggle too. The mental model: don't reach into the component; interact with its rendered output the way a human would, and assert on what they'd observe.

**Use this technique when.** Writing component tests; choosing a query; deciding whether a testid is justified.

```jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('shows a greeting after submitting a name', async () => {
  render(<Greeter />);

  // Query the way a user / screen reader finds things
  await userEvent.type(screen.getByLabelText(/your name/i), 'Ada');
  await userEvent.click(screen.getByRole('button', { name: /greet/i }));

  // Assert on what's visible, not on internal state or classes
  expect(screen.getByText(/hello, ada/i)).toBeInTheDocument();
});

// Avoid: container.querySelector('.greeting') -> couples to markup/CSS
```

**References.** [Testing Library · About Queries / priority](https://testing-library.com/docs/queries/about/#priority) · [jest-dom · custom matchers](https://github.com/testing-library/jest-dom)

---

### 10. user-event vs. fireEvent  `Medium`

**Pattern:** Simulating Interaction

**Problem.** What's the difference between user-event and fireEvent, and why is user-event usually the right choice?

**What it tests.** Understanding that real interactions are sequences of events, and why simulating a single event under-tests behavior.

**Approach & answer.** fireEvent dispatches a SINGLE DOM event exactly as you specify — fireEvent.click(el) fires one click, fireEvent.change(input, {target:{value:'x'}}) sets the value and fires one change. user-event simulates a REAL USER INTERACTION, which is usually a whole SEQUENCE of events plus browser-realistic behavior. When a user clicks, the browser fires pointerdown, mousedown, focus, pointerup, mouseup, and click; when they type 'ab', it fires keydown/keypress/input/keyup per character, respects focus, and won't 'type' into a disabled or readonly field. user-event reproduces those sequences, so it catches bugs fireEvent misses: a handler that relies on focus firing, a keydown listener, an input that should ignore typing when disabled, or logic that runs on the intermediate events. Because it's realistic, user-event is asynchronous (v14+ returns promises; you await it) and you typically set it up with userEvent.setup() at the top of the test. The guidance is: reach for user-event by default because 'the more your tests resemble how software is used, the more confidence they give you'; drop to fireEvent only for the rare low-level case user-event doesn't model well (certain scroll, custom, or media events, or when you need to fire one specific event in isolation). A concrete gotcha: fireEvent.change directly sets a value without the keystroke sequence, so a component that formats input on each keystroke, or blocks certain characters, can look correct under fireEvent and be broken for real users — user-event.type would expose it. So the difference isn't cosmetic: it's the gap between 'this event handler ran' and 'a person using this actually gets the right result'.

**Use this technique when.** Simulating clicks/typing in component tests; debugging a test that passes but the feature is broken for users.

```jsx
import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen } from '@testing-library/react';

test('typing runs the real event sequence', async () => {
  const user = userEvent.setup();
  render(<Search />);
  const box = screen.getByRole('searchbox');

  await user.type(box, 'hi'); // keydown/keypress/input/keyup per char, respects focus
  expect(box).toHaveValue('hi');
});

// fireEvent fires ONE event and skips the sequence:
// fireEvent.change(box, { target: { value: 'hi' } });
// -> can pass while per-keystroke formatting/validation is broken for users.
```

**References.** [Testing Library · user-event intro](https://testing-library.com/docs/user-event/intro/) · [Testing Library · Considerations (fireEvent vs userEvent)](https://testing-library.com/docs/user-event/intro/#differences-from-fireevent)

---

### 11. Mocking modules and the network (MSW vs. jest.mock)  `Medium`

**Pattern:** Mocking Boundaries

**Problem.** How do you mock a module vs. the network in tests, and why is intercepting HTTP (MSW) usually better than mocking fetch?

**What it tests.** Choosing the right seam to fake, and why mocking at the network boundary yields more realistic, less brittle tests.

**Approach & answer.** There are two common things people fake and they sit at different layers. MODULE mocking (jest.mock('./api')) replaces an imported module with a fake implementation — useful for pure code dependencies, feature flags, or a utility you want to control. But when the dependency is the NETWORK, you have a choice of seam. The tempting one is to mock fetch/axios directly (global.fetch = jest.fn().mockResolvedValue(...)) or jest.mock the api client. That works but is brittle and less realistic: your test now asserts against YOUR assumption of what the client returns, bypasses request-building, URL/param/header logic, serialization, error handling, and retries — so 'all mocks pass' can coexist with a broken integration, and every refactor of how you call the API breaks tests. The better approach is to intercept at the NETWORK BOUNDARY with MSW (Mock Service Worker): you declare request handlers ('GET /api/users returns this JSON') and MSW intercepts the actual outbound request, so your app runs its real fetch/axios code end to end — real URLs, headers, status codes — and you only stub the wire response. Benefits: tests are decoupled from HOW you fetch (swap fetch for axios and tests still pass), you exercise real request/response handling, you can model errors and edge cases (500s, timeouts, malformed bodies) declaratively, and the SAME handlers work in unit tests, Storybook, and the browser during development. jest.mock still has its place — mock a module for non-network dependencies, or when you truly want to isolate a unit from a collaborator — but for HTTP, prefer MSW. General principle: mock at the furthest-out boundary you can (the network, the clock), so the most of your real code runs and your tests break only when behavior actually changes.

**Use this technique when.** Testing code that calls an API; deciding between jest.mock and network interception.

```js
// MSW: intercept at the network boundary; real fetch/axios code still runs
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  http.get('/api/users', () => HttpResponse.json([{ id: 1, name: 'Ada' }]))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// Model an error case declaratively for one test:
// server.use(http.get('/api/users', () => new HttpResponse(null, { status: 500 })));

// jest.mock: right for NON-network module dependencies
jest.mock('./featureFlags', () => ({ isEnabled: () => true }));
```

**References.** [Mock Service Worker · Docs](https://mswjs.io/docs/) · [Jest · Mocking Modules (jest.mock)](https://jestjs.io/docs/mock-functions#mocking-modules)

---

### 12. Testing async code and fake timers  `Medium`

**Pattern:** Async Testing

**Problem.** How do you test promises, code that updates after awaiting, and time-dependent logic (debounce, setTimeout)?

**What it tests.** Handling asynchrony without arbitrary sleeps, and controlling time deterministically instead of waiting for it.

**Approach & answer.** The golden rule for async tests: never sleep for a fixed duration and hope — that's slow and flaky. Instead, AWAIT the thing or POLL for the expected state. For a promise-returning function, make the test async and await it (or use resolves/rejects matchers: await expect(load()).resolves.toEqual(...)). For UI that updates AFTER async work (fetch resolves, then the DOM changes), use Testing Library's findBy* (which retries until the element appears or times out) or waitFor(() => expect(...)) which re-runs the callback until it passes. That polls for the OUTCOME rather than guessing a delay, so it's both fast (resolves as soon as it's true) and robust (won't fail on a slightly slow machine). For TIME-dependent logic — debounce, throttle, setTimeout/setInterval, polling — don't wait real seconds; take control of the clock with FAKE TIMERS (jest.useFakeTimers()). Then you ADVANCE time deterministically: jest.advanceTimersByTime(300) fast-forwards 300ms so a debounced callback fires instantly and predictably, and the test runs in milliseconds with zero flakiness. Remember to restore real timers afterward (jest.useRealTimers() in afterEach) so you don't leak the fake clock into other tests, and note that when combining fake timers with user-event you configure user-event with the fake-timer advance function. Two classic mistakes to avoid: (1) not awaiting — the test finishes and passes before the assertion runs, giving false green (or a warning about state updates after the test); (2) asserting immediately after triggering async work without waiting, so you check the DOM before it has updated. The pattern to internalize: control what you can make deterministic (time — fake it), and for genuinely async outcomes, poll for the result instead of racing it.

**Use this technique when.** Testing fetch-driven UI, debounced handlers, timeouts, or anything that resolves later.

```jsx
// 1) Wait for an async OUTCOME (no arbitrary sleep)
test('shows users after load', async () => {
  render(<UserList />);
  expect(await screen.findByText('Ada')).toBeInTheDocument(); // retries until present
});

// 2) Control time for debounce/timers instead of waiting for it
test('debounces search by 300ms', () => {
  jest.useFakeTimers();
  const cb = jest.fn();
  const search = debounce(cb, 300);
  search('a'); search('ab');
  jest.advanceTimersByTime(300); // fast-forward, no real delay
  expect(cb).toHaveBeenCalledTimes(1);
  expect(cb).toHaveBeenCalledWith('ab');
  jest.useRealTimers();
});
```

**References.** [Testing Library · Async Methods (findBy, waitFor)](https://testing-library.com/docs/dom-testing-library/api-async/) · [Jest · Timer Mocks](https://jestjs.io/docs/timer-mocks)

---

### 13. Testing custom React hooks  `Medium`

**Pattern:** Hooks

**Problem.** How do you test a custom hook in isolation, and when should you test it through a component instead?

**What it tests.** Knowing renderHook + act for isolated hook logic, and when a real component test gives more confidence.

**Approach & answer.** A custom hook can't be called outside a component (Rules of Hooks), so to test one in isolation you use renderHook from @testing-library/react, which mounts a tiny host component that calls your hook and exposes its return value via result.current. You then trigger state changes and assert on result.current. Any call that updates state must be wrapped in act() (renderHook's utilities handle this) so React flushes updates and warnings don't fire; for async updates you await the state through waitFor or the async variants. renderHook also lets you re-render with new props (rerender) to test how the hook responds to prop changes, and provides a wrapper option to supply context providers the hook depends on (a store, a router, a theme). This isolated approach is great for hooks with real LOGIC — a useDebouncedValue, a usePagination reducer, a useToggle, a data-fetching hook where you want to assert loading/error/data transitions directly. HOWEVER, prefer testing through a real component when the hook's value only makes sense in the context of UI, or when isolating it would mean asserting on implementation details. Kent C. Dodds' guidance: if a hook is part of a component's behavior, testing the component that uses it often gives more confidence and is less coupled — you verify the user-facing result rather than the intermediate return value. A pragmatic split: test complex, reusable, logic-heavy hooks in isolation with renderHook (fast, focused, covers many states); test simple hooks and hook-plus-UI interactions through the component that consumes them. Either way, avoid the trap of testing the hook's internals (which state variable holds what) rather than its observable contract (given these calls, it returns these values / drives this UI).

**Use this technique when.** Deciding how to test a reusable hook; setting up renderHook with providers.

```jsx
import { renderHook, act, waitFor } from '@testing-library/react';

test('useCounter increments', () => {
  const { result } = renderHook(() => useCounter(0));
  expect(result.current.count).toBe(0);

  act(() => result.current.increment()); // state update -> wrap in act
  expect(result.current.count).toBe(1);
});

// Provide context the hook depends on via a wrapper:
// renderHook(() => useCartTotal(), { wrapper: ({children}) =>
//   <CartProvider>{children}</CartProvider> });

// Prefer a component test when the hook only matters through the UI it drives.
```

**References.** [Testing Library · renderHook API](https://testing-library.com/docs/react-testing-library/api/#renderhook) · [Kent C. Dodds · How to test custom React hooks](https://kentcdodds.com/blog/how-to-test-custom-react-hooks)

---

### 14. Flaky tests: causes and elimination  `Hard`

**Pattern:** Flakiness

**Problem.** What causes flaky tests, and how do you systematically hunt down and eliminate flakiness?

**What it tests.** Whether you can name the concrete sources of nondeterminism and fix root causes rather than paper over them.

**Approach & answer.** A flaky test passes and fails without any code change — the worst kind of test, because it destroys trust in the whole suite (people start re-running until green, then ignore real failures). Flakiness always comes from NONDETERMINISM, and the value is in naming the specific sources. (1) TIMING / async races: asserting before an async update lands, or a fixed sleep(500) that's usually-but-not-always enough. Fix: await the outcome (findBy/waitFor), never sleep a magic number. (2) TIME & RANDOMNESS: tests that depend on Date.now(), timezones, or Math.random(). Fix: inject/freeze the clock (fake timers), seed randomness, pin the timezone. (3) SHARED STATE / TEST ORDER: one test leaks state (a singleton, module cache, DB row, localStorage, un-reset mock) into another, so results depend on order or parallelism. Fix: isolate — fresh setup per test, reset mocks and global state, unique data per test; run with randomized order to surface it. (4) TEST INTERDEPENDENCE / resource contention: shared ports, files, or a shared DB across parallel workers. Fix: give each worker its own namespace/schema/tmp dir. (5) NETWORK / EXTERNAL SERVICES: hitting real APIs that are slow or occasionally down. Fix: stub at the boundary (MSW), don't call the real internet in unit/integration tests. (6) ANIMATIONS / rendering timing in e2e: asserting mid-transition. Fix: wait for a stable condition (element visible/enabled), disable animations, use auto-waiting locators. (7) IMPROPER WAITS in e2e: waiting for a fixed time instead of a condition. The systematic hunt: reproduce by running the test in a loop (--repeat), in random order, and in parallel; quarantine the flaky test (tag/skip) so it stops blocking CI, but track it — quarantine is triage, not a fix; then bisect the nondeterminism (does it fail alone? only after test X? only in CI?). Root-cause it into one of the buckets above and remove the source. Crucially, do NOT 'fix' flakiness with automatic retries as the primary strategy — retries hide real intermittent bugs and let flakiness accumulate; use them sparingly, if at all, and always with visibility into what retried and why.

**Use this technique when.** A test fails intermittently in CI; auditing suite reliability; setting a retry/quarantine policy.

```text
Source of nondeterminism        Root-cause fix
-------------------------------  -------------------------------------
timing / async race              await the outcome (findBy/waitFor), no fixed sleep
time & randomness                fake timers, seed RNG, pin timezone
shared state / order dependence  fresh setup, reset mocks+globals, unique data
resource contention (parallel)   per-worker db schema / port / tmp dir
real network                     stub at boundary (MSW), never hit the internet
animations / e2e waits           wait for a stable condition, not a duration

Hunt: run in a loop, randomized order, and in parallel to reproduce.
Quarantine to unblock CI, but that's triage. Retries HIDE bugs -> last resort.
```

**References.** [Google Testing Blog · Flaky Tests](https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html) · [Playwright · Test retries & flakiness](https://playwright.dev/docs/test-retries)

---

### 15. End-to-end testing: when and how  `Hard`

**Pattern:** End-to-End

**Problem.** When are Playwright/Cypress-style e2e tests worth it, what are the tradeoffs, and how do you keep them stable?

**What it tests.** Judgment about the cost/confidence of e2e plus the concrete practices that keep a real-browser suite from rotting.

**Approach & answer.** E2E tests drive the fully assembled app in a REAL browser as a user would — navigate, click, type, assert on rendered UI — usually against a running server and often a real (test) backend. Their value is the HIGHEST confidence: they prove the whole system actually works together, catching integration gaps that unit/component tests can't (routing, auth flow, real DOM/CSS, the bundle actually loading). The tradeoffs are why they sit at the TOP of the pyramid: they're the SLOWEST (seconds to minutes each, real network/render), the most EXPENSIVE to write and maintain, and the most PRONE TO FLAKINESS (timing, environment, data setup). So the judgment is: use e2e for a SMALL number of CRITICAL user journeys — sign-up/login, checkout/payment, the one or two flows that would be catastrophic if broken — and push everything else down to faster layers. Don't re-test every field validation or edge case through the browser; do that in component/unit tests, and use e2e to prove the happy path and a couple of high-value error paths end to end. Keeping them stable is a discipline: (1) use AUTO-WAITING, resilient LOCATORS (Playwright's getByRole/getByLabel, web-first assertions that retry) instead of fixed sleeps or brittle CSS selectors; (2) control TEST DATA — seed a known state via API/DB fixtures and reset between runs, don't depend on data that drifts; (3) make tests INDEPENDENT and idempotent (each creates its own user/data, can run in any order and in parallel); (4) stub only truly external/third-party services (payment sandboxes, email) while keeping YOUR stack real; (5) run headless in CI with tracing/video/screenshots on failure so you can debug the intermittent ones; (6) shard across machines to keep wall-clock down. Modern tools (Playwright especially) reduce flakiness with auto-waiting and isolated browser contexts, but the strategic point stands: e2e is a scalpel for critical flows, not a bucket for coverage.

**Use this technique when.** Deciding what deserves an e2e test; stabilizing a slow/flaky browser suite.

```js
import { test, expect } from '@playwright/test';

test('user can log in and reach the dashboard', async ({ page }) => {
  await page.goto('/login');

  // Resilient, user-facing locators + auto-waiting web-first assertions
  await page.getByLabel('Email').fill('ada@example.com');
  await page.getByLabel('Password').fill('correct horse');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Retries until true; no fixed sleep
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
// Reserve e2e for critical journeys; seed data via API; keep each test independent.
```

**References.** [Playwright · Best Practices](https://playwright.dev/docs/best-practices) · [Cypress · Core Concepts & best practices](https://docs.cypress.io/app/core-concepts/best-practices)

---

### 16. Testing accessibility in the suite  `Hard`

**Pattern:** Accessibility Testing

**Problem.** How do you build accessibility checks into automated tests, and what can and can't they catch?

**What it tests.** Integrating a11y assertions into CI while understanding the ceiling of automated a11y checking.

**Approach & answer.** You bake accessibility into tests at two levels. First, by HOW you query: using Testing Library's getByRole/getByLabelText already forces your components to expose an accessible name and role — if a test can't find a control by its role/name, neither can a screen reader, so the test surfaces the gap. Second, with a dedicated axe-core assertion: jest-axe in unit/component tests (expect(await axe(container)).toHaveNoViolations()) or @axe-core/playwright in e2e runs the axe rules engine against the rendered DOM and flags violations — missing form labels, insufficient color contrast, invalid ARIA, images without alt, duplicate ids, wrong heading structure. Wiring this into CI catches a whole class of regressions automatically and cheaply. The crucial caveat is the CEILING: automated tools catch only a MINORITY of accessibility issues — commonly cited as roughly 30–50% — because most a11y is about MEANING and EXPERIENCE that a machine can't judge. Axe can tell you an image has alt text, not whether the alt text is meaningful; it can confirm a button has a name, not whether the tab order is logical, whether focus is managed when a modal opens, whether a custom widget is actually operable by keyboard, whether the screen-reader announcement makes sense, or whether an animation triggers vestibular issues. So the strategy is layered: (1) automated axe checks in CI as a regression net for the mechanical rules; (2) role/label-based queries so components are built accessible by default; (3) explicit tests for keyboard operability and focus management (tab through, assert focus lands where it should, Escape closes and returns focus) — things you CAN automate and axe won't check; and (4) manual testing with real assistive tech (VoiceOver/NVDA) and keyboard-only for the judgment calls automation can't make. Framing it honestly in an interview — 'automation is a floor, not a ceiling; it catches the mechanical violations so humans can spend their time on the experiential ones' — is the point.

**Use this technique when.** Adding a11y regression checks to CI; explaining why automated a11y isn't sufficient alone.

```jsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('form has no axe violations', async () => {
  const { container } = render(<SignupForm />);
  expect(await axe(container)).toHaveNoViolations(); // mechanical rules only (~30-50%)
});

// Also test what axe CAN'T: keyboard operability + focus management
test('modal traps focus and Escape returns it', async () => {
  const user = userEvent.setup();
  render(<Page />);
  await user.click(screen.getByRole('button', { name: /open/i }));
  await user.keyboard('{Escape}');
  expect(screen.getByRole('button', { name: /open/i })).toHaveFocus();
});
```

**References.** [jest-axe](https://github.com/nickcolley/jest-axe) · [Deque · axe-core (what automated testing catches)](https://github.com/dequelabs/axe-core)

---

### 17. Snapshot testing: value, pitfalls, and rot  `Hard`

**Pattern:** Snapshot Testing

**Problem.** What is snapshot testing good and bad at? How do snapshots 'rot', and how do you use them responsibly?

**What it tests.** Whether you understand snapshots as change-detectors, not correctness checks, and can avoid the classic failure mode.

**Approach & answer.** A snapshot test renders something (a component, a serializable object) and saves its serialized output to a file on first run; subsequent runs DIFF the current output against the stored snapshot and fail if they differ. Its genuine value is CHANGE DETECTION with almost no assertion-writing: it alerts you when output changed unexpectedly, which is handy for catching accidental UI/markup regressions and for large structured outputs where hand-writing assertions is tedious. The critical mental model: a snapshot proves output DIDN'T CHANGE, not that it's CORRECT. The first snapshot is captured blind — if the component was already buggy, the snapshot enshrines the bug and happily 'passes'. That's the core weakness. The classic failure mode is snapshot ROT: large, whole-component snapshots change on almost every legitimate edit, producing constant diffs; developers stop reading them and reflexively run `jest -u` to update, at which point the snapshot tests nothing — they rubber-stamp whatever the code produces. Big snapshots also make diffs unreadable (hundreds of lines), so real regressions hide among noise. Responsible use: (1) keep snapshots SMALL and FOCUSED — snapshot a specific piece of output or use inline snapshots (toMatchInlineSnapshot) that live next to the test and get reviewed in the diff, not giant DOM dumps; (2) REVIEW every snapshot change in code review as deliberately as any assertion — an updated snapshot is a claim that the new output is correct; (3) prefer EXPLICIT assertions (getByRole, toHaveTextContent) for the behavior you actually care about, and use snapshots only as a supplementary net; (4) don't snapshot things that legitimately vary (dates, ids, random) without serializers/masks, or they'll be perpetually flaky; (5) treat a failing snapshot as a question ('did I mean to change this?'), and only update after confirming the new output is intended. The honest summary: snapshots are a cheap tripwire, not a substitute for asserting correctness — their value collapses the moment updating them becomes reflexive.

**Use this technique when.** Deciding whether to snapshot; reviewing a PR full of updated snapshots; taming snapshot rot.

```jsx
// Rot risk: a giant blind snapshot of a whole component
expect(render(<Dashboard />).container).toMatchSnapshot(); // 300-line diff, rubber-stamped

// Better: small, reviewable, inline
expect(formatMoney(1999)).toMatchInlineSnapshot('"$19.99"');

// Best for behavior you care about: an explicit assertion
expect(screen.getByRole('status')).toHaveTextContent('Saved');

// Rules: keep snapshots tiny, REVIEW every update, don't reflexively run 'jest -u'.
```

**References.** [Jest · Snapshot Testing](https://jestjs.io/docs/snapshot-testing) · [Kent C. Dodds · Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing)

---

### 18. Visual regression testing  `Hard`

**Pattern:** Visual Regression

**Problem.** What is visual regression testing, what does it catch that DOM tests miss, and what makes it hard?

**What it tests.** Understanding pixel-diff testing, its unique coverage (actual rendering), and the stability challenges it introduces.

**Approach & answer.** Visual regression testing captures a SCREENSHOT of a rendered UI (a component, a page, a state) and compares it pixel-by-pixel against an approved BASELINE image; a diff beyond a threshold fails and highlights the changed pixels for a human to approve or reject. It catches a class of bugs that DOM/behavior tests structurally CANNOT: purely VISUAL regressions where the markup and roles are unchanged but the appearance broke — a CSS change that shifts layout, a wrong color or contrast, an overlapping element, a broken web font, a component that looks fine in isolation but collides with a neighbor, a responsive breakpoint gone wrong. Testing Library asserts 'the button exists and says Submit'; only a visual test notices the button is now white-on-white or pushed off-screen. It's especially valuable for design systems and shared components, where one CSS tweak can ripple across many usages. What makes it HARD is stability and cost. Pixel diffs are exquisitely sensitive to nondeterminism: font rendering and anti-aliasing differ across OS/browser/GPU (so baselines captured on a Mac fail in Linux CI), animations and transitions catch mid-frame, dynamic content (dates, avatars, random data) changes every run, and even sub-pixel layout jitters. The result is FALSE POSITIVES — noisy diffs that, like snapshot rot, train people to approve blindly. Mitigations: render in a CONSISTENT environment (pin the browser/OS, run in Docker or a hosted service like Chromatic/Percy so baselines and comparisons match), DISABLE animations and freeze time, MASK or stub dynamic regions, allow a small anti-aliasing threshold, and test COMPONENTS in fixed states (often via Storybook) rather than whole live pages to shrink the surface. There's also a review-workflow cost: every intended visual change requires a human to approve the new baseline, and baselines must be versioned. Positioning it correctly: visual regression is a complement, not a replacement — behavior tests verify it WORKS, visual tests verify it LOOKS right — and it earns its keep most on design systems and high-traffic pages where appearance is part of the contract.

**Use this technique when.** Protecting a design system from CSS regressions; deciding if pixel diffing is worth the upkeep.

```js
import { test, expect } from '@playwright/test';

test('primary button looks right', async ({ page }) => {
  await page.goto('/storybook/button--primary');
  // Freeze nondeterminism first: disable animations, mask dynamic areas
  await expect(page.getByRole('button', { name: 'Buy now' }))
    .toHaveScreenshot('button-primary.png', {
      animations: 'disabled',
      maxDiffPixelRatio: 0.01,   // small AA tolerance
    });
});
// Baselines must be captured in the SAME env as CI (pin browser/OS, e.g. Docker),
// else font/AA differences cause false positives -> blind approvals (rot).
```

**References.** [Storybook · Visual tests](https://storybook.js.org/docs/writing-tests/visual-testing) · [Playwright · Visual comparisons (toHaveScreenshot)](https://playwright.dev/docs/test-snapshots)

---

### 19. Test suite performance and parallelization  `Hard`

**Pattern:** Suite Performance

**Problem.** A test suite takes 20 minutes and blocks every PR. How do you make it fast without losing confidence?

**What it tests.** Diagnosing what makes a suite slow and applying parallelization, sharding, and selection without sacrificing coverage.

**Approach & answer.** A slow suite is a real cost — it slows every merge, so people batch changes, skip running it locally, and lose the fast feedback tests exist to provide. Attack it in layers. First, DIAGNOSE: find the slow tests (runners report per-test/-file timing) and the slow LAYER. Usually the shape is wrong — too many slow e2e/integration tests doing work that unit tests could do (the ice-cream cone). Rebalancing toward the pyramid (push logic coverage down to fast unit tests, keep e2e to critical journeys) is the biggest structural win. Second, PARALLELIZE: unit/component runners (Jest, Vitest) already run test FILES across worker processes/threads on multiple cores — ensure that's on and workers are tuned to the machine; the prerequisite is ISOLATION (no shared state/ports/db rows), because parallelism exposes any hidden coupling as flakiness. Third, SHARD across MACHINES in CI: split the suite into N shards run on N runners in parallel (jest --shard=1/4, Playwright's built-in sharding), cutting wall-clock roughly linearly; combine with per-worker resource namespacing (own DB schema, own tmp dir, own port) so shards don't collide. Fourth, TEST SELECTION: run only what changed — Jest's --onlyChanged / --changedSince, or tooling that maps changed files to affected tests (Nx/Turbo affected graphs), so a one-file PR doesn't run 10k tests; keep the full suite for main/merge. Fifth, cut per-test OVERHEAD: avoid real network (MSW instead of live calls), fake timers instead of real waits, seed DB via fast fixtures/transactions with rollback instead of full migrations per test, reuse expensive setup with beforeAll where safe, and mock heavy modules you don't need. Sixth, CACHE: dependency and build caches, and transform caches (SWC/esbuild transforms are far faster than babel-ts) so startup isn't dominated by compilation. The guardrail throughout: speed must not come from deleting assertions or over-mocking until tests prove nothing — the goal is the same confidence, faster. Measure before and after, and watch that parallelization didn't introduce flakiness (the tax for hidden shared state).

**Use this technique when.** CI is slow; scaling a growing suite; tuning parallel workers and sharding.

```text
Lever                    How                                   Watch out for
-----------------------  ------------------------------------  --------------------------
rebalance the pyramid    move coverage from e2e -> unit        keep e2e for critical flows
parallelize (1 machine)  Jest/Vitest workers across cores      needs test isolation
shard (N machines)       jest --shard=1/4, pw sharding         per-worker db/port/tmp
test selection           --onlyChanged / affected graph        run full suite on main
cut per-test overhead    MSW, fake timers, fast fixtures       don't over-mock -> false green
caching                  deps + SWC/esbuild transform cache    invalidate correctly

Rule: same confidence, faster. Speed must NOT come from weaker assertions.
Measure before/after; parallelism turns hidden shared state into flakiness.
```

**References.** [Jest · CLI (--shard, --onlyChanged, maxWorkers)](https://jestjs.io/docs/cli) · [Playwright · Parallelism and sharding](https://playwright.dev/docs/test-parallel)

---

### 20. What NOT to test, and testing ROI  `Hard`

**Pattern:** Test Strategy

**Problem.** How do you decide what NOT to test? Explain testing ROI and the 'testing trophy' vs. the pyramid.

**What it tests.** Strategic judgment: spending test effort where risk and confidence-per-cost are highest, not chasing coverage.

**Approach & answer.** Mature testing is as much about what you DON'T test as what you do, because every test has ongoing cost (write it, run it, maintain it, debug it when it flakes). ROI = confidence gained per unit of cost, and you want to spend where that ratio is highest. Things generally NOT worth testing directly: (1) third-party code and the framework/language itself — don't test that React renders or that lodash's map works; trust your dependencies and test YOUR usage of them at the boundary. (2) Trivial code with no logic — a getter that returns a field, a component that renders a static string, a config object; a test here just duplicates the code and breaks when it changes, providing near-zero confidence. (3) IMPLEMENTATION DETAILS — private methods, internal state, exact call counts; testing these is negative ROI because they break on refactors without catching real bugs. (4) Purely generated or declarative code, and throwaway/spike code. Conversely, spend heavily where risk × likelihood is high: complex business logic and calculations, edge cases and error handling, code that's changed often or broken before, security-sensitive paths, and the critical user journeys. The 'testing trophy' (Kent C. Dodds) reframes the pyramid for UI-heavy apps: instead of a huge unit base, it FATTENS the INTEGRATION layer (components + hooks + real-ish collaborators) because for front-end apps that's where confidence-per-cost peaks — integration tests exercise realistic behavior without e2e's flakiness/slowness, and 'the more your tests resemble how software is used, the more confidence they give you'. It keeps a static-analysis base (TypeScript, ESLint — free bug-catching before tests even run), some unit tests for pure logic, a strong integration middle, and a few e2e at the top. Pyramid vs. trophy isn't a contradiction so much as a difference in emphasis by app type: backend/library code with lots of pure logic leans pyramid (big unit base); UI apps lean trophy (big integration middle). The unifying principle for an interview: don't chase a coverage number — target the tests that would catch the failures you'd most regret, at the layer that gives realistic confidence per dollar, and consciously skip the low-ROI ones. Contract/consumer-driven tests are the analog at service boundaries: test the interface others depend on, not every internal path.

**Use this technique when.** Prioritizing limited testing time; justifying test strategy in review; pushing back on coverage mandates.

```text
Skip (low ROI)                     Invest (high ROI)
---------------------------------  ----------------------------------------
framework/3rd-party internals      complex business logic & calculations
trivial getters / static markup    edge cases, error & boundary handling
private methods / internal state   code that changed often / broke before
exact call counts (implementation) critical journeys (login, checkout)

Testing Trophy (UI apps)      vs.  Pyramid (logic/backend)
  e2e            (few)                E2E        (few)
  INTEGRATION    (most) <- fat        Integration (some)
  unit           (some)               Unit        (many) <- fat
  static (TS/lint) (base)             

Aim: max confidence per cost. Don't chase a coverage %.
```

**References.** [Kent C. Dodds · The Testing Trophy and Testing Classifications](https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications) · [Martin Fowler · Test Pyramid](https://martinfowler.com/bliki/TestPyramid.html)

---

## Networking/Security

> The browser is a hostile environment running trusted code, and the network between it and your server is a place where things get read, tampered with, and forged. This section is about the two halves of that reality: how HTTP actually works — methods, status codes, the anatomy of a message, caching and conditional requests, and the protocol's evolution from HTTP/1.1 through HTTP/2 to HTTP/3 — and how the web's security model defends users against a web where any page can send requests to any other. It works up from fundamentals — the same-origin policy, CORS, cookies, TLS, and what a URL bar actually triggers — into the vulnerability classes every frontend engineer must reason about (XSS, CSRF, clickjacking, supply-chain risk) and the layered defenses that contain them (context-aware escaping, Content Security Policy, SameSite cookies, security headers, Subresource Integrity). It closes with the harder design questions: where to store auth tokens, the OAuth/OIDC authorization-code flow with PKCE, choosing a real-time transport, and building request handling that survives a flaky, rate-limited network. The recurring lesson: the platform gives you strong defaults, but security is defense in depth — no single header, flag, or escape saves you, and the engineer who understands why each layer exists is the one who ships software that holds up.

### 1. HTTP methods and status codes  `Easy`

**Pattern:** HTTP Basics

**Problem.** Walk through the common HTTP methods and status-code families. What does each convey?

**What it tests.** Fluency with the vocabulary of HTTP — the semantics clients and servers rely on to communicate intent and outcome.

**Approach & answer.** HTTP is a request/response protocol where the METHOD states the client's intent and the STATUS CODE states the server's outcome. The common methods: GET retrieves a resource and must have no side effects (safe); POST submits data to create a resource or trigger processing; PUT replaces a resource entirely at a known URL (idempotent — repeating it lands the same state); PATCH applies a partial update; DELETE removes a resource (idempotent); HEAD is GET without a body (fetch just the headers, e.g. to check existence or size); OPTIONS asks what a resource supports and is the mechanism behind the CORS preflight. Status codes come in five families, and knowing the family tells you who's responsible: 1xx informational (rare, e.g. 100 Continue, 101 Switching Protocols for WebSocket upgrade); 2xx success — 200 OK, 201 Created (with a Location header for the new resource), 204 No Content (success, nothing to return); 3xx redirection — 301 Moved Permanently, 302/307 temporary redirect, and the important 304 Not Modified (your cached copy is still valid, sent in response to a conditional request); 4xx client errors — 400 Bad Request (malformed), 401 Unauthorized (you are not authenticated — misnamed, it really means unauthenticated), 403 Forbidden (authenticated but not allowed), 404 Not Found, 405 Method Not Allowed, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests (rate limited); 5xx server errors — 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout. The distinction people most often get wrong is 401 vs 403: 401 means 'I don't know who you are, authenticate', 403 means 'I know who you are and you still can't'. Using the right code matters because caches, browsers, and clients act on them — a 301 gets cached and rewrites future requests, a 429 tells a well-behaved client to back off, a 405 advertises allowed methods in the Allow header.

**Use this technique when.** Designing an API's responses; debugging why a client mishandles a response; choosing the right status code.

```text
Methods:  GET(safe) HEAD  POST  PUT(idempotent) PATCH  DELETE(idempotent) OPTIONS

Status families:
  1xx info         100 Continue, 101 Switching Protocols (WebSocket upgrade)
  2xx success      200 OK, 201 Created, 204 No Content
  3xx redirect     301 Moved, 302/307 Found/Temp, 304 Not Modified (cache valid)
  4xx client error 400 Bad Request, 401 Unauthenticated, 403 Forbidden,
                   404 Not Found, 405 Method Not Allowed, 429 Too Many Requests
  5xx server error 500 Internal, 502 Bad Gateway, 503 Unavailable, 504 Timeout

401 = "who are you?" (authenticate)   403 = "I know you, still no"
```

**References.** [MDN · HTTP request methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) · [MDN · HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)

---

### 2. Anatomy of an HTTP request and response  `Easy`

**Pattern:** HTTP Basics

**Problem.** Describe the parts of an HTTP request and response. What lives in the start line, headers, and body?

**What it tests.** Understanding the concrete structure of an HTTP message — the thing every fetch, form post, and API call actually sends.

**Approach & answer.** An HTTP message has three parts: a START LINE, a block of HEADERS, and an optional BODY, separated by blank lines. A REQUEST's start line is method + path + version — 'GET /users/42 HTTP/1.1'. Then request headers: Host (which site — required in HTTP/1.1 so one IP can serve many domains), Accept (what content types the client wants back), Content-Type (the format of the body being sent, e.g. application/json), Content-Length, Authorization (credentials, e.g. a Bearer token), Cookie (stored cookies for this origin), User-Agent, and conditional headers like If-None-Match (with an ETag) for caching. The body carries the payload for methods that send data (POST/PUT/PATCH) — JSON, form-encoded fields, or a file; GET/HEAD requests normally have no body. A RESPONSE's start line is version + status code + reason phrase — 'HTTP/1.1 200 OK'. Then response headers: Content-Type (how to interpret the body), Content-Length, Cache-Control / ETag / Expires (caching directives), Set-Cookie (ask the browser to store a cookie), Location (redirect target or created-resource URL), and security headers like Content-Security-Policy, Strict-Transport-Security, and X-Frame-Options. The body is the returned representation — the HTML page, the JSON, the image bytes. Two things worth internalizing: headers are metadata ABOUT the message (who, what format, how to cache, auth, cookies), while the body is the actual content; and header NAMES are case-insensitive. Because headers drive so much behavior — caching, content negotiation, auth, security — reading them in DevTools' Network tab is the first move when debugging almost any request/response problem.

**Use this technique when.** Reading the Network tab; constructing a request by hand; understanding what a header does.

```text
REQUEST                          RESPONSE
-------------------------------  -------------------------------
GET /users/42 HTTP/1.1           HTTP/1.1 200 OK           <- start line
Host: api.example.com            Content-Type: application/json
Accept: application/json         Content-Length: 68
Authorization: Bearer eyJ...     Cache-Control: max-age=60
Cookie: sid=abc123               ETag: "9f2-a1"
If-None-Match: "9f2-a0"          Set-Cookie: sid=abc123; HttpOnly
                                 
(no body for GET)                {"id":42,"name":"Ada"}    <- body

Headers = metadata about the message; body = the content. Names are case-insensitive.
```

**References.** [MDN · HTTP Messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages) · [MDN · HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)

---

### 3. HTTPS and TLS: what encryption buys you  `Easy`

**Pattern:** Transport Security

**Problem.** What does HTTPS actually protect, and what does the TLS handshake establish? What does it NOT protect?

**What it tests.** Understanding the three guarantees of TLS and the common misconception that 'HTTPS = safe'.

**Approach & answer.** HTTPS is HTTP carried over TLS (Transport Layer Security). TLS provides three guarantees for data in transit: (1) CONFIDENTIALITY — the traffic is encrypted, so a network eavesdropper (someone on the same Wi-Fi, an ISP, a proxy) sees ciphertext, not your URLs' paths, headers, cookies, or bodies; (2) INTEGRITY — tampering is detected, so an attacker can't silently modify the page or inject content in flight; (3) AUTHENTICATION of the server — the certificate, signed by a trusted Certificate Authority, proves you're actually talking to example.com and not an impostor, which defeats man-in-the-middle attacks. The handshake establishes this: the client and server negotiate a cipher, the server presents its certificate (which the client validates against trusted CAs and checks the domain/expiry), and they derive a shared symmetric session key (modern TLS 1.3 does this in one round trip, using ephemeral keys for forward secrecy so a later key compromise can't decrypt past traffic). After the handshake, the actual HTTP flows encrypted with that fast symmetric key. Crucially, HTTPS protects data IN TRANSIT ONLY — it says nothing about what happens at the endpoints. It does NOT mean the site is trustworthy or safe (a phishing site can have a valid certificate — the padlock means 'encrypted connection to this domain', not 'good site'), it doesn't protect data once it's decrypted on the server or sitting in your browser, and it doesn't hide WHICH site you visited (the domain leaks via DNS and the TLS SNI field, though the path and content are hidden). Related pieces: HSTS (Strict-Transport-Security header) forces browsers to always use HTTPS for a domain, preventing downgrade attacks and protocol stripping. The practical takeaway: HTTPS is table stakes and protects the pipe, but 'has a padlock' is not the same as 'is safe to trust'.

**Use this technique when.** Explaining why HTTPS matters; correcting 'the padlock means it's safe'; reasoning about MITM.

```text
TLS gives you, for data IN TRANSIT:
  1. Confidentiality  eavesdropper sees ciphertext (not paths, cookies, bodies)
  2. Integrity        tampering in flight is detected
  3. Authentication   the CA-signed cert proves you're talking to the real domain

Handshake (TLS 1.3, ~1 round trip):
  negotiate cipher -> validate server cert -> derive shared session key
  (ephemeral keys => forward secrecy)

Does NOT mean:
  - the site is trustworthy (phishing sites can have valid certs)
  - your data is safe once decrypted at the endpoints
  - the DOMAIN is hidden (leaks via DNS + TLS SNI); path & body are hidden
HSTS header forces HTTPS-only, blocking downgrade/stripping attacks.
```

**References.** [MDN · What is HTTPS / TLS](https://developer.mozilla.org/en-US/docs/Glossary/HTTPS) · [Cloudflare · What happens in a TLS handshake?](https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/)

---

### 4. Cookie flags and session security  `Easy`

**Pattern:** Cookies & Sessions

**Problem.** You store a session id in a cookie. Which cookie attributes make it secure, and what does each defend against?

**What it tests.** Knowing the security-relevant cookie attributes and mapping each to the attack it mitigates.

**Approach & answer.** A session cookie is a prime target, and its ATTRIBUTES are your first line of defense — each maps to a specific threat. HttpOnly: the cookie is invisible to JavaScript (document.cookie can't read it), so if an attacker manages to run script on your page (XSS), they still can't STEAL the session cookie. Every session/auth cookie should be HttpOnly. Secure: the cookie is only ever sent over HTTPS, so it can't leak over a plaintext connection an eavesdropper could read. SameSite: controls whether the cookie is attached to CROSS-SITE requests, and it's the main defense against CSRF. SameSite=Strict never sends the cookie on cross-site navigations (safest, but breaks 'click a link from email and stay logged in'); SameSite=Lax (the modern browser default) sends it on top-level navigations but not on cross-site subrequests like a hidden form POST or an image — blocking the classic CSRF vector while keeping normal links working; SameSite=None means send it cross-site (needed for legitimate third-party contexts) but browsers require Secure with it. Beyond flags: Domain and Path scope WHERE the cookie is sent (keep them tight — don't scope a session cookie to a parent domain that subdomains you don't control can see); Expires/Max-Age control lifetime (a session cookie with no expiry dies when the browser closes; long-lived cookies are more exposure); and a __Host- name prefix enforces Secure + no Domain + Path=/ for extra hardening. The combination that matters for a session id: HttpOnly (blocks theft via XSS) + Secure (blocks leak over HTTP) + SameSite=Lax or Strict (blocks CSRF). Getting these three right neutralizes the most common cookie attacks; forgetting HttpOnly, in particular, turns any XSS into instant session hijacking.

**Use this technique when.** Setting a session/auth cookie; reviewing why a cookie is exploitable; explaining CSRF/XSS cookie defenses.

```text
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600

Attribute      Defends against
-------------  -------------------------------------------------------
HttpOnly       JS can't read it -> XSS can't STEAL the session cookie
Secure         sent only over HTTPS -> no leak over plaintext
SameSite=Lax   not sent on cross-site subrequests -> blocks CSRF
SameSite=Strict strongest CSRF defense (breaks cross-site link login)
Domain/Path    scope tightly so it isn't exposed more widely than needed
__Host- prefix forces Secure + Path=/ + no Domain (extra hardening)

Session id essentials: HttpOnly + Secure + SameSite. Forgetting HttpOnly
turns any XSS into instant session hijacking.
```

**References.** [MDN · Set-Cookie / cookie attributes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) · [OWASP · Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)

---

### 5. The same-origin policy  `Easy`

**Pattern:** Browser Security Model

**Problem.** What is the same-origin policy, what counts as an 'origin', and why does the web need it?

**What it tests.** Understanding the foundational browser isolation boundary that everything else (CORS, cookies, XSS impact) builds on.

**Approach & answer.** The same-origin policy (SOP) is the browser's foundational security boundary: script running on one ORIGIN cannot read data from a different origin. An ORIGIN is the triple of SCHEME + HOST + PORT — https://app.example.com:443. All three must match to be 'same-origin'; https vs http differs, app. vs api. differs, :443 vs :3000 differs. So https://example.com and http://example.com are different origins, as are example.com and www.example.com. Why the web needs it: browsers routinely hold your authenticated state for many sites at once (cookies, sessions). Without SOP, a malicious page you open in one tab could script requests to your bank in another tab, read the responses (which include your logged-in data because the browser attaches your cookies), and exfiltrate them — total cross-site data theft. SOP prevents that by isolating origins: evil.com's JavaScript cannot read the response from bank.com, cannot read bank.com's cookies or localStorage, and cannot reach into a cross-origin iframe's DOM. What SOP restricts is READING cross-origin responses via script; it does NOT block all cross-origin activity — the browser still SENDS many cross-origin requests (loading an <img>, <script>, <link> stylesheet, or submitting a form to another site all work, which is exactly why CSRF is possible). It's specifically the programmatic READING of the response, and access to another origin's DOM/storage, that's blocked. SOP is the default deny; CORS is the controlled, server-opt-in mechanism to RELAX it for specific cross-origin reads. Understanding SOP also explains the blast radius of XSS: because script runs WITH the origin's privileges, an attacker who injects script into your origin inherits full same-origin access — which is why XSS is so damaging and why the origin boundary is the thing you're protecting.

**Use this technique when.** Reasoning about cross-origin access; explaining why CORS exists; scoping the impact of XSS.

```text
Origin = scheme + host + port   (all three must match)

  https://app.example.com:443  vs
  ----------------------------------------------------
  https://app.example.com          SAME (default :443)
  http://app.example.com           DIFFERENT (scheme)
  https://api.example.com          DIFFERENT (host)
  https://app.example.com:3000     DIFFERENT (port)

SOP blocks: reading cross-origin RESPONSES via script; reading another
            origin's DOM, cookies, localStorage.
SOP allows: SENDING cross-origin requests (<img>,<script>,<form>) -> why CSRF exists.
Default-deny; CORS is the server's opt-in to relax it.
```

**References.** [MDN · Same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) · [MDN · Origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin)

---

### 6. CORS and the preflight request  `Easy`

**Pattern:** Browser Security Model

**Problem.** What problem does CORS solve, and how does the preflight request work? Which headers matter?

**What it tests.** Understanding CORS as a server-controlled relaxation of SOP, and the mechanics of preflight most developers hit.

**Approach & answer.** CORS (Cross-Origin Resource Sharing) is the HTTP-header mechanism by which a SERVER opts in to letting specific other origins READ its responses — a controlled relaxation of the same-origin policy. Without CORS, a browser will SEND a cross-origin request (say, app.com's JS fetching api.other.com) but will BLOCK the JavaScript from reading the response unless the server says it's allowed. The server signals permission with response headers, chiefly Access-Control-Allow-Origin: either the specific requesting origin (echoed back) or '*' (any origin). Crucial subtlety: for requests that send CREDENTIALS (cookies, Authorization), the server must send Access-Control-Allow-Credentials: true AND cannot use '*' for the origin — it must name the exact origin. For 'non-simple' requests — anything using methods beyond GET/POST/HEAD, or custom headers, or a Content-Type like application/json — the browser first sends a PREFLIGHT: an automatic OPTIONS request asking 'may I make this actual request?', carrying Access-Control-Request-Method and Access-Control-Request-Headers. The server responds (with no body) listing what it permits via Access-Control-Allow-Methods, Access-Control-Allow-Headers, and optionally Access-Control-Max-Age (how long the browser may cache this preflight so it doesn't re-ask every time). Only if the preflight approves does the browser send the real request. 'Simple' requests (GET/POST with standard headers and form/text content types) skip preflight. The mental model that clears up most confusion: CORS is enforced BY THE BROWSER to protect the USER, and it's granted BY THE SERVER; it is NOT a server-side access control (a non-browser client like curl ignores CORS entirely). So CORS errors are the browser refusing to hand YOUR script a cross-origin response the server didn't authorize — the fix is on the SERVER (send the right Allow headers), never 'disable CORS in the browser'.

**Use this technique when.** Debugging a CORS error; designing an API consumed cross-origin; deciding when preflight fires.

```text
Non-simple request (e.g. JSON PUT) triggers a PREFLIGHT:

  Browser  --OPTIONS-->  Server
     Access-Control-Request-Method: PUT
     Access-Control-Request-Headers: content-type

  Server   --200------>  Browser   (no body)
     Access-Control-Allow-Origin: https://app.example.com
     Access-Control-Allow-Methods: GET, PUT
     Access-Control-Allow-Headers: content-type
     Access-Control-Max-Age: 600      (cache the preflight)

  ...then the REAL PUT is sent.

Credentialed requests: Allow-Credentials: true AND a specific origin (never '*').
CORS is enforced by the BROWSER, granted by the SERVER. curl ignores it.
Fix CORS errors on the SERVER, not the browser.
```

**References.** [MDN · Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) · [MDN · Preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request)

---

### 7. Safe and idempotent methods: GET vs POST  `Easy`

**Pattern:** HTTP Semantics

**Problem.** What do 'safe' and 'idempotent' mean for HTTP methods, and why do these properties matter in practice?

**What it tests.** Understanding method semantics that browsers, caches, and retry logic depend on — not just 'GET reads, POST writes'.

**Approach & answer.** Two properties define how a method is allowed to behave. SAFE means the request has no side effects on the server — it only reads. GET, HEAD, and OPTIONS are safe. IDEMPOTENT means making the request N times has the SAME effect on server state as making it once. GET, HEAD, PUT, and DELETE are idempotent; POST and PATCH are generally NOT. Note safe implies idempotent (reading twice changes nothing), but idempotent does not imply safe (DELETE changes state, but deleting twice leaves the same 'gone' state as deleting once). Why these matter beyond trivia: (1) CACHING — safe methods (GET) can be cached by browsers, CDNs, and proxies because they don't change anything; POST responses generally aren't cached. Putting a state-changing action behind GET is a real bug: a prefetcher, a crawler, a browser preloader, or a cache can fire it unexpectedly (the classic 'a search-engine bot deleted our records by following GET /delete?id=5 links'). (2) RETRIES — clients, proxies, and load balancers safely RETRY idempotent requests after a network hiccup because a duplicate is harmless; they must NOT blindly retry a non-idempotent POST, or you get double charges / double orders. This is exactly why 'submit payment' is a POST and why real systems add an IDEMPOTENCY KEY so even a POST can be safely retried without duplicating the effect. (3) Browser behavior — refreshing or navigating back to a POST prompts 'resubmit form?' precisely because POST isn't idempotent. So the practical rules: use GET only for reads (never for actions), use POST for non-idempotent creates/actions, use PUT/DELETE when the operation genuinely is idempotent, and design write endpoints so retries don't double-apply. Choosing the method by its semantics — not just 'GET vs POST' by habit — is what lets the whole caching/retry infrastructure around your app behave correctly.

**Use this technique when.** Choosing a method for an endpoint; deciding what a client may retry; avoiding action-behind-GET bugs.

```text
Method   Safe?  Idempotent?   Notes
-------  -----  -----------   ---------------------------------
GET      yes    yes           cacheable; reads only
HEAD     yes    yes           GET without a body
PUT      no     yes           full replace; retry-safe
DELETE   no     yes           deleting twice == deleted once
PATCH    no     no*           partial update
POST     no     no            create/action; NOT retry-safe

Consequences:
  * caches/prefetchers may fire GET -> never hide an action behind GET
  * proxies/clients retry idempotent methods -> POST needs an idempotency key
  * browser "resubmit form?" prompt exists because POST isn't idempotent
```

**References.** [MDN · Idempotent](https://developer.mozilla.org/en-US/docs/Glossary/Idempotent) · [MDN · Safe (HTTP methods)](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP)

---

### 8. Conditional requests: ETag, Last-Modified, and 304  `Easy`

**Pattern:** Caching

**Problem.** How do conditional requests work? Explain ETag / If-None-Match and the 304 Not Modified response.

**What it tests.** Understanding HTTP validation — how the browser confirms a cached copy is still fresh without re-downloading it.

**Approach & answer.** Conditional requests let a client REVALIDATE a cached resource cheaply: instead of re-downloading, it asks 'has this changed since the version I have?' and the server answers with either the new content or a tiny '304 Not Modified' meaning 'your copy is still good'. Two validators drive this. An ETAG is an opaque identifier the server assigns to a specific version of a resource (often a hash or version tag), sent in the response as ETag: "abc123". On the next request the browser sends If-None-Match: "abc123"; if the server's current ETag still matches, it replies 304 with no body, and the browser uses its cached copy. If it differs, the server sends 200 with the new body and a new ETag. LAST-MODIFIED is the time-based equivalent: the server sends Last-Modified: <date>, and the browser revalidates with If-Modified-Since: <date>; the server returns 304 if unchanged or 200 with fresh content otherwise. ETags are more precise than timestamps (they catch sub-second changes and content that changed then changed back to identical bytes — a 'strong' ETag can even signal byte-identical content). Why this matters: revalidation saves BANDWIDTH and time — a 304 is a few bytes of headers versus re-transferring a whole file — while still guaranteeing freshness, which is the sweet spot between 'always re-download' (wasteful) and 'trust the cache blindly' (risks staleness). This complements, rather than replaces, freshness caching via Cache-Control: max-age tells the browser it can use the cached copy WITHOUT even asking for a period; once that expires (or with no-cache / must-revalidate), the conditional request kicks in to check before reusing. The typical strong setup pairs long max-age with content-hashed filenames for static assets (never revalidate until the URL changes) and short max-age + ETag for dynamic resources (cheap revalidation). So 304 is the browser and server agreeing 'nothing changed, don't waste the transfer'.

**Use this technique when.** Explaining a 304 in the Network tab; designing cache revalidation; reducing redundant transfers.

```text
First response:              Next request (revalidate):
  200 OK                        GET /logo.png HTTP/1.1
  ETag: "abc123"                If-None-Match: "abc123"
  Cache-Control: max-age=60
  <image bytes>              Server compares current ETag:
                               unchanged -> 304 Not Modified (no body, use cache)
                               changed   -> 200 OK + new bytes + new ETag

Last-Modified / If-Modified-Since = the time-based equivalent (less precise).
max-age = use cache WITHOUT asking; conditional request = cheap check when it expires.
304 = "nothing changed, skip the transfer" (a few header bytes vs the whole file).
```

**References.** [MDN · HTTP conditional requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests) · [MDN · ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)

---

### 9. Cross-site scripting (XSS): types and defenses  `Medium`

**Pattern:** Web Vulnerabilities

**Problem.** Explain the types of XSS and the layered defenses. Why is escaping context-dependent, and where does CSP fit?

**What it tests.** Understanding the web's most pervasive vulnerability class end-to-end: how injection happens, the three variants, and the defense-in-depth stack.

**Approach & answer.** XSS is when an attacker gets their JavaScript to run in a victim's browser IN YOUR ORIGIN'S CONTEXT — which, because of the same-origin policy, means that script inherits full access to your page: it can read the DOM, steal non-HttpOnly cookies and tokens, make authenticated requests as the user, keylog, and rewrite the page. There are three types. STORED (persistent) XSS: the payload is saved on the server (a comment, a profile field) and served to every viewer — the most dangerous because it's wormable and hits many users. REFLECTED XSS: the payload rides in the request (a query param, form field) and is echoed straight back into the response, so the attacker must lure the victim to a crafted URL. DOM-BASED XSS: the vulnerability is entirely client-side — JS reads attacker-controlled input (location.hash, a URL param) and writes it into a sink like innerHTML or eval without sanitizing, so the payload may never touch the server. The core defense is CONTEXT-AWARE OUTPUT ENCODING: escape untrusted data for the exact place it lands, because the rules differ — HTML body context needs &lt; &gt; &amp; escaping; an HTML attribute needs attribute-encoding and quoting; inside a <script> or a URL or a CSS context the rules change again, and HTML-escaping alone won't save you. In practice: prefer APIs that don't parse HTML — textContent, setAttribute, and framework text bindings ({value} in React, which auto-escapes) treat input as data, not markup. AVOID the dangerous sinks: innerHTML, outerHTML, document.write, eval, and React's dangerouslySetInnerHTML. When you genuinely must render user-supplied HTML (a rich-text field), run it through a vetted SANITIZER (DOMPurify) with an allowlist — never a hand-rolled blocklist. Then layer defenses that limit the blast radius even if something slips through: a Content Security Policy that forbids inline scripts and restricts script sources turns many injections into no-ops; HttpOnly cookies keep session tokens unreadable by injected script; Trusted Types (where supported) make dangerous sinks refuse raw strings. No single layer is sufficient — escaping is the primary control, CSP and HttpOnly are the safety net, and a sanitizer covers the deliberate-HTML case.

**Use this technique when.** Reviewing code that renders user input; designing an XSS defense strategy; explaining why a sink is dangerous.

```js
// DANGEROUS — parses the string as HTML, executes injected script
el.innerHTML = userInput;                 // stored/reflected/DOM XSS sink
container.insertAdjacentHTML('beforeend', userInput);
element.setAttribute('onclick', userInput);
// React equivalent:
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// SAFE — treat input as DATA, not markup
el.textContent = userInput;               // no parsing, no execution
el.setAttribute('title', userInput);      // attribute value, encoded
<div>{userInput}</div>                     // React auto-escapes

// Must render real HTML? Sanitize with an allowlist:
el.innerHTML = DOMPurify.sanitize(userHtml);

// Defense in depth (server response header):
//   Content-Security-Policy: default-src 'self'; script-src 'self'
//   -> inline & injected scripts won't run even if one slips through
// Plus: HttpOnly cookies so injected JS can't read the session token.
```

**References.** [OWASP · XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) · [MDN · Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)

---

### 10. CSRF: how it works and how to stop it  `Medium`

**Pattern:** Web Vulnerabilities

**Problem.** Explain a cross-site request forgery attack step by step, and the modern defenses. How does SameSite change the picture?

**What it tests.** Understanding CSRF as an abuse of ambient cookie authority, and why SameSite + tokens together are the answer.

**Approach & answer.** CSRF (cross-site request forgery) tricks a logged-in user's browser into sending a state-changing request to a site they're authenticated with, WITHOUT the attacker ever seeing the response. It exploits a specific fact: the browser attaches your cookies to a request based on the DESTINATION, regardless of which site initiated it. So the flow is: you're logged into bank.com (you have a session cookie). You visit evil.com, which contains a hidden auto-submitting form that POSTs to bank.com/transfer with attacker-chosen params. Your browser sends that POST and — because it's going to bank.com — automatically includes your bank.com session cookie. The server sees a valid, authenticated request and executes the transfer. The attacker doesn't need to read anything (the same-origin policy still blocks that); they just need the side effect. Note CSRF only works against actions authenticated by AMBIENT credentials the browser sends automatically — cookies (and HTTP Basic/NTLM). It does NOT work against auth that requires the app to actively attach a token, like an Authorization: Bearer header from JS, because evil.com's request can't add that header. Defenses, layered: (1) SameSite cookies — SameSite=Lax (now the browser default) stops the cookie from being sent on cross-site subrequests like that hidden POST, which neutralizes the classic attack; Strict is even tighter. This is the biggest single improvement and is why CSRF is less pervasive than it once was. (2) Anti-CSRF TOKENS — the server embeds an unpredictable, per-session (or per-request) token in forms/pages; legitimate requests echo it back (in a hidden field or header), and evil.com can't read or guess it (SOP blocks reading the page). The synchronizer-token and double-submit-cookie patterns implement this. (3) Verifying Origin/Referer headers on state-changing requests as a secondary check. Don't rely on SameSite alone (older browsers, and some cross-site flows legitimately need None), and never assume 'it's a POST so it's safe' — POSTs are exactly what CSRF targets. The robust posture: SameSite cookies as the baseline PLUS anti-CSRF tokens for state-changing endpoints.

**Use this technique when.** Securing state-changing endpoints; explaining why a POST needs a token; choosing SameSite settings.

```html
<!-- The attack: evil.com auto-submits a request to a site you're logged into -->
<form action="https://bank.com/transfer" method="POST" id="f">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('f').submit();</script>
<!-- Browser attaches YOUR bank.com cookie because the request GOES TO bank.com.
     Attacker never reads the response (SOP blocks that) — the side effect is enough. -->

<!-- Defenses (layered) -->
<!-- 1. Cookie: sid=...; SameSite=Lax  -> not sent on this cross-site POST -->
<!-- 2. Anti-CSRF token the attacker can't read or guess: -->
<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="{{ per-session unpredictable token }}">
  ...
</form>
<!-- 3. Server also checks Origin/Referer on state-changing requests. -->
```

**References.** [OWASP · CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) · [MDN · SameSite cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)

---

### 11. Authentication vs authorization; sessions vs tokens  `Medium`

**Pattern:** Authentication

**Problem.** Distinguish authentication from authorization, then compare session-cookie auth with token (JWT) auth. What are the trade-offs?

**What it tests.** Getting the two 'auth' concepts straight, and understanding the real trade-offs between stateful sessions and stateless tokens.

**Approach & answer.** AUTHENTICATION (authN) is proving WHO you are — logging in with a password, passkey, or OAuth. AUTHORIZATION (authZ) is deciding WHAT you're allowed to do once identified — roles, permissions, ownership checks. They're sequential and distinct: you authenticate once, then every protected action requires an authorization check. The classic status codes map to them: 401 = not authenticated, 403 = authenticated but not authorized. A common security bug is doing authN but skimping on authZ — e.g. trusting a resource id from the client without checking the logged-in user actually owns it (IDOR / broken object-level authorization). Now, two ways to carry the authenticated identity across requests. SESSION-COOKIE (stateful): on login the server creates a session, stores its state server-side (in memory, Redis, a DB), and sends the client an opaque session id in a cookie. Each request carries the cookie; the server looks up the session. Pros: the server can INVALIDATE a session instantly (logout, ban, password change just delete it); the cookie is opaque so no sensitive data leaves the server; with HttpOnly+Secure+SameSite it's well-defended. Cons: requires server-side session storage (a scaling/sharing concern across many servers); cookies bring CSRF exposure (mitigated by SameSite). TOKEN / JWT (stateless): on login the server issues a signed JSON Web Token containing claims (user id, roles, expiry); the client stores it and sends it, typically as Authorization: Bearer <token>. The server VERIFIES THE SIGNATURE and trusts the claims without a lookup. Pros: stateless and horizontally scalable (any server can verify with the key, no shared session store), natural for APIs and cross-service auth. Cons: REVOCATION is hard — a valid signed token works until it expires, so 'log out everywhere' / instant ban needs extra machinery (short lifetimes + refresh tokens, or a server-side denylist, which reintroduces state); tokens can be bloated; and if stored in localStorage they're readable by XSS. The pragmatic pattern many apps land on: short-lived access tokens for statelessness PLUS a long-lived refresh token stored in an HttpOnly cookie, giving you scalability with a revocation point — or simply session cookies for a classic server-rendered app, since they're simpler and safely revocable. The right choice is about revocation needs and architecture, not fashion.

**Use this technique when.** Designing an auth system; choosing sessions vs tokens; explaining a 401 vs 403 or an IDOR bug.

```text
AuthN = who are you (login)           401 = not authenticated
AuthZ = what may you do (permissions) 403 = authenticated, not allowed
  (always re-check ownership per request -> avoid IDOR)

                 Session cookie (stateful)   JWT / token (stateless)
Storage          server-side session store   nothing server-side (signed)
Carried in       Cookie (auto-sent)          Authorization: Bearer (JS-attached)
Revocation       instant (delete session)    hard (valid until expiry)
Scaling          needs shared session store  any server verifies with the key
CSRF             exposed (mitigate: SameSite) not via ambient cookie
XSS token theft  HttpOnly cookie -> no        localStorage -> yes

Common hybrid: short-lived access token + refresh token in an HttpOnly cookie.
```

**References.** [MDN · Authentication](https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Authentication) · [OWASP · JWT / Session cheat sheets](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html)

---

### 12. HTTP/1.1 vs HTTP/2 vs HTTP/3  `Medium`

**Pattern:** Protocol Evolution

**Problem.** Compare HTTP/1.1, HTTP/2, and HTTP/3. What problem does each version solve, and what is head-of-line blocking?

**What it tests.** Understanding how the protocol evolved to fix connection/latency bottlenecks, and where head-of-line blocking moves at each step.

**Approach & answer.** Each HTTP version attacks the previous one's bottleneck. HTTP/1.1 sends one request at a time per TCP connection: a response must complete before the next request on that connection starts. This is APPLICATION-LAYER head-of-line (HOL) blocking — a slow response stalls everything queued behind it. The historical workaround was opening 6-ish parallel connections per origin (costly: each needs its own TCP + TLS handshake) plus hacks like domain sharding, spriting, and concatenating files to reduce request count. HTTP/2 introduced MULTIPLEXING: many concurrent request/response STREAMS interleaved over a SINGLE TCP connection, using binary framing. That kills application-layer HOL blocking and removes the need for sharding/spriting (one connection, many parallel streams). It also added HEADER COMPRESSION (HPACK — headers repeat heavily across requests) and server push (largely deprecated in practice). But HTTP/2 still runs on TCP, and TCP guarantees in-order delivery of its byte stream — so if ONE packet is lost, TCP holds back ALL streams' data until it's retransmitted, even streams that had no loss. That's TCP-level (transport) HOL blocking: H2 solved it at the app layer but TCP reintroduced it underneath, and it bites hardest on lossy/mobile networks. HTTP/3 fixes that by abandoning TCP for QUIC, a protocol built on UDP. QUIC implements streams itself with INDEPENDENT loss recovery, so a lost packet only stalls the stream it belongs to, not the others — finally eliminating transport HOL blocking. QUIC also folds the transport + TLS 1.3 handshake together for faster (often 1-RTT, or 0-RTT on resumption) connection setup, and its connection ids let a connection survive network changes (Wi-Fi to cellular) without re-handshaking. The practical implications for frontend: with H2/H3 the old 'bundle everything into one file' advice weakens — many smaller cacheable files parallelize fine — though there's still per-request overhead, so extreme fragmentation isn't free. You generally get H2/H3 for free from your CDN/host; the main thing to know is WHY (multiplexing removed the connection-count tax, QUIC removed the last HOL bottleneck).

**Use this technique when.** Explaining why bundling advice changed; reasoning about multiplexing; discussing HOL blocking.

```text
HTTP/1.1  one request at a time per connection
          -> APP-layer HOL blocking; workaround: ~6 connections + sharding/spriting

HTTP/2    multiplexed streams over ONE TCP connection + HPACK header compression
          -> removes app-layer HOL blocking & the connection-count tax
          -> BUT still on TCP: one lost packet stalls ALL streams (TCP-level HOL)

HTTP/3    QUIC (over UDP) with per-stream loss recovery + TLS 1.3 built in
          -> lost packet stalls only its own stream (no transport HOL)
          -> faster handshake (1-RTT / 0-RTT), connection survives network switch

Frontend takeaway: with H2/H3, "bundle it all into one file" matters far less —
many small cacheable files parallelize well.
```

**References.** [MDN · Evolution of HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Evolution_of_HTTP) · [Cloudflare · HTTP/3 and QUIC](https://www.cloudflare.com/learning/performance/what-is-http3/)

---

### 13. DNS resolution and the request lifecycle  `Medium`

**Pattern:** Request Lifecycle

**Problem.** Trace what the network does when a browser needs to reach example.com: DNS resolution through the first bytes of the response. Where does caching happen?

**What it tests.** End-to-end grasp of name resolution and connection setup — the layers beneath fetch() that determine latency.

**Approach & answer.** Before any HTTP flows, the browser must turn the hostname into an IP address (DNS) and open a connection. DNS RESOLUTION walks a cache hierarchy, stopping at the first hit: (1) the browser's own DNS cache; (2) the OS resolver cache (and the hosts file); (3) the configured RECURSIVE RESOLVER (your ISP's, or 1.1.1.1 / 8.8.8.8), which itself caches. On a full miss the recursive resolver does the iterative lookup: ask a ROOT server (which delegates to the .com TLD servers), ask the TLD server (which returns the AUTHORITATIVE nameserver for example.com), then ask that authoritative server for the A/AAAA record. Each record carries a TTL that governs how long every layer may cache it — which is why DNS changes take time to propagate and why a low TTL is set before a planned migration. (DNS traditionally runs over UDP port 53; DNS-over-HTTPS/TLS now encrypts it.) With an IP in hand the browser sets up the connection: a TCP handshake (SYN, SYN-ACK, ACK — one round trip), then for HTTPS a TLS handshake (validate the certificate, derive session keys — one more round trip in TLS 1.3, more in older versions). Only now does the browser send the actual HTTP request; the server processes it and streams back the response, whose first byte you see as TTFB (time to first byte). Then the real cost surfaces: the browser parses the HTML and discovers subresources (CSS, JS, images), each of which may need its own DNS + connection unless it's same-origin or the connection is reused. This is why latency optimizations target these steps: DNS PREFETCH and PRECONNECT warm up resolution/handshakes for known third-party origins before they're needed; keep-alive and HTTP/2/3 REUSE one connection for many requests instead of paying the handshake tax repeatedly; a CDN shortens every leg by putting the server (and often the DNS answer) physically closer. The mental model: reaching a server is DNS lookup (cached at several layers) → TCP → TLS → request → response, and each round trip is latency you can sometimes cache away or parallelize, but never wish away entirely.

**Use this technique when.** Explaining latency sources; justifying preconnect/dns-prefetch; understanding DNS propagation and TTL.

```text
Reaching example.com:

1. DNS resolution (first cache hit wins):
     browser cache -> OS cache/hosts -> recursive resolver (ISP/1.1.1.1)
     full miss: root -> .com TLD -> example.com authoritative NS -> A/AAAA
     each record cached per its TTL (why changes "propagate" slowly)

2. TCP handshake      SYN / SYN-ACK / ACK        (~1 RTT)
3. TLS handshake      cert validation + keys     (~1 RTT in TLS 1.3)
4. HTTP request  -->  server processes  -->  response (first byte = TTFB)
5. parse HTML, discover CSS/JS/img -> more DNS+connections (or reuse)

Speedups: dns-prefetch / preconnect warm steps 1-3 for third-party origins;
keep-alive + HTTP/2/3 reuse one connection; a CDN shortens every leg.
```

**References.** [MDN · DNS](https://developer.mozilla.org/en-US/docs/Glossary/DNS) · [Cloudflare · What is DNS?](https://www.cloudflare.com/learning/dns/what-is-dns/)

---

### 14. Content Security Policy in depth  `Hard`

**Pattern:** Security Headers

**Problem.** Design a Content Security Policy for a modern app. Explain nonces vs hashes, strict-dynamic, and common bypasses of weak policies.

**What it tests.** Deep understanding of CSP as an XSS mitigation layer — how to write one that actually holds, and how weak ones get bypassed.

**Approach & answer.** CSP is a response header that tells the browser which sources of script, style, images, etc. are allowed to load and execute — a defense-in-depth net that limits what an injected script can do even if XSS gets past your escaping. The naive approach, an allowlist like script-src 'self' cdn.example.com, is widely BYPASSABLE: if any allowlisted host serves a JSONP endpoint, a vulnerable AngularJS build, or user-uploaded content, an attacker abuses it; and allowlists don't stop injected inline event handlers unless you also forbid them. The modern, robust approach is a NONCE-based (or hash-based) STRICT CSP. A NONCE is a random value generated PER RESPONSE, put in the header (script-src 'nonce-r4nd0m') and echoed as an attribute on every legitimate <script nonce="r4nd0m">. The browser runs only scripts bearing the current nonce; an injected <script> from the attacker has no valid nonce (it can't predict the per-request random value, and SOP stops it reading the page), so it won't execute. The nonce MUST be cryptographically random and unique per response — a static or reused nonce is worthless. HASHES are the alternative for scripts whose content is fixed: you put the SHA-256 of the exact inline script in the policy (script-src 'sha256-...'); good for static inline blocks where a nonce is awkward. The problem nonces create: scripts loaded dynamically by your legitimate code (a script that injects another script) won't carry the nonce. 'strict-dynamic' solves this: it says 'trust scripts loaded by an already-trusted (nonced/hashed) script, and ignore host allowlists entirely'. So a solid modern policy is: script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none' — where 'unsafe-inline' and https: are IGNORED by CSP3 browsers that honor the nonce (they exist only as fallback for old browsers), object-src 'none' kills Flash/plugin vectors, and base-uri 'none' blocks <base> tag injection that could hijack relative script URLs. Additional hardening: report-uri/report-to to collect violation reports (deploy in Content-Security-Policy-Report-Only first to find breakage before enforcing), and remember CSP is a SECOND layer — it reduces impact, it doesn't replace output encoding. Common bypasses to avoid: 'unsafe-inline' without a nonce (defeats the point), 'unsafe-eval' (re-enables eval/Function), overly broad allowlists, and dangling-markup / base-uri gaps.

**Use this technique when.** Writing or auditing a CSP; explaining nonces vs hashes; hardening against XSS beyond escaping.

```text
Weak (bypassable) allowlist policy:
  Content-Security-Policy: script-src 'self' cdn.example.com 'unsafe-inline'
  -> JSONP/old-lib on an allowed host, or any inline, defeats it.

Strict, nonce-based policy (per response):
  Content-Security-Policy:
    script-src 'nonce-r4nd0mPerResponse' 'strict-dynamic' https: 'unsafe-inline';
    object-src 'none';
    base-uri 'none';
    report-to csp-endpoint

  <script nonce="r4nd0mPerResponse" src="/app.js"></script>   <- runs
  <script>stolenPayload()</script>                            <- no nonce -> blocked

  'strict-dynamic' = trust scripts loaded by already-trusted scripts
  'unsafe-inline' + https: = IGNORED by modern browsers (old-browser fallback)

Roll out with Content-Security-Policy-Report-Only first to catch breakage.
CSP is a SECOND layer — it limits XSS impact, it does not replace escaping.
```

**References.** [MDN · Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) · [web.dev · Strict CSP](https://web.dev/articles/strict-csp)

---

### 15. Where to store auth tokens: cookies vs localStorage  `Hard`

**Pattern:** Token Storage

**Problem.** An SPA needs to keep the user logged in across reloads. Compare storing the token in localStorage vs an HttpOnly cookie. What's the secure design?

**What it tests.** Reasoning about the XSS-vs-CSRF trade-off in token storage — a decision juniors get wrong by defaulting to localStorage.

**Approach & answer.** This is a trade-off between two attack classes, and the popular default (localStorage) optimizes for the wrong one. LOCALSTORAGE / sessionStorage: JavaScript reads and writes it, so your SPA can attach the token as an Authorization: Bearer header on each fetch. Because it's not an ambient cookie, it's immune to CSRF (the browser never auto-sends it cross-site). BUT it's fully readable by any JavaScript running on the page — so a SINGLE XSS vulnerability means the attacker's injected script reads the token out of localStorage and exfiltrates it, and now they have the user's credentials to use from anywhere, even after the user closes the tab. XSS token theft is the dominant real-world risk, and localStorage is maximally exposed to it. HTTPONLY COOKIE: the browser stores the token and sends it automatically, but document.cookie CANNOT read it — so even with XSS, the attacker's script can't exfiltrate the token itself (they can still make requests AS the user while the page is open, but they can't steal the durable credential to reuse elsewhere). The cost: because it's an ambient cookie, you now have CSRF exposure — which you close with SameSite=Lax/Strict plus anti-CSRF tokens on state-changing endpoints. So the secure design for most apps is: keep the token in an HttpOnly + Secure + SameSite cookie, NOT localStorage. Concretely, a common robust pattern: a short-lived ACCESS token and a long-lived REFRESH token, both in HttpOnly Secure SameSite cookies; the access token authorizes API calls, and when it expires the client hits a /refresh endpoint that rotates it. This gives you: XSS can't steal the durable credential (HttpOnly), traffic is HTTPS-only (Secure), CSRF is blunted (SameSite) and further covered by CSRF tokens, and the server keeps a revocation point (invalidate the refresh token to log out everywhere). If you're forced into Bearer-header/localStorage (e.g. a pure API consumed by native + web, cross-domain constraints), then you MUST compensate: minimize XSS aggressively (strict CSP, sanitize, Trusted Types), keep access-token lifetimes very short, and never store a long-lived refresh token in JS-reachable storage. The one-liner: cookies trade CSRF (which SameSite+tokens fix well) for protection against token THEFT via XSS (which localStorage can't fix at all) — that's usually the better trade.

**Use this technique when.** Designing SPA session persistence; reviewing a 'token in localStorage' choice; weighing XSS vs CSRF.

```text
                     localStorage (Bearer)      HttpOnly cookie
Readable by JS?      YES  -> XSS steals token   NO  -> XSS can't exfiltrate it
CSRF exposure?       none (not auto-sent)       yes -> fix w/ SameSite + CSRF token
Durable theft risk   HIGH (reusable anywhere)   LOW (bound to the browser)

Secure default: token in HttpOnly + Secure + SameSite cookie.
Robust pattern: short-lived access token + refresh token, both HttpOnly cookies;
  /refresh rotates the access token; delete refresh token = log out everywhere.

The trade: cookies swap CSRF (SameSite + tokens fix it) for immunity to
XSS token THEFT (localStorage cannot fix that) — usually the better deal.
```

**References.** [OWASP · HTML5 / storage security](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html) · [MDN · Set-Cookie (HttpOnly, Secure, SameSite)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)

---

### 16. OAuth 2.0 and OIDC: the authorization-code flow with PKCE  `Hard`

**Pattern:** Authentication

**Problem.** Explain the OAuth 2.0 authorization-code flow with PKCE and how OpenID Connect fits in. Why is PKCE required for SPAs and why is the implicit flow dead?

**What it tests.** Understanding delegated authorization end-to-end, the role of PKCE, and OIDC's identity layer — without hand-waving.

**Approach & answer.** OAuth 2.0 is a DELEGATED AUTHORIZATION protocol: it lets a user grant your app limited access to their resources on another service (or lets you 'Sign in with X') WITHOUT the user handing your app their password. OpenID Connect (OIDC) is a thin identity layer ON TOP of OAuth 2.0: OAuth alone gives you an ACCESS TOKEN (authorization to call APIs); OIDC adds an ID TOKEN (a signed JWT asserting WHO the user is — authentication), which is what 'Sign in with Google' actually uses. The AUTHORIZATION-CODE flow is the recommended flow, and PKCE (Proof Key for Code Exchange) hardens it for public clients. Steps: (1) the app generates a random CODE VERIFIER and its SHA-256 hash, the CODE CHALLENGE. (2) It redirects the user to the authorization server's /authorize with client_id, redirect_uri, scope, a state value (CSRF protection for the redirect), and the code_challenge. (3) The user authenticates and consents AT THE AUTH SERVER (your app never sees the password). (4) The auth server redirects back to your redirect_uri with a short-lived AUTHORIZATION CODE (and echoes state, which you verify). (5) The app exchanges that code at the /token endpoint, sending the original code_verifier. (6) The server hashes the verifier, checks it matches the challenge from step 2, and only then returns the access token (+ refresh token, + ID token for OIDC). Why PKCE is essential for SPAs and mobile (public clients that can't keep a client secret): the authorization code travels through the browser/redirect and could be intercepted (a malicious app registering the redirect scheme, logs, referrer leakage). Without PKCE, a stolen code could be redeemed by the attacker. With PKCE, redeeming the code REQUIRES the code_verifier, which never left the original app — so an intercepted code is useless. Why the IMPLICIT flow is dead: it returned the access token DIRECTLY in the redirect URL fragment (no code exchange), which exposed the token in browser history, referrer headers, and logs, and offered no PKCE-style binding — the current OAuth 2.0 Security BCP and OAuth 2.1 deprecate it and mandate authorization-code + PKCE for everyone (SPAs included). Practical notes: validate state (and OIDC nonce) to prevent CSRF/replay on the callback, verify the ID token's signature/issuer/audience/expiry, request least-privilege scopes, and prefer having the auth server set tokens in HttpOnly cookies (or a BFF — backend-for-frontend — pattern) rather than exposing them to SPA JavaScript.

**Use this technique when.** Integrating 'Sign in with X'; explaining PKCE; choosing an OAuth flow for an SPA.

```text
Authorization-Code flow with PKCE (OIDC adds an ID token):

  App: verifier = random(); challenge = SHA256(verifier)

  1. redirect -> /authorize?client_id&redirect_uri&scope&state&code_challenge
  2. user logs in + consents AT THE AUTH SERVER (app never sees password)
  3. redirect back -> redirect_uri?code=AUTH_CODE&state   (verify state)
  4. POST /token  { code: AUTH_CODE, code_verifier: verifier }
  5. server: SHA256(verifier) == challenge ?  -> yes
  6. <- access_token (+ refresh_token) (+ id_token for OIDC = who the user is)

PKCE: an intercepted AUTH_CODE is useless without the verifier (never left the app)
      -> required for SPAs/mobile (public clients, no client secret).

Implicit flow (DEAD): returned the token in the URL fragment -> leaked via
      history/referrer/logs, no PKCE binding. OAuth 2.1 mandates code+PKCE.
Validate state + OIDC nonce; verify id_token sig/iss/aud/exp; least-privilege scopes.
```

**References.** [OAuth 2.0 · Authorization Code + PKCE](https://oauth.net/2/pkce/) · [MDN / OpenID Connect overview](https://developer.mozilla.org/en-US/docs/Web/Security)

---

### 17. Real-time transport: WebSockets vs SSE vs long-polling  `Hard`

**Pattern:** Real-time Transport

**Problem.** You need to push server updates to the browser. Compare long-polling, Server-Sent Events, and WebSockets. How do you choose?

**What it tests.** Choosing the right push mechanism by directionality, infrastructure fit, and operational cost — not defaulting to WebSockets reflexively.

**Approach & answer.** HTTP is request/response — the server can't natively initiate — so 'push' needs one of three techniques, and the right choice depends on directionality and how much machinery you want. LONG-POLLING: the client makes a request and the server HOLDS it open until it has data (or a timeout), then responds; the client immediately re-requests. It emulates push over ordinary HTTP, works everywhere (any proxy, any browser), but each message costs a full request/response cycle plus reconnection overhead, and it scales poorly under high message rates. It's the compatibility fallback. SERVER-SENT EVENTS (SSE): a single long-lived HTTP response streams text events from server to client over the EventSource API. It's ONE-DIRECTIONAL (server → client only), which is exactly right for feeds, notifications, live scores, progress updates. Big wins: it's just HTTP (works with HTTP/2 multiplexing, standard infra, CDNs, auth cookies), it AUTO-RECONNECTS and supports resuming via the Last-Event-ID header, and it's simple to implement. Limits: text-only (UTF-8; binary must be encoded), no client→server channel (you still POST normally for that), and over HTTP/1.1 it consumes a connection from the ~6-per-origin budget (HTTP/2 fixes this via multiplexing). WEBSOCKETS: a single TCP connection UPGRADED (via an HTTP Upgrade handshake, 101 Switching Protocols) to a persistent, FULL-DUPLEX, bidirectional channel carrying binary or text frames with minimal per-message overhead. This is the choice when you need low-latency two-way communication: chat, multiplayer games, collaborative editing, live cursors. Costs: it's a different protocol (ws/wss), so it needs infra that understands the upgrade (some proxies/load balancers need config), it doesn't get HTTP caching/semantics, you handle reconnection/heartbeats/backpressure yourself, and auth is trickier (do it during the handshake). How to choose: if updates are one-way server→client, prefer SSE — it's simpler, cheaper, reconnects for free, and rides normal HTTP; reach for WebSockets only when you genuinely need bidirectional or high-frequency client→server messaging; use long-polling as the fallback when neither is available or when message frequency is low and simplicity/compat trumps everything. A frequent mistake is defaulting to WebSockets for a notification feed that SSE would serve with far less operational cost.

**Use this technique when.** Choosing a push mechanism; justifying SSE over WebSockets; building notifications, chat, or live data.

```text
                  Long-polling      SSE (EventSource)     WebSocket
Direction         server->client    server->client only   full-duplex (both)
Protocol          plain HTTP        plain HTTP (stream)    ws/wss (Upgrade 101)
Data              any               text (UTF-8) only      text or binary
Reconnect         manual re-request auto + Last-Event-ID   you implement it
Infra fit         universal         standard HTTP/CDN/H2   needs upgrade-aware proxies
Per-msg cost      full req/response cheap after connect    lowest

Choose:
  one-way feed/notifications/progress -> SSE (simple, cheap, auto-reconnect)
  two-way / high-frequency (chat, games, collab) -> WebSocket
  neither available / low rate + max compat -> long-polling
Don't default to WebSockets for a one-way notification feed.
```

**References.** [MDN · Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) · [MDN · WebSockets API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)

---

### 18. Clickjacking and defensive security headers  `Hard`

**Pattern:** Security Headers

**Problem.** What is clickjacking, and which HTTP security headers should a production app set? Explain what each one does.

**What it tests.** Knowledge of framing-based attacks plus the practical security-header hardening checklist for a real app.

**Approach & answer.** CLICKJACKING (UI redress) is an attack where a malicious page loads YOUR site in a transparent or disguised iframe and overlays it with its own deceptive UI, so the victim thinks they're clicking the attacker's page but their clicks actually land on your framed page — e.g. an invisible 'Delete account' or 'Approve payment' button positioned under a fake 'Win a prize' button. The defense is to control WHO CAN FRAME your pages. Two headers do this: X-Frame-Options (the older header: DENY = never framed, SAMEORIGIN = only your own origin may frame it) and the modern replacement, the CSP directive frame-ancestors (frame-ancestors 'none' or frame-ancestors 'self' https://trusted-partner.com), which is more flexible and takes precedence in modern browsers. Set one (frame-ancestors preferred) on any page that performs actions. That's the specific fix; a production app should also set the broader hardening headers: (1) Strict-Transport-Security (HSTS) — forces HTTPS for the domain for a max-age, blocking SSL-stripping/downgrade and protecting first-load with includeSubDomains and preload. (2) Content-Security-Policy — the XSS/injection net (nonce-based, as discussed), also carrying frame-ancestors. (3) X-Content-Type-Options: nosniff — stops the browser from MIME-sniffing a response into an executable type (prevents a text/plain or image being run as script). (4) Referrer-Policy (e.g. strict-origin-when-cross-origin or no-referrer) — limits how much of your URL leaks in the Referer header to other sites, protecting tokens/PII in URLs. (5) Permissions-Policy (formerly Feature-Policy) — disables powerful APIs you don't use (camera, geolocation, microphone) so injected/embedded content can't invoke them. (6) The Cross-Origin isolation trio — Cross-Origin-Opener-Policy (COOP), Cross-Origin-Embedder-Policy (COEP), and Cross-Origin-Resource-Policy (CORP) — which isolate your browsing context (mitigating Spectre-style cross-origin leaks and enabling powerful features like SharedArrayBuffer). Also set Set-Cookie flags (HttpOnly/Secure/SameSite) which we covered. Operationally: don't hand-maintain these per-response — set them at the edge/framework layer, test with a header scanner, and roll CSP out in Report-Only first. The mental model: each header closes one class of attack (framing, downgrade, injection, MIME confusion, referrer leakage, API abuse, cross-origin leaks), and 'defense in depth' means shipping the whole set, not picking one.

**Use this technique when.** Hardening a production app; preventing clickjacking; assembling a security-headers checklist.

```text
Clickjacking: attacker iframes your page invisibly, overlays fake UI,
              victim's clicks hit YOUR framed buttons. Fix = control framing.

Production security headers:
  Content-Security-Policy: ...; frame-ancestors 'none'   XSS net + anti-framing
  X-Frame-Options: DENY                                  legacy anti-framing
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload   force HTTPS
  X-Content-Type-Options: nosniff                        no MIME sniffing
  Referrer-Policy: strict-origin-when-cross-origin       limit URL leakage
  Permissions-Policy: camera=(), geolocation=(), microphone=()   disable unused APIs
  Cross-Origin-Opener-Policy: same-origin                isolate context (COOP)
  Cross-Origin-Resource-Policy: same-origin              (CORP; COEP pairs with it)

Set at edge/framework layer, scan to verify, roll CSP out Report-Only first.
```

**References.** [MDN · X-Frame-Options / frame-ancestors](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) · [OWASP · Secure Headers Project](https://owasp.org/www-project-secure-headers/)

---

### 19. Subresource Integrity and third-party script risk  `Hard`

**Pattern:** Supply-chain Security

**Problem.** Your app loads a third-party script from a CDN. What's the supply-chain risk, how does Subresource Integrity help, and what are its limits?

**What it tests.** Awareness of third-party/supply-chain script risk and the specific role (and boundaries) of SRI.

**Approach & answer.** Every third-party script you load — an analytics snippet, a tag manager, a charting library from a CDN — runs with your ORIGIN'S FULL PRIVILEGES: it can read the DOM, cookies (non-HttpOnly), and localStorage, make authenticated requests as the user, and rewrite the page. So a third-party script is a trust decision equivalent to giving that vendor code-execution on your users. The supply-chain risk: you don't control that file. If the CDN is compromised, the vendor is breached, or an attacker hijacks the account/domain, the served script can be swapped for a malicious one and every visitor to your site is attacked (the Magecart card-skimming attacks worked exactly this way — poisoning a widely-included third-party script). SUBRESOURCE INTEGRITY (SRI) defends against the file being TAMPERED WITH: you add an integrity attribute containing the cryptographic hash of the exact file you vetted (integrity="sha384-...") plus crossorigin="anonymous". The browser fetches the resource, hashes it, and REFUSES TO EXECUTE it if the hash doesn't match — so a modified script simply won't run. This pins the content: you're no longer trusting 'whatever the CDN serves today', you're trusting 'the specific bytes I hashed'. SRI's LIMITS are important and often missed: (1) It only verifies the ONE file you pinned. If that script dynamically loads FURTHER scripts at runtime, those aren't covered — SRI doesn't transitively protect the dependency graph. (2) It protects against tampering, NOT against a malicious version being what you pinned in the first place, and it breaks 'auto-updating' scripts — many analytics/tag vendors ship a tiny loader that always pulls the latest code, which is fundamentally incompatible with a fixed hash (you'd have to re-hash on every vendor update). (3) It doesn't reduce the PRIVILEGE the script has once it does run legitimately. So SRI is necessary but not sufficient. The fuller defense-in-depth posture: pin versioned files with SRI where you can; minimize the number of third-party scripts and prefer self-hosting vetted copies; constrain them with a strict CSP (allowlist exact sources; 'strict-dynamic' + nonces so only trusted loaders run); ISOLATE risky third-party widgets in a sandboxed iframe so they don't share your origin's privileges; use Permissions-Policy to strip capabilities; and monitor with CSP violation reports. The mental model: SRI freezes the bytes, CSP limits the sources, iframes limit the privilege — you need all three because a third-party script is untrusted code running in your users' sessions.

**Use this technique when.** Adding a CDN/third-party script; explaining supply-chain risk; deciding SRI vs CSP vs sandboxing.

```html
<!-- Third-party script runs with YOUR origin's full privileges. -->
<!-- Risk: CDN/vendor compromise swaps the file -> every visitor attacked (Magecart). -->

<!-- Subresource Integrity pins the exact bytes you vetted: -->
<script src="https://cdn.example.com/lib@1.2.3/lib.min.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>
<!-- Browser hashes the fetched file; hash mismatch -> script is BLOCKED. -->

<!-- SRI limits:
     - covers only THIS file, not scripts it loads at runtime
     - breaks auto-updating loaders (hash must change per version)
     - doesn't reduce the privilege the script has once it runs -->

<!-- Defense in depth: SRI (freeze bytes) + strict CSP (limit sources)
     + sandboxed iframe for risky widgets (limit privilege) + Permissions-Policy. -->
```

**References.** [MDN · Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) · [OWASP · Third-Party JavaScript Management](https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html)

---

### 20. Rate limiting, retries, and idempotency keys  `Hard`

**Pattern:** Resilience

**Problem.** Design the client/server contract for a flaky network: rate limiting (429), retries with backoff, and safe retries of non-idempotent requests.

**What it tests.** Reasoning about resilient request handling end-to-end — backoff, jitter, Retry-After, and idempotency keys — not just 'add a retry'.

**Approach & answer.** On a real network, requests fail transiently and servers protect themselves, so a robust client and server share a contract. RATE LIMITING: a server caps how many requests a client may make in a window and returns 429 Too Many Requests when exceeded, ideally with a Retry-After header (seconds, or a date) telling the client exactly how long to wait. A well-behaved client HONORS Retry-After rather than hammering. (Servers commonly implement limits with token-bucket or sliding-window algorithms; the client's job is to react correctly to the 429.) RETRIES must be disciplined, or they amplify outages. Rules: (1) Only retry on TRANSIENT failures — network errors, timeouts, 429, and 5xx like 502/503/504; do NOT retry 4xx like 400/401/403/404 (retrying a bad request just repeats a guaranteed failure). (2) Use EXPONENTIAL BACKOFF — wait ~base * 2^attempt (e.g. 0.5s, 1s, 2s, 4s) with a cap — so you back off fast under load instead of retrying instantly. (3) Add JITTER (randomize the delay) — this is critical at scale: without it, many clients that failed at the same instant retry in lockstep and create a synchronized 'thundering herd' that re-crushes the recovering server; randomized delays spread the load. (4) Cap the number of attempts and set an overall deadline so you fail fast rather than retrying forever. The hard part is retrying NON-IDEMPOTENT requests safely. GET/PUT/DELETE are idempotent — a duplicate is harmless — so they're naturally retry-safe. But a POST like 'charge the card' or 'place the order' is dangerous to retry: if the original actually succeeded but the response was lost to a timeout, a blind retry double-charges. The solution is an IDEMPOTENCY KEY: the client generates a unique key (a UUID) for the logical operation and sends it (e.g. Idempotency-Key header) on the request AND on every retry of that same operation. The server records the key with the result of the first successful execution; if it sees the key again, it returns the STORED result instead of executing again. Now a retry is safe — the operation happens at most once regardless of how many times the client resends. (This is exactly how payment APIs like Stripe make POSTs retry-safe.) Putting it together: client uses backoff+jitter, honors Retry-After, retries only transient failures, bounds attempts, and attaches an idempotency key to any non-idempotent write; server enforces limits with clear 429+Retry-After and deduplicates by idempotency key. Bonus resilience: a CIRCUIT BREAKER on the client stops sending after a run of failures (fail fast, give the server room to recover) and probes periodically before closing again.

**Use this technique when.** Building a resilient API client; making a POST retry-safe; handling 429s and outages gracefully.

```js
// Disciplined retry: transient-only, exponential backoff + JITTER, honor Retry-After.
async function request(url, opts = {}, { attempts = 4, base = 500 } = {}) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(url, opts);
    if (res.ok) return res;

    const transient = res.status === 429 || (res.status >= 500 && res.status <= 599);
    if (!transient || i === attempts - 1) return res;   // don't retry 4xx; stop at cap

    const retryAfter = Number(res.headers.get('Retry-After'));
    const backoff = base * 2 ** i;                        // 0.5s,1s,2s,4s...
    const jitter = Math.random() * backoff;              // spread the herd
    const waitMs = retryAfter ? retryAfter * 1000 : backoff / 2 + jitter;
    await new Promise(r => setTimeout(r, waitMs));
  }
}

// Non-idempotent POST made retry-safe with an idempotency key:
const key = crypto.randomUUID();                          // one key per logical op
await request('/charge', {
  method: 'POST',
  headers: { 'Idempotency-Key': key, 'Content-Type': 'application/json' },
  body: JSON.stringify({ amount: 5000 }),
});
// Server stores result under key; a repeated key returns the SAME result,
// so a timed-out-but-succeeded charge is never double-applied.
```

**References.** [MDN · 429 Too Many Requests / Retry-After](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) · [AWS · Exponential backoff and jitter](https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/)

---

## DSA

> Frontend interviews skew to easy/medium: arrays, strings, hash maps, trees, one design problem. The scoring isn't the answer — it's the method. Every single time: (1) clarify inputs, sizes, edge cases; (2) state the brute force + its Big-O out loud; (3) name the pattern and optimize; (4) code cleanly; (5) dry-run one example + edge cases. Use the decision table above to name the pattern before you write anything.

### 1. Two Sum  `Easy`

**Pattern:** Hash Map / Set

**Problem.** Given an array of integers and a target, return the indices of the two numbers that add up to target. Exactly one solution; can't reuse an element.

**What it tests.** Do you spot that a nested loop (O(n²)) can become one pass by remembering what you've seen?

**Approach & answer.** Scan once. For each number x, the partner you need is target − x. Keep a map of value → index; if the partner was already seen, you're done. This is the archetypal 'trade space for time' move: the map turns the inner search from O(n) to O(1), collapsing the whole thing from O(n²) to O(n). Store value→index (not just presence) so you can return indices. Check for the partner BEFORE inserting the current number — otherwise a target like 6 with a lone 3 would falsely match itself. The same 'have I already seen the complement?' reflex powers duplicate detection, pair-with-difference, and two-sum's many variants.

**Use this technique when.** The moment you catch yourself writing a nested loop to find a pair or a match, ask: 'could a hash map remember this for me?'

**Complexity.** Time O(n) · Space O(n)

```js
function twoSum(nums, target) {
  const seen = new Map();            // value -> index
  for (let i = 0; i < nums.length; i++) {
    const need = target - nums[i];
    if (seen.has(need)) return [seen.get(need), i];
    seen.set(nums[i], i);
  }
  return [];
}
```

**References.** [LeetCode 1 · Two Sum](https://leetcode.com/problems/two-sum/) · [MDN · Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)

---

### 2. First Unique Character  `Easy`

**Pattern:** Hash Map / Set

**Problem.** Return the index of the first non-repeating character in a string, or -1 if none.

**What it tests.** Frequency counting — and knowing that objects/Maps give O(1) tallying.

**Approach & answer.** Two passes: first tally every character's count, then scan again and return the first with count 1. Two passes is still O(n) and beats re-counting inside a loop (which would be O(n²)). The key insight is that 'first' requires original order, so you can't just look at the frequency map alone — you re-walk the string in order and consult the tally. A single-pass alternative stores {count, firstIndex} per char, but two clean passes are simpler and just as fast asymptotically.

**Use this technique when.** Any 'first/only/most frequent element' question → build a frequency map first.

**Complexity.** Time O(n) · Space O(k) where k = distinct chars

```js
function firstUniqChar(s) {
  const count = new Map();
  for (const ch of s) count.set(ch, (count.get(ch) || 0) + 1);
  for (let i = 0; i < s.length; i++) {
    if (count.get(s[i]) === 1) return i;
  }
  return -1;
}
```

**References.** [LeetCode 387 · First Unique Character in a String](https://leetcode.com/problems/first-unique-character-in-a-string/) · [MDN · Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)

---

### 3. Valid Palindrome  `Easy`

**Pattern:** Two Pointers

**Problem.** Given a string, return true if it reads the same forwards and backwards, considering only alphanumeric characters and ignoring case.

**What it tests.** Recognizing that comparing ends inward avoids extra space.

**Approach & answer.** One pointer at each end. Skip non-alphanumerics, compare lowercased characters, move inward. O(1) extra space — better than reversing the string or filtering into a new array first. The two-pointers-converging shape works because a palindrome is symmetric: position i must mirror position n−1−i. The fiddly part is the skip logic — advance each pointer past junk independently before comparing, and guard `i < j` inside the skip loops so pointers never cross. This same converge-from-both-ends idea underlies reversing in place and the sorted-array two-sum.

**Use this technique when.** Comparing a sequence against itself from both ends, or scanning a sorted array for a condition.

**Complexity.** Time O(n) · Space O(1)

```js
function isPalindrome(s) {
  const ok = c => /[a-z0-9]/i.test(c);
  let i = 0, j = s.length - 1;
  while (i < j) {
    if (!ok(s[i])) { i++; continue; }
    if (!ok(s[j])) { j--; continue; }
    if (s[i].toLowerCase() !== s[j].toLowerCase()) return false;
    i++; j--;
  }
  return true;
}
```

**References.** [LeetCode 125 · Valid Palindrome](https://leetcode.com/problems/valid-palindrome/) · [MDN · String.prototype.toLowerCase](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase)

---

### 4. Max Average Subarray (size k)  `Easy`

**Pattern:** Sliding Window

**Problem.** Find the contiguous subarray of length k with the maximum average, return that average.

**What it tests.** Fixed-size window: recognizing you don't recompute the sum each time.

**Approach & answer.** Compute the sum of the first k. Then slide: add the incoming element, subtract the outgoing one. Each step is O(1), so the whole thing is O(n) instead of O(n·k). The realization is that consecutive windows overlap in k−1 elements — recomputing the whole sum re-reads shared work you already did. Track the max sum and divide by k once at the end (dividing each step is wasteful and risks floating-point drift). Fixed-size windows are the gateway to the harder variable-size window template.

**Use this technique when.** 'Best window of fixed length k' → maintain a running sum, add-one/drop-one as you slide.

**Complexity.** Time O(n) · Space O(1)

```js
function findMaxAverage(nums, k) {
  let sum = 0;
  for (let i = 0; i < k; i++) sum += nums[i];
  let best = sum;
  for (let i = k; i < nums.length; i++) {
    sum += nums[i] - nums[i - k];   // slide the window
    best = Math.max(best, sum);
  }
  return best / k;
}
```

**References.** [LeetCode 643 · Maximum Average Subarray I](https://leetcode.com/problems/maximum-average-subarray-i/)

---

### 5. Running Sum of 1d Array  `Easy`

**Pattern:** Prefix Sum

**Problem.** Return an array where result[i] = sum of nums[0..i].

**What it tests.** The base building block: cumulative sums.

**Approach & answer.** Carry a running total, writing it at each index. This precomputation is what makes any range-sum query O(1) later: sum(i..j) becomes prefix[j] − prefix[i−1]. Doing it in place mutates the input to O(1) extra space; keep a separate output array if the caller still needs the originals. Trivial on its own, but it's the foundation the harder prefix-sum problems build on — the difference of two cumulative sums is the whole trick.

**Use this technique when.** Foundation for range queries and 'sum so far' problems.

**Complexity.** Time O(n) · Space O(1) (in place)

```js
function runningSum(nums) {
  for (let i = 1; i < nums.length; i++) nums[i] += nums[i - 1];
  return nums;
}
```

**References.** [LeetCode 1480 · Running Sum of 1d Array](https://leetcode.com/problems/running-sum-of-1d-array/)

---

### 6. Binary Search / Search Insert Position  `Easy`

**Pattern:** Binary Search

**Problem.** Given a sorted array and a target, return its index, or the index where it would be inserted to keep the array sorted.

**What it tests.** Getting the loop invariant and boundaries right — the #1 source of bugs.

**Approach & answer.** Classic halving. Use lo ≤ hi and mid = lo + (hi−lo)/2 (avoids overflow in other languages; `>> 1` floors it). When the loop ends, `lo` is exactly the insert position — the point where the target would go to keep order. Master this template; every binary search is a variation of it, and the bugs almost always live in three places: the loop condition (`<=` vs `<`), how you move the bounds (`mid+1` / `mid−1` vs `mid`), and what you return when not found. Fix a consistent invariant ('answer is in [lo, hi]') and the boundaries follow.

**Use this technique when.** Sorted data + find/insert/boundary in O(log n). Also: any monotonic 'is X feasible?' predicate.

**Complexity.** Time O(log n) · Space O(1)

```js
function searchInsert(nums, target) {
  let lo = 0, hi = nums.length - 1;
  while (lo <= hi) {
    const mid = lo + ((hi - lo) >> 1);
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) lo = mid + 1;
    else hi = mid - 1;
  }
  return lo;   // insertion point
}
```

**References.** [LeetCode 35 · Search Insert Position](https://leetcode.com/problems/search-insert-position/) · [LeetCode 704 · Binary Search](https://leetcode.com/problems/binary-search/)

---

### 7. Maximum Depth of Binary Tree  `Easy`

**Pattern:** DFS (Depth-First)

**Problem.** Return the maximum depth (number of nodes along the longest root-to-leaf path) of a binary tree.

**What it tests.** Recursive tree thinking: solve a node in terms of its children.

**Approach & answer.** Depth of a node = 1 + max(depth(left), depth(right)); an empty subtree is 0. This 'answer for me = combine answers of my children' recursion is the heart of nearly every tree problem — you trust the recursive call to return the right subresult and just describe how to merge. The base case (null → 0) is what stops the recursion and seeds the arithmetic. Space is O(h) for the call stack: O(log n) for a balanced tree, but O(n) for a degenerate (linked-list-shaped) one — worth stating when asked about worst case.

**Use this technique when.** Any tree aggregate (height, sum, diameter, 'does a path exist') → recurse into children and combine.

**Complexity.** Time O(n) · Space O(h) recursion (h = height)

```js
function maxDepth(root) {
  if (!root) return 0;
  return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
```

**References.** [LeetCode 104 · Maximum Depth of Binary Tree](https://leetcode.com/problems/maximum-depth-of-binary-tree/)

---

### 8. Valid Parentheses  `Easy`

**Pattern:** Stack / Monotonic Stack

**Problem.** Given a string of '()[]{}', return true if brackets are correctly opened and closed in order.

**What it tests.** Recognizing LIFO nesting — the defining use case of a stack.

**Approach & answer.** Push opening brackets. On a closing bracket, the top of the stack must be its matching opener; if not (or stack empty), it's invalid. Valid iff the stack ends empty. Nesting = last-opened-first-closed = stack. Map closers→openers for O(1) matching. The subtle case is a leading closer like ')': the stack is empty, so `stack.pop()` returns `undefined`, which never equals a valid opener — the early `return false` catches it without a separate emptiness check. This 'match against the most recent unmatched thing' shape recurs in expression parsing, HTML/XML validation, and editor bracket-highlighting.

**Use this technique when.** Matching pairs, nesting, or 'undo to the most recent' → stack.

**Complexity.** Time O(n) · Space O(n)

```js
function isValid(s) {
  const pairs = { ')': '(', ']': '[', '}': '{' };
  const stack = [];
  for (const ch of s) {
    if (ch === '(' || ch === '[' || ch === '{') stack.push(ch);
    else if (stack.pop() !== pairs[ch]) return false;
  }
  return stack.length === 0;
}
```

**References.** [LeetCode 20 · Valid Parentheses](https://leetcode.com/problems/valid-parentheses/) · [MDN · Array (as stack: push/pop)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)

---

### 9. Reverse a Linked List  `Easy`

**Pattern:** Linked List / Fast-Slow

**Problem.** Reverse a singly linked list and return the new head.

**What it tests.** Pointer manipulation without losing the rest of the list.

**Approach & answer.** Walk the list carrying prev, cur, next. Save next, point cur.next back to prev, advance both. The key is caching `next` before you overwrite the pointer. Iterative is O(1) space; recursion is elegant but O(n) stack. Draw three boxes and move the arrows one at a time — the classic mistake is reassigning `cur.next = prev` before stashing `cur.next`, which severs the rest of the list. `prev` starts at null so the original head correctly becomes the new tail pointing at null. Reversal is a building block for palindrome-linked-list and reverse-nodes-in-k-group.

**Use this technique when.** In-place linked list rewiring → three-pointer walk (prev/cur/next).

**Complexity.** Time O(n) · Space O(1)

```js
function reverseList(head) {
  let prev = null, cur = head;
  while (cur) {
    const next = cur.next;   // save before overwriting
    cur.next = prev;         // reverse the link
    prev = cur;
    cur = next;
  }
  return prev;               // new head
}
```

**References.** [LeetCode 206 · Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/)

---

### 10. Climbing Stairs  `Easy`

**Pattern:** Dynamic Programming

**Problem.** You can climb 1 or 2 steps at a time. How many distinct ways to reach the n-th step?

**What it tests.** Spotting overlapping subproblems (it's Fibonacci) and rolling variables.

**Approach & answer.** Ways(n) = Ways(n−1) + Ways(n−2): your last move was a 1-step or a 2-step. That's Fibonacci. Naive recursion recomputes the same values exponentially — cache them, or better, roll two variables for O(1) space. Recognizing the recurrence is the whole game. The three-part DP checklist lives here in miniature: state (ways to reach step i), transition (sum of the two reachable predecessors), and base cases (one way to stand at step 0, one way to reach step 1). Once you see 'the answer for n is built from a fixed number of smaller answers', top-down memoization and bottom-up tabulation are two spellings of the same idea.

**Use this technique when.** 'Count the ways to reach/build X' where each state depends on a few earlier states → DP.

**Complexity.** Time O(n) · Space O(1)

```js
function climbStairs(n) {
  let a = 1, b = 1;                 // ways to reach step 0 and 1
  for (let i = 2; i <= n; i++) {
    [a, b] = [b, a + b];
  }
  return b;
}
```

**References.** [LeetCode 70 · Climbing Stairs](https://leetcode.com/problems/climbing-stairs/)

---

### 11. Single Number  `Easy`

**Pattern:** Bit Manipulation

**Problem.** Every element appears twice except one. Find the element that appears once, in O(n) time and O(1) space.

**What it tests.** Recognizing XOR as the tool for 'pairs cancel out' problems instead of reaching for a hash set.

**Approach & answer.** XOR has three properties that make this a one-liner: x ^ x === 0 (a value cancels itself), x ^ 0 === x (identity), and it is commutative and associative (order doesn't matter). So XOR-ing every element together cancels each duplicated pair and leaves only the unique value. Signal: 'everything appears an even number of times except one' → fold with XOR. This beats a hash set (which costs O(n) extra space) and sorting (O(n log n)). The same trick finds a missing number (XOR the values against the full index range) and swaps two variables without a temp.

**Use this technique when.** 'Appears twice / even count except one', parity checks, toggling flags, or swapping without a temp.

**Complexity.** Time O(n), Space O(1)

```js
function singleNumber(nums) {
  let acc = 0;
  for (const n of nums) acc ^= n;  // duplicated pairs cancel to 0
  return acc;                      // the lone unpaired value survives
}
```

**References.** [LeetCode · Single Number](https://leetcode.com/problems/single-number/) · [MDN · Bitwise XOR (^)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_XOR)

---

### 12. Best Time to Buy and Sell Stock  `Easy`

**Pattern:** Greedy / One-Pass

**Problem.** Given daily prices, find the max profit from a single buy followed by a later sell.

**What it tests.** Spotting a single-pass greedy (track a running minimum) instead of comparing every pair O(n²).

**Approach & answer.** Sweep once, tracking the minimum price seen so far; at each day the best profit if you sold today is price - minSoFar, so keep the max of those. Greedy is correct because the optimal sell day depends only on the cheapest day at or before it — you never need to reconsider earlier decisions. Signal: 'best value relative to a running extreme' or 'one transaction' → maintain a running min/max in a single sweep rather than nested loops. Watch the constraint that you must buy before you sell, so profit never goes below 0.

**Use this technique when.** 'Max profit / one transaction', running min or max, any problem reducible to comparing each element to a running extreme.

**Complexity.** Time O(n), Space O(1)

```js
function maxProfit(prices) {
  let minSoFar = Infinity, best = 0;
  for (const p of prices) {
    minSoFar = Math.min(minSoFar, p);    // cheapest buy seen so far
    best = Math.max(best, p - minSoFar); // profit if we sold today
  }
  return best;
}
```

**References.** [LeetCode · Best Time to Buy and Sell Stock](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/) · [Wikipedia · Greedy algorithm](https://en.wikipedia.org/wiki/Greedy_algorithm)

---

### 13. Group Anagrams  `Medium`

**Pattern:** Hash Map / Set

**Problem.** Group a list of strings so that anagrams are together. e.g. ['eat','tea','tan','ate','nat','bat'] → [['eat','tea','ate'],['tan','nat'],['bat']].

**What it tests.** Designing a good hash KEY. The insight: anagrams share a canonical form.

**Approach & answer.** The signature of a word is its sorted letters ('eat'→'aet'). Use that as a map key and push words into buckets. Sorting each word is O(k log k); a letter-count key (e.g. a 26-length tally serialized to 'a1e1t1') makes it O(k) if asked to optimize. The general move — 'group things equivalent under some transform' → derive a canonical key and bucket by it — is exactly how you'd dedupe records or cluster equivalent states. The Map's insertion order also gives you deterministic output grouping.

**Use this technique when.** 'Group things that are equivalent under some transform' → derive a canonical key, bucket by it.

**Complexity.** Time O(n·k log k) · Space O(n·k)

```js
function groupAnagrams(words) {
  const groups = new Map();
  for (const w of words) {
    const key = [...w].sort().join('');   // canonical signature
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(w);
  }
  return [...groups.values()];
}
```

**References.** [LeetCode 49 · Group Anagrams](https://leetcode.com/problems/group-anagrams/) · [MDN · Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)

---

### 14. Container With Most Water  `Medium`

**Pattern:** Two Pointers

**Problem.** Given heights[], each a vertical line, find two lines that together with the x-axis hold the most water. Return the max area.

**What it tests.** Greedy two-pointer reasoning: why moving the shorter side is always safe.

**Approach & answer.** Start pointers at both ends. Area = min(h[i], h[j]) × width. Always move the SHORTER line inward — moving the taller can only lower or keep the height while shrinking width, so it can never beat the current best. Moving the shorter one is the only move with upside: it discards a line that already capped the area, giving a shorter-but-possibly-taller pairing a chance. This greedy 'the limiting side is the one to abandon' argument is the crux; brute force is O(n²) and this proves you can skip almost all pairs safely.

**Use this technique when.** Maximize/minimize something between two ends of an array where width shrinks as you move in.

**Complexity.** Time O(n) · Space O(1)

```js
function maxArea(height) {
  let i = 0, j = height.length - 1, best = 0;
  while (i < j) {
    const area = Math.min(height[i], height[j]) * (j - i);
    best = Math.max(best, area);
    if (height[i] < height[j]) i++;   // move the shorter side
    else j--;
  }
  return best;
}
```

**References.** [LeetCode 11 · Container With Most Water](https://leetcode.com/problems/container-with-most-water/) · [MDN · Math.min / Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min)

---

### 15. 3Sum  `Medium`

**Pattern:** Two Pointers

**Problem.** Return all unique triplets [a,b,c] in an array that sum to zero.

**What it tests.** Composing sort + two pointers, and the fiddly part: skipping duplicates.

**Approach & answer.** Sort first. Fix each index i, then two-pointer the rest for pairs summing to −nums[i]. Skip duplicate values at i, and after finding a triplet skip duplicate lo/hi values — that's what keeps triplets unique. Sorting (O(n log n)) unlocks the two-pointer scan and makes dedup a simple 'skip equal neighbors' check instead of a hash set of tuples. Total O(n²), which is optimal for 3Sum. The generalization: k-sum reduces to (k−1)-sum by fixing one element and recursing, bottoming out at the two-pointer base case.

**Use this technique when.** Any k-sum / triplet problem: sort, fix one, two-pointer the remainder.

**Complexity.** Time O(n²) · Space O(1) extra (excluding output)

```js
function threeSum(nums) {
  nums.sort((a, b) => a - b);
  const res = [];
  for (let i = 0; i < nums.length - 2; i++) {
    if (i > 0 && nums[i] === nums[i - 1]) continue;   // skip dup anchor
    let lo = i + 1, hi = nums.length - 1;
    while (lo < hi) {
      const sum = nums[i] + nums[lo] + nums[hi];
      if (sum === 0) {
        res.push([nums[i], nums[lo], nums[hi]]);
        while (lo < hi && nums[lo] === nums[lo + 1]) lo++;
        while (lo < hi && nums[hi] === nums[hi - 1]) hi--;
        lo++; hi--;
      } else if (sum < 0) lo++;
      else hi--;
    }
  }
  return res;
}
```

**References.** [LeetCode 15 · 3Sum](https://leetcode.com/problems/3sum/) · [LeetCode 18 · 4Sum](https://leetcode.com/problems/4sum/)

---

### 16. Longest Substring Without Repeating Characters  `Medium`

**Pattern:** Sliding Window

**Problem.** Given a string, return the length of the longest substring with no repeating characters.

**What it tests.** Variable-size window: expand right, and shrink left just enough when the invariant breaks.

**Approach & answer.** Grow a window with `right`. Keep last-seen index of each char. When you hit a repeat inside the window, jump `left` to just past the previous occurrence. The window always holds a valid (unique) substring; track its max length. The subtlety is the `>= left` guard: a duplicate whose last position is behind `left` is already outside the window and must be ignored — otherwise `left` jumps backward and the window corrupts. Jumping left directly (instead of stepping) keeps it O(n) with each pointer only moving forward.

**Use this technique when.** 'Longest/shortest contiguous run satisfying a constraint' → variable window; move left only enough to restore the constraint.

**Complexity.** Time O(n) · Space O(min(n, alphabet))

```js
function lengthOfLongestSubstring(s) {
  const lastSeen = new Map();
  let left = 0, best = 0;
  for (let right = 0; right < s.length; right++) {
    const c = s[right];
    if (lastSeen.has(c) && lastSeen.get(c) >= left) {
      left = lastSeen.get(c) + 1;   // shrink past the duplicate
    }
    lastSeen.set(c, right);
    best = Math.max(best, right - left + 1);
  }
  return best;
}
```

**References.** [LeetCode 3 · Longest Substring Without Repeating Characters](https://leetcode.com/problems/longest-substring-without-repeating-characters/)

---

### 17. Subarray Sum Equals K  `Medium`

**Pattern:** Prefix Sum

**Problem.** Count the number of contiguous subarrays whose sum equals k.

**What it tests.** The prefix-sum + hash-map combo. Sliding window fails here because values can be negative.

**Approach & answer.** Running sum `sum`. A subarray ending at i sums to k exactly when a previous prefix equalled `sum − k`. Store how many times each prefix sum has occurred; add that count. Seed the map with {0: 1} so subarrays starting at index 0 are counted. Note: sliding window doesn't work with negatives — growing the window can decrease the sum, so there's no monotonic 'shrink when too big' invariant. That's the trap this problem sets, and the reason the prefix-sum + hash-map combo is the right reach.

**Use this technique when.** 'How many subarrays sum to K' (especially with negatives) → prefix sums counted in a hash map.

**Complexity.** Time O(n) · Space O(n)

```js
function subarraySum(nums, k) {
  const seen = new Map([[0, 1]]);   // prefix 0 seen once (empty prefix)
  let sum = 0, count = 0;
  for (const x of nums) {
    sum += x;
    count += seen.get(sum - k) || 0;
    seen.set(sum, (seen.get(sum) || 0) + 1);
  }
  return count;
}
```

**References.** [LeetCode 560 · Subarray Sum Equals K](https://leetcode.com/problems/subarray-sum-equals-k/)

---

### 18. Subsets (Power Set)  `Medium`

**Pattern:** Backtracking

**Problem.** Return all possible subsets of a set of distinct integers.

**What it tests.** The choose / recurse / un-choose skeleton.

**Approach & answer.** At each index you make a binary choice: include this number or not. Recurse, then remove it (backtrack) to explore the other branch. The path at every node is one subset — you record it on entry, not just at leaves, because every prefix is itself a valid subset. This choose→recurse→undo shape is every backtracking problem; the `start` index prevents revisiting earlier elements, so you generate combinations (not permutations). There are 2ⁿ subsets and copying each costs O(n), giving O(n·2ⁿ).

**Use this technique when.** 'Generate all combinations/subsets' → build a partial solution, recurse, undo the last choice.

**Complexity.** Time O(n·2ⁿ) · Space O(n) recursion depth

```js
function subsets(nums) {
  const res = [], path = [];
  function backtrack(start) {
    res.push([...path]);              // every node is a valid subset
    for (let i = start; i < nums.length; i++) {
      path.push(nums[i]);             // choose
      backtrack(i + 1);               // explore
      path.pop();                     // un-choose
    }
  }
  backtrack(0);
  return res;
}
```

**References.** [LeetCode 78 · Subsets](https://leetcode.com/problems/subsets/)

---

### 19. Generate Parentheses  `Medium`

**Pattern:** Backtracking

**Problem.** Given n pairs of parentheses, generate all combinations of well-formed parentheses.

**What it tests.** Pruning invalid branches early instead of generating-then-filtering.

**Approach & answer.** Track how many '(' and ')' used. You may add '(' while open < n, and ')' only while close < open (otherwise it's malformed). Pruning the impossible branches is what makes this efficient — you never build a string you'd throw away, so the recursion tree only contains valid prefixes. The two invariants (open ≤ n, close ≤ open) are exactly the well-formedness rules stated incrementally. The count of results is the nth Catalan number, hence the O(4ⁿ/√n) bound.

**Use this technique when.** Generate all valid arrangements under constraints → backtrack, and prune the moment the partial becomes invalid.

**Complexity.** Time O(4ⁿ/√n) (Catalan) · Space O(n)

```js
function generateParenthesis(n) {
  const res = [];
  function backtrack(cur, open, close) {
    if (cur.length === 2 * n) { res.push(cur); return; }
    if (open < n) backtrack(cur + '(', open + 1, close);
    if (close < open) backtrack(cur + ')', open, close + 1);
  }
  backtrack('', 0, 0);
  return res;
}
```

**References.** [LeetCode 22 · Generate Parentheses](https://leetcode.com/problems/generate-parentheses/) · [Wikipedia · Catalan number](https://en.wikipedia.org/wiki/Catalan_number)

---

### 20. Combination Sum  `Medium`

**Pattern:** Backtracking

**Problem.** Given distinct candidates and a target, return all unique combinations that sum to target. Each number may be reused unlimited times.

**What it tests.** Backtracking with reuse (passing `i`, not `i+1`) and pruning on remaining target.

**Approach & answer.** Recurse choosing candidates from `start` onward; because reuse is allowed, recurse with the same index `i`. Subtract from the remaining target and stop when it hits 0 (record) or goes negative (prune). Passing `start` prevents permuted duplicates like [2,3] and [3,2] — you only ever move forward or stay, never back. If reuse were NOT allowed you'd recurse with `i+1` instead; that single change is the difference between Combination Sum and Combination Sum II. Sorting candidates first lets you `break` early once a candidate exceeds the remainder.

**Use this technique when.** 'All combinations summing to target', with or without reuse → backtrack; control reuse via the start index.

**Complexity.** Time exponential in target/min · Space O(target/min)

```js
function combinationSum(candidates, target) {
  const res = [], path = [];
  function backtrack(start, remain) {
    if (remain === 0) { res.push([...path]); return; }
    if (remain < 0) return;                 // prune
    for (let i = start; i < candidates.length; i++) {
      path.push(candidates[i]);
      backtrack(i, remain - candidates[i]); // i (not i+1) => reuse allowed
      path.pop();
    }
  }
  backtrack(0, target);
  return res;
}
```

**References.** [LeetCode 39 · Combination Sum](https://leetcode.com/problems/combination-sum/) · [LeetCode 40 · Combination Sum II](https://leetcode.com/problems/combination-sum-ii/)

---

### 21. Search in Rotated Sorted Array  `Medium`

**Pattern:** Binary Search

**Problem.** A sorted array was rotated at an unknown pivot. Find a target's index in O(log n), or -1.

**What it tests.** Adapting binary search when the array isn't globally sorted but each half still is.

**Approach & answer.** At each mid, one half is always sorted. Detect which (compare nums[lo] to nums[mid]). If the target lies within that sorted half's range, search it; otherwise search the other half. Same O(log n), just smarter branch selection. The rotation breaks global order but preserves it locally — that's the exploit. Use `nums[lo] <= nums[mid]` (with `<=`) to handle the two-element case where lo and mid coincide. If duplicates were allowed you'd lose the O(log n) guarantee, because nums[lo] == nums[mid] no longer tells you which half is sorted (that's the LeetCode 81 variant).

**Use this technique when.** Binary search on data with a twist (rotation, mountain, matrix) — figure out which half is 'well-behaved' each step.

**Complexity.** Time O(log n) · Space O(1)

```js
function search(nums, target) {
  let lo = 0, hi = nums.length - 1;
  while (lo <= hi) {
    const mid = lo + ((hi - lo) >> 1);
    if (nums[mid] === target) return mid;
    if (nums[lo] <= nums[mid]) {                 // left half sorted
      if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
      else lo = mid + 1;
    } else {                                      // right half sorted
      if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
      else hi = mid - 1;
    }
  }
  return -1;
}
```

**References.** [LeetCode 33 · Search in Rotated Sorted Array](https://leetcode.com/problems/search-in-rotated-sorted-array/) · [LeetCode 81 · Search in Rotated Sorted Array II](https://leetcode.com/problems/search-in-rotated-sorted-array-ii/)

---

### 22. Number of Islands  `Medium`

**Pattern:** DFS (Depth-First)

**Problem.** Given a grid of '1' (land) and '0' (water), count the islands (land connected 4-directionally).

**What it tests.** Grid-as-graph, and 'flood fill' to mark a whole component visited.

**Approach & answer.** Scan the grid; each time you hit unvisited land, that's a new island — then DFS/flood-fill to sink the entire connected landmass so you don't recount it. Treat the grid as a graph where neighbors are up/down/left/right. Mutating visited land to '0' in place is the cheap way to mark visited (mention it destroys the input; use a separate visited set if the grid must survive). Recursive DFS can stack-overflow on a huge all-land grid — an explicit stack or BFS queue is the safe alternative. Same skeleton (find a seed, flood its component, count) solves max-area-of-island and surrounded-regions.

**Use this technique when.** Connected regions / components in a grid or graph → DFS or BFS flood fill, marking visited.

**Complexity.** Time O(rows·cols) · Space O(rows·cols) worst-case recursion

```js
function numIslands(grid) {
  let count = 0;
  const sink = (r, c) => {
    if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length || grid[r][c] === '0') return;
    grid[r][c] = '0';                    // mark visited
    sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1);
  };
  for (let r = 0; r < grid.length; r++) {
    for (let c = 0; c < grid[0].length; c++) {
      if (grid[r][c] === '1') { count++; sink(r, c); }
    }
  }
  return count;
}
```

**References.** [LeetCode 200 · Number of Islands](https://leetcode.com/problems/number-of-islands/)

---

### 23. Binary Tree Level Order Traversal  `Medium`

**Pattern:** BFS (Breadth-First)

**Problem.** Return the node values grouped by level, top to bottom.

**What it tests.** Recognizing that 'level by level' means a queue, not recursion.

**Approach & answer.** Use a queue. The trick: snapshot the queue's length at the start of each level — that count is exactly the nodes on the current level. Process that many, enqueuing their children for the next round. This 'level size' technique is what separates rings cleanly without storing a depth on every node. Note that `queue.shift()` on a JS array is O(n); for large inputs mention a real queue (two-pointer head index, or a deque) to keep it O(n) overall. The same level-snapshot pattern gives you zigzag traversal, right-side-view, and level averages.

**Use this technique when.** 'Level by level', 'nearest', or 'minimum number of steps' → BFS with a queue.

**Complexity.** Time O(n) · Space O(n)

```js
function levelOrder(root) {
  if (!root) return [];
  const res = [], queue = [root];
  while (queue.length) {
    const levelSize = queue.length, level = [];
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    res.push(level);
  }
  return res;
}
```

**References.** [LeetCode 102 · Binary Tree Level Order Traversal](https://leetcode.com/problems/binary-tree-level-order-traversal/)

---

### 24. Rotting Oranges  `Medium`

**Pattern:** BFS (Breadth-First)

**Problem.** In a grid, 2 = rotten orange, 1 = fresh, 0 = empty. Each minute, rotten oranges rot 4-directional fresh neighbors. Return minutes until none are fresh, or -1 if impossible.

**What it tests.** Multi-source BFS — start from ALL rotten cells at once — and counting 'time' as BFS levels.

**Approach & answer.** Seed the queue with every rotten orange (multi-source BFS). BFS outward one minute per level, rotting fresh neighbors and enqueuing them. The number of levels processed is the elapsed time. If any fresh orange remains after the queue drains, it was unreachable — return -1. The key insight: single-source BFS finds the shortest distance from one origin, but here rot spreads from many origins at once, so you push all sources onto the queue up front and let them expand in lockstep. Track the count of fresh oranges and decrement as you rot them, so the final reachability check is O(1). Time = distance from the nearest source, which is exactly what BFS computes level by level.

**Use this technique when.** 'Time to spread', 'shortest distance from any of several sources' → multi-source BFS.

**Complexity.** Time O(rows·cols) · Space O(rows·cols)

```js
function orangesRotting(grid) {
  const q = [];
  let fresh = 0;
  for (let r = 0; r < grid.length; r++)
    for (let c = 0; c < grid[0].length; c++) {
      if (grid[r][c] === 2) q.push([r, c]);
      else if (grid[r][c] === 1) fresh++;
    }
  let minutes = 0;
  const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
  while (q.length && fresh > 0) {
    const size = q.length;
    for (let i = 0; i < size; i++) {
      const [r, c] = q.shift();
      for (const [dr, dc] of dirs) {
        const nr = r + dr, nc = c + dc;
        if (nr >= 0 && nc >= 0 && nr < grid.length && nc < grid[0].length && grid[nr][nc] === 1) {
          grid[nr][nc] = 2; fresh--; q.push([nr, nc]);
        }
      }
    }
    minutes++;
  }
  return fresh === 0 ? minutes : -1;
}
```

**References.** [LeetCode 994 · Rotting Oranges](https://leetcode.com/problems/rotting-oranges/)

---

### 25. Daily Temperatures  `Medium`

**Pattern:** Stack / Monotonic Stack

**Problem.** For each day, how many days until a warmer temperature? Return an array of waits (0 if none).

**What it tests.** The monotonic-stack trick for 'next greater element' in O(n).

**Approach & answer.** Keep a stack of indices whose warmer-day is still unknown, kept decreasing. When today is warmer than the temperature at the stack top, we've just found that day's answer — pop and record the gap. Each index is pushed/popped once → O(n), beating the O(n²) scan. The invariant is what makes it click: the stack always holds a strictly-decreasing run of temperatures, so the first day taller than today resolves possibly many pending days in a burst. This is the canonical 'next greater element' template — the same skeleton solves stock-span, largest-rectangle-in-histogram, and trapping-rain-water.

**Use this technique when.** 'Next greater/smaller element', 'span until a bigger value' → monotonic stack.

**Complexity.** Time O(n) · Space O(n)

```js
function dailyTemperatures(temps) {
  const res = new Array(temps.length).fill(0);
  const stack = [];                     // indices, decreasing temps
  for (let i = 0; i < temps.length; i++) {
    while (stack.length && temps[i] > temps[stack[stack.length - 1]]) {
      const prev = stack.pop();
      res[prev] = i - prev;
    }
    stack.push(i);
  }
  return res;
}
```

**References.** [LeetCode 739 · Daily Temperatures](https://leetcode.com/problems/daily-temperatures/) · [LeetCode 496 · Next Greater Element I](https://leetcode.com/problems/next-greater-element-i/)

---

### 26. Kth Largest Element  `Medium`

**Pattern:** Heap / Top-K

**Problem.** Return the k-th largest element in an unsorted array.

**What it tests.** Knowing a min-heap of size k beats sorting when k ≪ n — and JS has no built-in heap.

**Approach & answer.** Keep a min-heap of size k. The smallest of the k largest sits on top, so once the heap exceeds k you pop the min. The root is the answer. O(n log k) vs O(n log n) for a full sort. (In an interview, either implement a tiny binary heap or state you'd use one.) Know the alternatives too: Quickselect gives O(n) average time by partitioning around a pivot and recursing into only one side, though worst case is O(n²). The size-k heap wins when the data streams in or n is huge and k is small, because it caps memory at k. JavaScript still ships no built-in priority queue, so naming this gap and sketching the heap earns points.

**Use this technique when.** 'Top K', 'K-th largest/smallest', 'K closest' → bounded heap of size k.

**Complexity.** Time O(n log k) · Space O(k)

```js
// Minimal min-heap for the pattern; in interview, mention you'd use a PQ.
function findKthLargest(nums, k) {
  const heap = [];                       // simple array-based min-heap
  const up = i => { while (i>0){const p=(i-1)>>1; if(heap[p]<=heap[i])break; [heap[p],heap[i]]=[heap[i],heap[p]]; i=p;} };
  const down = i => { const n=heap.length; while(true){let s=i,l=2*i+1,r=2*i+2;
    if(l<n&&heap[l]<heap[s])s=l; if(r<n&&heap[r]<heap[s])s=r; if(s===i)break;
    [heap[s],heap[i]]=[heap[i],heap[s]]; i=s;} };
  for (const x of nums) {
    heap.push(x); up(heap.length - 1);
    if (heap.length > k) { heap[0] = heap.pop(); down(0); }
  }
  return heap[0];
}
```

**References.** [LeetCode 215 · Kth Largest Element](https://leetcode.com/problems/kth-largest-element-in-an-array/) · [Wikipedia · Binary heap](https://en.wikipedia.org/wiki/Binary_heap)

---

### 27. Top K Frequent Elements  `Medium`

**Pattern:** Heap / Top-K

**Problem.** Return the k most frequent elements in an array.

**What it tests.** Combining frequency map + selection, and the bucket-sort optimization.

**Approach & answer.** Count frequencies in a map. Then either a size-k heap (O(n log k)), or bucket sort by frequency (index = count) for O(n): frequencies can't exceed n, so bucket into an array of lists and read from the high-frequency end. Bucket sort is the slick optimal answer. The key realization is that the count itself is a bounded integer in [1, n], which is exactly the precondition for counting/bucket sort to beat comparison sorts. If the interviewer adds 'return them in sorted order within a frequency tier', you layer a sort inside each bucket — but the O(n) bucket pass is what they're fishing for.

**Use this technique when.** 'K most/least frequent' → frequency map, then heap or frequency-bucket sort.

**Complexity.** Time O(n) with buckets · Space O(n)

```js
function topKFrequent(nums, k) {
  const freq = new Map();
  for (const x of nums) freq.set(x, (freq.get(x) || 0) + 1);
  const buckets = Array.from({ length: nums.length + 1 }, () => []);
  for (const [num, count] of freq) buckets[count].push(num);
  const res = [];
  for (let c = buckets.length - 1; c >= 0 && res.length < k; c--) {
    for (const num of buckets[c]) { res.push(num); if (res.length === k) break; }
  }
  return res;
}
```

**References.** [LeetCode 347 · Top K Frequent Elements](https://leetcode.com/problems/top-k-frequent-elements/) · [MDN · Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)

---

### 28. Merge Intervals  `Medium`

**Pattern:** Intervals

**Problem.** Given a list of intervals, merge all overlapping ones. e.g. [[1,3],[2,6],[8,10]] → [[1,6],[8,10]].

**What it tests.** The universal interval move: sort by start, then a single sweep.

**Approach & answer.** Sort by start. Walk through; if the current interval starts before the last merged one ends, they overlap — extend the end to the max. Otherwise push a new interval. Almost every interval problem starts with 'sort by start'. The O(n log n) is dominated by the sort; the sweep is O(n). Watch the merge detail — extend with `Math.max(last[1], cur[1])`, not just `cur[1]`, because a fully-nested interval like [1,10] then [2,3] must keep the 10. Sorting by start is the setup move for insert-interval, meeting-rooms (min rooms = max concurrent), and interval-intersection; a few variants instead sort by end (for greedy 'maximum non-overlapping intervals').

**Use this technique when.** Overlapping ranges — merge, insert, count rooms, detect conflicts → sort by start, sweep once.

**Complexity.** Time O(n log n) · Space O(n)

```js
function merge(intervals) {
  intervals.sort((a, b) => a[0] - b[0]);
  const res = [intervals[0]];
  for (let i = 1; i < intervals.length; i++) {
    const last = res[res.length - 1];
    if (intervals[i][0] <= last[1]) last[1] = Math.max(last[1], intervals[i][1]);
    else res.push(intervals[i]);
  }
  return res;
}
```

**References.** [LeetCode 56 · Merge Intervals](https://leetcode.com/problems/merge-intervals/) · [LeetCode 57 · Insert Interval](https://leetcode.com/problems/insert-interval/)

---

### 29. Linked List Cycle Detection  `Medium`

**Pattern:** Linked List / Fast-Slow

**Problem.** Return true if a linked list has a cycle.

**What it tests.** Floyd's tortoise-and-hare — O(1) space instead of a visited set.

**Approach & answer.** Two pointers: slow moves 1 step, fast moves 2. If there's a cycle, fast laps slow and they meet; if fast hits null, no cycle. This 'different speeds' idea also finds the middle node and the n-th-from-end. Why they must meet: once both are inside the loop, fast closes the gap to slow by exactly one node per step, so it can never jump over — the gap hits zero. A follow-up asks for the cycle's entry node: after they meet, reset one pointer to head and advance both one step at a time; they meet again at the entry (Floyd's algorithm). It's O(1) space versus a hash set of visited nodes.

**Use this technique when.** Cycle detection, finding the middle, or n-th-from-end in one pass → fast & slow pointers.

**Complexity.** Time O(n) · Space O(1)

```js
function hasCycle(head) {
  let slow = head, fast = head;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) return true;   // they met inside the loop
  }
  return false;
}
```

**References.** [LeetCode 141 · Linked List Cycle](https://leetcode.com/problems/linked-list-cycle/) · [LeetCode 142 · Linked List Cycle II](https://leetcode.com/problems/linked-list-cycle-ii/)

---

### 30. Coin Change (fewest coins)  `Medium`

**Pattern:** Dynamic Programming

**Problem.** Given coin denominations and an amount, return the fewest coins to make that amount, or -1 if impossible.

**What it tests.** Defining the DP state and transition; why greedy fails for arbitrary coins.

**Approach & answer.** dp[a] = fewest coins to make amount a. For each amount, try every coin: dp[a] = min(dp[a], dp[a−coin] + 1). Build up from 0. Greedy (biggest coin first) is wrong for coin sets like [1,3,4] making 6 — DP is the safe answer. State + transition + base case (dp[0]=0). Initialize the rest to Infinity so unreachable amounts stay unreachable and never masquerade as a real solution; the final Infinity check maps to -1. This is the 'unbounded knapsack' shape — each coin is reusable — so the inner loop iterates coins for every amount, unlike the 0/1 knapsack where each item is used at most once.

**Use this technique when.** Min/max to reach a target from smaller sub-targets, when greedy can be fooled → bottom-up DP.

**Complexity.** Time O(amount·coins) · Space O(amount)

```js
function coinChange(coins, amount) {
  const dp = new Array(amount + 1).fill(Infinity);
  dp[0] = 0;
  for (let a = 1; a <= amount; a++) {
    for (const coin of coins) {
      if (coin <= a) dp[a] = Math.min(dp[a], dp[a - coin] + 1);
    }
  }
  return dp[amount] === Infinity ? -1 : dp[amount];
}
```

**References.** [LeetCode 322 · Coin Change](https://leetcode.com/problems/coin-change/)

---

### 31. Jump Game  `Medium`

**Pattern:** Greedy / One-Pass

**Problem.** Each element is the max jump length from that index. Starting at index 0, can you reach the last index?

**What it tests.** Greedy reachability (track the farthest index reachable) versus an exponential DFS or heavier DP.

**Approach & answer.** Track the farthest index reachable so far. Iterate left to right; if the current index i is beyond farthest, you can never land here, so return false; otherwise extend farthest = max(farthest, i + nums[i]). If farthest ever reaches the last index, return true. Greedy works because reachability is monotonic — if you can reach index i you can reach everything up to farthest, so there's never a reason to 'save' a jump. Signal: 'can you reach / minimum steps to reach the end' → greedy farthest-reach in one pass, which turns an O(2^n) branch search into O(n).

**Use this technique when.** 'Can you reach the end', minimum jumps, interval covering — anywhere a running reachable-frontier suffices.

**Complexity.** Time O(n), Space O(1)

```js
function canJump(nums) {
  let farthest = 0;
  for (let i = 0; i < nums.length; i++) {
    if (i > farthest) return false;         // stuck: can't even reach i
    farthest = Math.max(farthest, i + nums[i]);
    if (farthest >= nums.length - 1) return true;
  }
  return true;
}
```

**References.** [LeetCode · Jump Game](https://leetcode.com/problems/jump-game/) · [Wikipedia · Greedy algorithm](https://en.wikipedia.org/wiki/Greedy_algorithm)

---

### 32. Implement a Trie  `Medium`

**Pattern:** Trie (Prefix Tree)

**Problem.** Implement a prefix tree supporting insert(word), search(word), and startsWith(prefix).

**What it tests.** Knowing the trie structure for prefix queries — the data structure behind autocomplete and spell-check.

**Approach & answer.** A trie stores strings along character paths: each node holds a map of children keyed by the next character plus an isEnd flag marking where a complete word terminates. insert walks the word, creating nodes as needed, and sets isEnd on the last node. search walks the word and returns node.isEnd. startsWith walks the prefix and returns true if the whole path exists (regardless of isEnd). Signal: 'prefix', 'autocomplete', 'dictionary of words sharing prefixes', or 'word search on a board' → trie. Each operation is O(L) in the word length and independent of how many words are stored, versus O(n·L) to scan every word.

**Use this technique when.** Autocomplete / typeahead, spell-check, prefix matching, IP routing, and word-search backtracking.

**Complexity.** insert / search / startsWith O(L); space O(total characters)

```js
class Trie {
  constructor() { this.root = {}; }
  insert(word) {
    let node = this.root;
    for (const ch of word) node = node[ch] ??= {};
    node.isEnd = true;
  }
  _walk(str) {
    let node = this.root;
    for (const ch of str) { if (!node[ch]) return null; node = node[ch]; }
    return node;
  }
  search(word) { const n = this._walk(word); return !!n && !!n.isEnd; }
  startsWith(prefix) { return this._walk(prefix) !== null; }
}
```

**References.** [LeetCode · Implement Trie (Prefix Tree)](https://leetcode.com/problems/implement-trie-prefix-tree/) · [Wikipedia · Trie](https://en.wikipedia.org/wiki/Trie)

---

### 33. Course Schedule (cycle detection)  `Medium`

**Pattern:** Topological Sort / Graph

**Problem.** Given numCourses and prerequisite pairs [a, b] (b must be taken before a), can you finish all courses?

**What it tests.** Modeling dependencies as a directed graph and detecting a cycle via topological sort (Kahn's algorithm).

**Approach & answer.** Model courses as nodes and each prerequisite as a directed edge b → a. Build an adjacency list and an indegree count per node. Enqueue every node with indegree 0 (no prerequisites); repeatedly pop one, 'complete' it, and decrement each neighbor's indegree, enqueuing any that drop to 0. If you process all nodes, a valid ordering exists → true; if some remain, they sit in a cycle that can never start → false. Signal: 'ordering with dependencies', 'can this be scheduled', or 'detect a cycle in a directed graph' → topological sort (Kahn's BFS with indegrees, or DFS with a visiting/visited coloring).

**Use this technique when.** Build/task ordering, dependency resolution, package managers, and cycle detection in DAGs.

**Complexity.** Time O(V + E), Space O(V + E)

```js
function canFinish(numCourses, prerequisites) {
  const adj = Array.from({ length: numCourses }, () => []);
  const indeg = new Array(numCourses).fill(0);
  for (const [a, b] of prerequisites) { adj[b].push(a); indeg[a]++; }

  const queue = [];
  for (let i = 0; i < numCourses; i++) if (indeg[i] === 0) queue.push(i);

  let done = 0;
  while (queue.length) {
    const node = queue.shift();
    done++;
    for (const next of adj[node]) if (--indeg[next] === 0) queue.push(next);
  }
  return done === numCourses;   // leftover nodes => a cycle
}
```

**References.** [LeetCode · Course Schedule](https://leetcode.com/problems/course-schedule/) · [Wikipedia · Topological sorting](https://en.wikipedia.org/wiki/Topological_sorting)

---

### 34. Maximum Subarray (Kadane's Algorithm)  `Medium`

**Pattern:** Dynamic Programming / Kadane

**Problem.** Given an integer array (may contain negatives), find the contiguous subarray with the largest sum and return that sum. e.g. [-2,1,-3,4,-1,2,1,-5,4] → 6 (from [4,-1,2,1]).

**What it tests.** Recognising that a running sum should be reset the moment it stops helping — the core Kadane insight.

**Approach & answer.** At each index you make one decision: extend the previous best-ending-here subarray, or start fresh at the current element. Formally `cur = Math.max(x, cur + x)` — if the running sum has gone so negative that x alone is bigger, throw it away and restart. Track a separate `best` for the global maximum, because the best window may have ended before the array does. The trap is initialising `best` to 0: with an all-negative array like [-3,-1,-2] the answer is -1, not 0, so seed both `cur` and `best` with the first element (or -Infinity) and iterate from index 1. This is a 1-D DP collapsed to O(1) space — the 'DP where each state depends only on the previous state' family, the same shape as house-robber and best-time-to-buy-sell. To also recover the indices, remember where `cur` reset.

**Use this technique when.** Best contiguous run (max sum / max product) in one pass → carry a running value, reset it when it stops helping.

**Complexity.** Time O(n) · Space O(1)

```js
function maxSubArray(nums) {
  let cur = nums[0], best = nums[0];
  for (let i = 1; i < nums.length; i++) {
    cur = Math.max(nums[i], cur + nums[i]); // extend or restart
    best = Math.max(best, cur);
  }
  return best;
}
```

**References.** [LeetCode 53 · Maximum Subarray](https://leetcode.com/problems/maximum-subarray/) · [Wikipedia · Maximum subarray problem](https://en.wikipedia.org/wiki/Maximum_subarray_problem)

---

### 35. Product of Array Except Self  `Medium`

**Pattern:** Prefix Sum

**Problem.** Return an array where output[i] is the product of every element except nums[i] — without using division and in O(n). e.g. [1,2,3,4] → [24,12,8,6].

**What it tests.** Turning a 'combine everything but me' requirement into a prefix pass and a suffix pass.

**Approach & answer.** The no-division constraint is the whole point (division breaks on a zero anyway). Answer for i is (product of everything left of i) × (product of everything right of i). Do two sweeps: a left-to-right pass filling res[i] with the running prefix product, then a right-to-left pass multiplying in the running suffix product. You can keep the suffix in a single scalar so no second array is needed — O(1) extra space beyond the output. This is the prefix-sum pattern in multiplicative form: precompute cumulative results from both ends so each answer is an O(1) combine. Zeros are handled automatically — one zero makes every other slot's prefix-or-suffix carry it, and the zero slot itself gets the product of all non-zero neighbours.

**Use this technique when.** Each output combines everything on both sides of i → prefix pass + suffix pass, no division.

**Complexity.** Time O(n) · Space O(1) extra (output aside)

```js
function productExceptSelf(nums) {
  const n = nums.length, res = new Array(n).fill(1);
  for (let i = 1; i < n; i++) res[i] = res[i - 1] * nums[i - 1]; // prefix
  let suffix = 1;
  for (let i = n - 1; i >= 0; i--) {
    res[i] *= suffix;         // combine with running suffix
    suffix *= nums[i];
  }
  return res;
}
```

**References.** [LeetCode 238 · Product of Array Except Self](https://leetcode.com/problems/product-of-array-except-self/)

---

### 36. Number of Connected Components  `Medium`

**Pattern:** Union-Find (Disjoint Set)

**Problem.** Given n nodes labelled 0..n-1 and a list of undirected edges, count how many connected components the graph has. e.g. n=5, edges=[[0,1],[1,2],[3,4]] → 2.

**What it tests.** Reaching for Union-Find (Disjoint Set Union) when the question is purely about connectivity/grouping rather than paths.

**Approach & answer.** Start with n components. Each edge unions two nodes; a union that actually merges two different sets drops the count by one. Union-Find keeps a `parent` array where `find(x)` walks to the set representative and `union(a,b)` links one root under the other. Two optimisations make it near-O(1) amortised per op: path compression (point nodes directly at the root during find) and union by rank/size (attach the smaller tree under the larger). The signal for DSU: the problem is about 'are these in the same group / how many groups', edges arrive incrementally, or you need cycle detection in an undirected graph — cheaper and simpler than BFS/DFS flood-fill when you only care about membership, not traversal order or shortest path. Same tool powers accounts-merge, redundant-connection, and Kruskal's MST.

**Use this technique when.** Connectivity / grouping / 'same set?' / undirected cycle detection, edges added incrementally → Union-Find.

**Complexity.** Time ~O((n+e)·α(n)) ≈ near-linear · Space O(n)

```js
function countComponents(n, edges) {
  const parent = Array.from({ length: n }, (_, i) => i);
  const find = x => {
    while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } // path compression
    return x;
  };
  let count = n;
  for (const [a, b] of edges) {
    const ra = find(a), rb = find(b);
    if (ra !== rb) { parent[ra] = rb; count--; } // a real merge
  }
  return count;
}
```

**References.** [LeetCode 323 · Number of Connected Components](https://leetcode.com/problems/number-of-connected-components-in-an-undirected-graph/) · [Wikipedia · Disjoint-set data structure](https://en.wikipedia.org/wiki/Disjoint-set_data_structure)

---

### 37. LRU Cache  `Medium`

**Pattern:** Design (Hash Map + Doubly Linked List)

**Problem.** Design a cache with fixed capacity supporting get(key) and put(key,value), both in O(1). When full, evict the least-recently-used entry. Any get or put counts as a use.

**What it tests.** Composing two structures so that both lookup and recency-reordering are O(1) — the canonical 'design' interview.

**Approach & answer.** No single structure gives you both O(1) lookup and O(1) 'move to most-recent'. The standard answer composes two: a hash map from key → node for O(1) find, and a doubly linked list holding nodes in usage order (most-recent at the head, least-recent at the tail). On get: look up the node, unlink it, splice it to the head, return its value. On put: if the key exists, update and move to head; otherwise create a node at the head and, if over capacity, drop the tail node and delete its key from the map. The doubly linked list is essential — you need O(1) removal of an arbitrary node, which a singly linked list can't do without the predecessor. A dummy head and dummy tail sentinel remove all the null-edge bookkeeping. In JS you can cheat with a `Map`, which preserves insertion order: delete-then-set moves a key to the end, and `map.keys().next().value` is the oldest — but interviewers usually want the explicit map+DLL to prove you understand why. LFU is the harder cousin (frequency buckets).

**Use this technique when.** Need O(1) lookup AND O(1) ordering/recency updates → hash map for find + doubly linked list for order.

**Complexity.** Time O(1) get/put · Space O(capacity)

```js
class LRUCache {
  constructor(capacity) { this.cap = capacity; this.map = new Map(); }
  get(key) {
    if (!this.map.has(key)) return -1;
    const val = this.map.get(key);
    this.map.delete(key); this.map.set(key, val); // move to most-recent
    return val;
  }
  put(key, value) {
    if (this.map.has(key)) this.map.delete(key);
    this.map.set(key, value);
    if (this.map.size > this.cap) {
      this.map.delete(this.map.keys().next().value); // evict oldest
    }
  }
}
```

**References.** [LeetCode 146 · LRU Cache](https://leetcode.com/problems/lru-cache/) · [MDN · Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)

---

### 38. Spiral Matrix  `Medium`

**Pattern:** Matrix Traversal

**Problem.** Return all elements of an m×n matrix in spiral order (right across the top, down the right side, left across the bottom, up the left side, inward). e.g. [[1,2,3],[4,5,6],[7,8,9]] → [1,2,3,6,9,8,7,4,5].

**What it tests.** Managing four shrinking boundaries without off-by-one errors — disciplined index bookkeeping.

**Approach & answer.** Track four boundaries — top, bottom, left, right — and peel one layer per loop: traverse top row left→right then top++, right column top→bottom then right--, bottom row right→left then bottom--, left column bottom→top then left++. The two guards that prevent duplicate visits on a non-square or single-row/column matrix: before the bottom-row pass check `top <= bottom`, and before the left-column pass check `left <= right`; otherwise a thin matrix re-reads a row or column. Loop while `top <= bottom && left <= right`. There's no clever trick here — the interview signal is whether you keep the boundaries and the four directions straight and handle the degenerate shapes. The same boundary-shrinking idea generalises to rotate-image and spiral-matrix-II (fill instead of read).

**Use this technique when.** Layer-by-layer or directional walk over a grid → maintain shrinking top/bottom/left/right boundaries.

**Complexity.** Time O(m·n) · Space O(1) extra (output aside)

```js
function spiralOrder(matrix) {
  const res = [];
  let top = 0, bottom = matrix.length - 1;
  let left = 0, right = matrix[0].length - 1;
  while (top <= bottom && left <= right) {
    for (let c = left; c <= right; c++) res.push(matrix[top][c]);
    top++;
    for (let r = top; r <= bottom; r++) res.push(matrix[r][right]);
    right--;
    if (top <= bottom) { for (let c = right; c >= left; c--) res.push(matrix[bottom][c]); bottom--; }
    if (left <= right) { for (let r = bottom; r >= top; r--) res.push(matrix[r][left]); left++; }
  }
  return res;
}
```

**References.** [LeetCode 54 · Spiral Matrix](https://leetcode.com/problems/spiral-matrix/)

---

### 39. Validate Binary Search Tree  `Medium`

**Pattern:** DFS (Depth-First)

**Problem.** Determine whether a binary tree is a valid BST: every node's left subtree holds only smaller values, its right subtree only larger, and both subtrees are themselves valid BSTs.

**What it tests.** Understanding that BST validity is a global range constraint, not just a local parent-child check.

**Approach & answer.** The classic wrong answer only compares each node to its immediate children — that passes trees that aren't BSTs, because a node deep in the left subtree can still exceed the root. Validity is a *range* property: carry a (low, high) open interval down the recursion. The root is unbounded (-∞, +∞); going left tightens the upper bound to the parent's value, going right tightens the lower bound. A node is valid iff `low < node.val < high` and both children validate against their narrowed ranges. Use strict comparisons if duplicates are disallowed. An equivalent solution: an in-order traversal of a BST yields strictly increasing values, so walk in-order tracking the previous value and fail if it ever doesn't increase — that's often the cleaner code. Either way it's DFS carrying state down (the range) versus DFS reading state across (the in-order predecessor).

**Use this technique when.** Tree property that depends on ancestors, not just parent → DFS carrying a (low, high) range down each branch.

**Complexity.** Time O(n) · Space O(h) recursion

```js
function isValidBST(root, low = -Infinity, high = Infinity) {
  if (!root) return true;
  if (root.val <= low || root.val >= high) return false; // global range check
  return isValidBST(root.left, low, root.val) &&
         isValidBST(root.right, root.val, high);
}
```

**References.** [LeetCode 98 · Validate Binary Search Tree](https://leetcode.com/problems/validate-binary-search-tree/) · [Wikipedia · Binary search tree](https://en.wikipedia.org/wiki/Binary_search_tree)

---

### 40. House Robber  `Medium`

**Pattern:** Dynamic Programming

**Problem.** Given an array where each element is the money in a house along a street, maximise what you can rob without robbing two adjacent houses. e.g. [2,7,9,3,1] → 12 (rob houses 0, 2, 4).

**What it tests.** Spotting the 'include-or-skip with an adjacency constraint' recurrence and collapsing it to O(1) space.

**Approach & answer.** At each house you choose: rob it (its money + the best up to two houses back, since the neighbour is off-limits) or skip it (the best up to the previous house). So `dp[i] = max(dp[i-1], dp[i-2] + nums[i])`. Because each state looks back only two steps, you don't need the whole table — carry two rolling scalars `prev1` (best up to i-1) and `prev2` (best up to i-2) and update in place, giving O(1) space. This 'take-or-leave with a no-adjacent constraint' is a distinct DP signal from unbounded-choice problems like coin-change: here the constraint is positional adjacency. Variants layer on top: house-robber-II wraps the street into a circle (run the linear version twice — once excluding house 0, once excluding the last — and take the max), and the tree version (rob a binary tree) applies the same include/exclude at each node via DFS returning a pair.

**Use this technique when.** Max/min over a sequence where picking i forbids i±1 → dp[i] = max(dp[i-1], dp[i-2] + value[i]).

**Complexity.** Time O(n) · Space O(1)

```js
function rob(nums) {
  let prev2 = 0, prev1 = 0; // best up to i-2 and i-1
  for (const n of nums) {
    const cur = Math.max(prev1, prev2 + n); // skip vs. rob this house
    prev2 = prev1;
    prev1 = cur;
  }
  return prev1;
}
```

**References.** [LeetCode 198 · House Robber](https://leetcode.com/problems/house-robber/) · [LeetCode 213 · House Robber II](https://leetcode.com/problems/house-robber-ii/)

---

### 41. Meeting Rooms II (minimum rooms)  `Medium`

**Pattern:** Intervals

**Problem.** Given meeting intervals [[start,end], ...], return the minimum number of rooms needed so no two overlapping meetings share a room. e.g. [[0,30],[5,10],[15,20]] → 2.

**What it tests.** Reframing 'minimum resources' as 'maximum concurrent intervals' and computing it with a sweep or a heap.

**Approach & answer.** The minimum rooms equals the maximum number of meetings happening at the same instant. Two standard solutions. (1) Min-heap of end times: sort meetings by start; for each meeting, if the earliest-ending room (heap top) is free by its start, reuse that room (pop); always push the current end. The heap size at the end is the peak concurrency. (2) Sweep line / two-pointer: split into sorted start times and sorted end times, walk both; a start before the next end needs a new room (rooms++, advance start), otherwise a meeting freed a room (rooms--, advance end) — track the running max. Both are O(n log n) dominated by the sort. This is the 'sort by start' interval family extended to a resource-counting question; the key reframing — 'min rooms = max overlap' — is exactly what the interviewer is checking. Same counting shows up in car-pooling and minimum-platforms.

**Use this technique when.** Minimum concurrent resources for overlapping intervals → count max overlap via a min-heap of end times or a start/end sweep.

**Complexity.** Time O(n log n) · Space O(n)

```js
function minMeetingRooms(intervals) {
  const starts = intervals.map(i => i[0]).sort((a, b) => a - b);
  const ends = intervals.map(i => i[1]).sort((a, b) => a - b);
  let rooms = 0, maxRooms = 0, e = 0;
  for (let s = 0; s < starts.length; s++) {
    if (starts[s] < ends[e]) rooms++;   // a meeting starts before one ends
    else e++;                           // a room freed up
    maxRooms = Math.max(maxRooms, rooms);
  }
  return maxRooms;
}
```

**References.** [LeetCode 253 · Meeting Rooms II](https://leetcode.com/problems/meeting-rooms-ii/)

---

### 42. Longest Common Subsequence  `Medium`

**Pattern:** Dynamic Programming (2D)

**Problem.** Given two strings text1 and text2, return the length of their longest common subsequence — characters appearing left-to-right but not necessarily contiguous. e.g. 'abcde' and 'ace' → 3 ('ace').

**What it tests.** Recognising a two-sequence problem as a 2D grid DP, and defining the state as the LCS of the two prefixes.

**Approach & answer.** When a DP ranges over two sequences, the state is almost always a 2D table indexed by a prefix of each: dp[i][j] = LCS length of text1[0..i) and text2[0..j). The transition reads off the last characters — if text1[i-1] === text2[j-1] they extend a common subsequence, so dp[i][j] = dp[i-1][j-1] + 1; otherwise the best comes from dropping one character, dp[i][j] = max(dp[i-1][j], dp[i][j-1]). Row 0 and column 0 are the empty-prefix base cases (0). This same grid powers edit distance, longest common substring (reset to 0 on mismatch), and diff tools. Recognition signal: two strings/arrays plus 'subsequence' or 'transform A into B' → 2D DP over prefixes. Space drops to O(min(m,n)) by keeping only the previous row.

**Use this technique when.** Two sequences and you need an optimal alignment/subsequence/edit measure → dp[i][j] over the two prefixes, transition on whether the last elements match.

**Complexity.** Time O(m·n) · Space O(m·n), reducible to O(min(m,n))

```js
function longestCommonSubsequence(a, b) {
  const m = a.length, n = b.length;
  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
      else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
    }
  }
  return dp[m][n];
}
```

**References.** [LeetCode 1143 · Longest Common Subsequence](https://leetcode.com/problems/longest-common-subsequence/) · [Wikipedia · Longest common subsequence](https://en.wikipedia.org/wiki/Longest_common_subsequence)

---

### 43. Word Break  `Medium`

**Pattern:** Dynamic Programming (partition)

**Problem.** Given a string s and a dictionary of words, return true if s can be segmented into a space-separated sequence of one or more dictionary words. e.g. s='leetcode', dict=['leet','code'] → true.

**What it tests.** Spotting that 'can this be partitioned' over a string is a 1D DP where each position asks whether some prior cut point is reachable and the gap between them is a valid word.

**Approach & answer.** Segmentation/partition questions on a string map to a boolean 1D DP: dp[i] = 'the prefix s[0..i) can be fully segmented'. dp[0] = true (empty prefix). For each end i, look back to every cut j < i: if dp[j] is true and s[j..i) is in the dictionary, then dp[i] is true. The answer is dp[n]. Put the dictionary in a Set for O(1) membership. This is O(n^2) cut points times the substring/lookup cost. The recognition signal is 'break/segment/partition a string using pieces from a set' → dp over prefix-reachability; the same shape solves word-break-II (store the actual cuts) and decode-ways where the 'dictionary' is implicit. A common optimisation caps the inner loop by the longest dictionary word.

**Use this technique when.** Deciding if a string can be split into allowed pieces (or counting such splits) → dp[i] = prefix s[0..i) is reachable, transition over cut points with a Set lookup.

**Complexity.** Time O(n²·k) · Space O(n) (k = substring cost)

```js
function wordBreak(s, wordDict) {
  const words = new Set(wordDict);
  const n = s.length;
  const dp = new Array(n + 1).fill(false);
  dp[0] = true;
  for (let i = 1; i <= n; i++) {
    for (let j = 0; j < i; j++) {
      if (dp[j] && words.has(s.slice(j, i))) { dp[i] = true; break; }
    }
  }
  return dp[n];
}
```

**References.** [LeetCode 139 · Word Break](https://leetcode.com/problems/word-break/) · [Wikipedia · Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming)

---

### 44. Unique Paths  `Medium`

**Pattern:** Dynamic Programming (grid count)

**Problem.** A robot sits at the top-left of an m×n grid and can move only right or down. How many distinct paths reach the bottom-right corner? e.g. 3×7 → 28.

**What it tests.** Recognising a grid-counting DP where each cell's path count is the SUM of the ways to reach the cells feeding into it.

**Approach & answer.** Counting paths on a grid is the canonical additive 2D DP: dp[i][j] = number of ways to reach cell (i,j). Because moves are only right/down, the only predecessors are the cell above and the cell to the left, so dp[i][j] = dp[i-1][j] + dp[i][j-1]. The first row and first column are all 1 (a single straight-line path). Answer is dp[m-1][n-1]. It collapses to one rolling row for O(n) space, and there is even a closed form C(m+n-2, m-1) since a path is just a choice of which of the m+n-2 steps go down. Recognition signal: 'count the number of ways to reach X' with local moves → additive DP summing the reachable predecessors (contrast min/max path DP, which takes the BEST predecessor, not the sum). Obstacles simply force those cells to 0.

**Use this technique when.** Counting paths/ways to reach a target under local move rules → dp[cell] = sum of dp[predecessors]; switch sum→max/min when you need best-cost paths instead of counts.

**Complexity.** Time O(m·n) · Space O(n) with a rolling row

```js
function uniquePaths(m, n) {
  const row = new Array(n).fill(1);
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      row[j] += row[j - 1];        // ways from above (row[j]) + from left (row[j-1])
    }
  }
  return row[n - 1];
}
```

**References.** [LeetCode 62 · Unique Paths](https://leetcode.com/problems/unique-paths/) · [Wikipedia · Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming)

---

### 45. Decode Ways  `Medium`

**Pattern:** Dynamic Programming (1D)

**Problem.** A message of digits is encoded where 'A'→1 … 'Z'→26. Given a digit string, count how many ways it can be decoded. e.g. '226' → 3 ('2 2 6', '22 6', '2 26'). Leading zeros are invalid.

**What it tests.** A Fibonacci-shaped 1D DP with guard conditions — the tricky part is validating one- and two-digit takes, especially zeros.

**Approach & answer.** This is climbing-stairs with validity rules: dp[i] = number of ways to decode the prefix of length i. From position i you can consume one digit (valid only if it is 1–9), contributing dp[i-1], or two digits (valid only if the pair is 10–26), contributing dp[i-2]. Base cases dp[0] = 1 (empty) and dp[1] = (s[0] is not '0'). The zero is the whole trap: a '0' has no single-digit decoding and is legal only as the second digit of 10 or 20, so an isolated or badly placed 0 zeroes the count. Recognition signal: 'count the number of ways to build/parse a sequence one or two steps at a time' → additive DP dp[i] = dp[i-1] + dp[i-2] gated by which steps are legal here. Only the last two values are ever needed, so O(1) space.

**Use this technique when.** Counting decodings/tilings/step-sequences where each move consumes 1 or 2 units under validity rules → gated Fibonacci-style dp[i] = (valid1 ? dp[i-1] : 0) + (valid2 ? dp[i-2] : 0).

**Complexity.** Time O(n) · Space O(1)

```js
function numDecodings(s) {
  if (!s || s[0] === '0') return 0;
  let prev2 = 1, prev1 = 1;                 // dp[0], dp[1]
  for (let i = 1; i < s.length; i++) {
    let cur = 0;
    if (s[i] !== '0') cur += prev1;          // single digit 1-9
    const two = Number(s.slice(i - 1, i + 1));
    if (two >= 10 && two <= 26) cur += prev2; // pair 10-26
    prev2 = prev1;
    prev1 = cur;
  }
  return prev1;
}
```

**References.** [LeetCode 91 · Decode Ways](https://leetcode.com/problems/decode-ways/) · [Wikipedia · Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming)

---

### 46. Maximum Product Subarray  `Medium`

**Pattern:** Dynamic Programming (running extremes)

**Problem.** Given an integer array, return the maximum product of any contiguous non-empty subarray. e.g. [2,3,-2,4] → 6, [-2,3,-4] → 24.

**What it tests.** Realising why Kadane's single running max fails for products, and carrying BOTH a running max and running min because a negative flips them.

**Approach & answer.** Products break the pure Kadane template, because multiplying by a negative turns the smallest (most negative) running value into the largest. So track a pair at each index: maxHere = best product ending here, minHere = worst (most negative) product ending here. For each number x the candidates are x alone, maxHere*x, and minHere*x; the new max is the largest of the three and the new min is the smallest. When x is negative this naturally lets a previous min become the new max. Keep a global best. This is the 'Kadane with sign-tracking' variant — the recognition signal is a running-subarray optimum where the combining operation is not monotonic (products, or sign changes), so one running extreme is not enough; carry both extremes. O(n) time, O(1) space.

**Use this technique when.** Best contiguous-subarray value where a single running extreme can be flipped (products, sign changes) → carry both the running max and running min and recombine each step.

**Complexity.** Time O(n) · Space O(1)

```js
function maxProduct(nums) {
  let maxHere = nums[0], minHere = nums[0], best = nums[0];
  for (let i = 1; i < nums.length; i++) {
    const x = nums[i];
    const cand = [x, maxHere * x, minHere * x];
    maxHere = Math.max(...cand);
    minHere = Math.min(...cand);
    best = Math.max(best, maxHere);
  }
  return best;
}
```

**References.** [LeetCode 152 · Maximum Product Subarray](https://leetcode.com/problems/maximum-product-subarray/) · [Wikipedia · Maximum subarray problem](https://en.wikipedia.org/wiki/Maximum_subarray_problem)

---

### 47. Rotate Image (90° in place)  `Medium`

**Pattern:** Matrix (in-place transform)

**Problem.** Rotate an n×n matrix 90° clockwise in place, using no second matrix. e.g. [[1,2,3],[4,5,6],[7,8,9]] → [[7,4,1],[8,5,2],[9,6,3]].

**What it tests.** Decomposing an in-place rotation into two simple reversible passes — transpose then reverse each row — instead of juggling four-way index swaps.

**Approach & answer.** The clean in-place trick is to factor the rotation into two operations you already trust. Transpose the matrix (swap a[i][j] with a[j][i], iterating only j > i so you don't undo it), which reflects across the main diagonal; then reverse each row. Transpose + row-reverse = 90° clockwise; transpose + column-reverse (or reverse rows first) gives counter-clockwise. Both passes use O(1) extra space and touch each cell once, so O(n^2) time. The alternative — rotating four cells at a time in concentric rings — is also O(1) space but far more error-prone to write under pressure. Recognition signal: 'rotate/reflect a square matrix in place' → express the geometric transform as a composition of transpose and axis reversals.

**Use this technique when.** In-place square-matrix rotation or reflection → compose transpose with row/column reversal rather than hand-rolling four-way ring swaps.

**Complexity.** Time O(n²) · Space O(1)

```js
function rotate(matrix) {
  const n = matrix.length;
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]; // transpose
    }
  }
  for (let i = 0; i < n; i++) matrix[i].reverse();                 // reverse each row
  return matrix;
}
```

**References.** [LeetCode 48 · Rotate Image](https://leetcode.com/problems/rotate-image/) · [Wikipedia · Transpose](https://en.wikipedia.org/wiki/Transpose)

---

### 48. Set Matrix Zeroes  `Medium`

**Pattern:** Matrix (in-place markers)

**Problem.** Given an m×n matrix, if a cell is 0 set its entire row and column to 0, in place. The catch: use O(1) extra space, not O(m+n) marker arrays.

**What it tests.** Avoiding the naive extra row/column marker arrays by reusing the matrix's own first row and column as the marker storage.

**Approach & answer.** The trap is that zeroing as you scan corrupts data you still need to read, so beginners allocate rows[] and cols[] boolean arrays (O(m+n)). The O(1) trick reuses the first row and first column as those marker arrays: first record separately whether row 0 and column 0 themselves contain a zero; then for every inner cell (i,j) that is 0, stamp matrix[i][0]=0 and matrix[0][j]=0. In a second pass, zero any inner cell whose row-marker or column-marker is 0. Finally, using the two flags saved at the start, zero the first row and/or first column if needed. Recognition signal: an in-place matrix pass where writes would clobber future reads → carve the marker state out of the structure itself (here the borders) and order the passes so all reads precede the writes. O(m·n) time, O(1) space.

**Use this technique when.** In-place matrix mutation where the marks you need would be overwritten by the mutation → repurpose a border row/column as the marker store and sequence the passes so reads come before writes.

**Complexity.** Time O(m·n) · Space O(1)

```js
function setZeroes(matrix) {
  const m = matrix.length, n = matrix[0].length;
  let firstRow = false, firstCol = false;
  for (let j = 0; j < n; j++) if (matrix[0][j] === 0) firstRow = true;
  for (let i = 0; i < m; i++) if (matrix[i][0] === 0) firstCol = true;
  for (let i = 1; i < m; i++)
    for (let j = 1; j < n; j++)
      if (matrix[i][j] === 0) { matrix[i][0] = 0; matrix[0][j] = 0; }
  for (let i = 1; i < m; i++)
    for (let j = 1; j < n; j++)
      if (matrix[i][0] === 0 || matrix[0][j] === 0) matrix[i][j] = 0;
  if (firstRow) for (let j = 0; j < n; j++) matrix[0][j] = 0;
  if (firstCol) for (let i = 0; i < m; i++) matrix[i][0] = 0;
  return matrix;
}
```

**References.** [LeetCode 73 · Set Matrix Zeroes](https://leetcode.com/problems/set-matrix-zeroes/) · [Wikipedia · In-place algorithm](https://en.wikipedia.org/wiki/In-place_algorithm)

---

### 49. Non-overlapping Intervals  `Medium`

**Pattern:** Greedy (interval scheduling)

**Problem.** Given a set of intervals, return the minimum number you must remove so the rest do not overlap. e.g. [[1,2],[2,3],[3,4],[1,3]] → 1 (remove [1,3]).

**What it tests.** Recognising the classic greedy 'activity selection' — sort by END time and keep the interval that finishes earliest whenever there's a conflict.

**Approach & answer.** Minimising removals is the same as maximising how many intervals you keep without overlap — the classic interval-scheduling / activity-selection problem, and it is greedy, not DP. Sort by END time; walk through tracking the end of the last kept interval. If the next interval starts at or after that end, keep it and advance the end; otherwise it overlaps, so count a removal and skip it. Keeping the earliest-finishing interval at each conflict is provably optimal because it leaves the most room for the rest (an exchange argument). Removals = total − kept. The distinct signal versus merge-intervals is the objective: merge/insert wants to combine overlaps and sorts by START, while scheduling wants to select a maximum non-overlapping set and sorts by END. O(n log n) for the sort.

**Use this technique when.** Maximise how many intervals fit without overlap (or minimise removals) → greedy: sort by END time and keep the earliest finisher on each conflict.

**Complexity.** Time O(n log n) · Space O(1)

```js
function eraseOverlapIntervals(intervals) {
  if (intervals.length === 0) return 0;
  intervals.sort((a, b) => a[1] - b[1]);      // sort by end time
  let kept = 1, end = intervals[0][1];
  for (let i = 1; i < intervals.length; i++) {
    if (intervals[i][0] >= end) { kept++; end = intervals[i][1]; }
  }
  return intervals.length - kept;
}
```

**References.** [LeetCode 435 · Non-overlapping Intervals](https://leetcode.com/problems/non-overlapping-intervals/) · [Wikipedia · Interval scheduling](https://en.wikipedia.org/wiki/Interval_scheduling)

---

### 50. Clone Graph  `Medium`

**Pattern:** Graph traversal + hashmap

**Problem.** Given a reference to a node in a connected undirected graph, return a deep copy: every node cloned, every edge reproduced, no node duplicated. Nodes carry a value and a neighbours list.

**What it tests.** Using a hashmap from original-node → clone as BOTH the visited set and the lookup that stops cycles from causing infinite recursion.

**Approach & answer.** Deep-copying a graph is a traversal (DFS or BFS) whose crucial extra ingredient is a map from each original node to its clone. That single map does two jobs: it tells you whether a node has already been cloned (so cycles and shared neighbours don't spin forever or duplicate), and it lets you wire cloned neighbours by looking up each original neighbour's clone. The recipe: on visiting a node, if it's in the map return its clone; otherwise create the clone, put it in the map BEFORE recursing (order matters — this is what breaks cycles), then for each neighbour recurse and push the returned clone into the copy's neighbour list. Recognition signal: 'deep copy / clone a linked structure with cycles or shared references' → traverse while memoising original→copy in a map. The same map pattern clones a linked list with random pointers.

**Use this technique when.** Deep-copying any graph or linked structure that may contain cycles or shared nodes → DFS/BFS with a Map from original node to its clone, populated before recursing.

**Complexity.** Time O(V + E) · Space O(V)

```js
function cloneGraph(node, seen = new Map()) {
  if (!node) return null;
  if (seen.has(node)) return seen.get(node);
  const copy = { val: node.val, neighbors: [] };
  seen.set(node, copy);                    // record BEFORE recursing to break cycles
  for (const nb of node.neighbors) {
    copy.neighbors.push(cloneGraph(nb, seen));
  }
  return copy;
}
```

**References.** [LeetCode 133 · Clone Graph](https://leetcode.com/problems/clone-graph/) · [Wikipedia · Depth-first search](https://en.wikipedia.org/wiki/Depth-first_search)

---

### 51. Word Search  `Medium`

**Pattern:** Backtracking (grid)

**Problem.** Given a grid of characters and a word, return true if the word can be formed by a path of horizontally/vertically adjacent cells, each cell used at most once. e.g. find 'ABCCED' in the letter grid.

**What it tests.** Grid backtracking: DFS that marks a cell used, explores neighbours, and — crucially — un-marks on the way out so other paths can reuse it.

**Approach & answer.** This is DFS with backtracking on a grid, and it differs from flood-fill (Number of Islands) in one essential way: because a cell may be reused by a DIFFERENT path, you must undo your mark when a branch fails. Start a DFS from every cell matching word[0]. At depth k, if the current cell equals word[k], temporarily mark it (e.g. overwrite with a sentinel like '#'), recurse into the four neighbours for word[k+1], and if none succeed, restore the original character before returning false. That restore is the backtracking step — skip it and you wrongly forbid cells on sibling paths. Success is reaching k === word.length - 1. Recognition signal: 'find a path/arrangement subject to a used-once constraint' → DFS that mutates state going in and reverts it coming out. Worst case O(cells · 4^len) since each step branches four ways.

**Use this technique when.** Search for a path or arrangement on a grid/board where cells (or choices) can't repeat within one attempt → DFS that marks state on entry and reverts it on exit.

**Complexity.** Time O(m·n·4^L) · Space O(L) recursion (L = word length)

```js
function exist(board, word) {
  const m = board.length, n = board[0].length;
  function dfs(i, j, k) {
    if (i < 0 || j < 0 || i >= m || j >= n || board[i][j] !== word[k]) return false;
    if (k === word.length - 1) return true;
    const tmp = board[i][j];
    board[i][j] = '#';                                  // mark used
    const found = dfs(i + 1, j, k + 1) || dfs(i - 1, j, k + 1) ||
                  dfs(i, j + 1, k + 1) || dfs(i, j - 1, k + 1);
    board[i][j] = tmp;                                  // backtrack: restore
    return found;
  }
  for (let i = 0; i < m; i++)
    for (let j = 0; j < n; j++)
      if (dfs(i, j, 0)) return true;
  return false;
}
```

**References.** [LeetCode 79 · Word Search](https://leetcode.com/problems/word-search/) · [Wikipedia · Backtracking](https://en.wikipedia.org/wiki/Backtracking)

---

### 52. Longest Consecutive Sequence  `Medium`

**Pattern:** Hashing (O(n) set trick)

**Problem.** Given an unsorted integer array, return the length of the longest run of consecutive integers. e.g. [100,4,200,1,3,2] → 4 (for 1,2,3,4). Required: O(n) time, so no sorting.

**What it tests.** Beating the obvious O(n log n) sort with a hash set, plus the key insight of only starting a count from a number that has no left-neighbour.

**Approach & answer.** Sorting gives the answer trivially but costs O(n log n); the O(n) solution is a hash set with one clever guard. Put every number in a Set. Then for each number, only begin counting a streak if n-1 is NOT in the set — i.e. this number is the START of its run. From a start, walk n+1, n+2, … while they are present, counting length. The guard is what makes it linear: each number is the interior of exactly one streak and is walked only once (from its start), so total work is O(n) despite the nested loop. Recognition signal: 'longest consecutive/adjacent group with no ordering requirement' under an O(n) constraint → hash set for O(1) membership plus start-of-run detection to avoid recounting. Trades O(n) space for the speedup.

**Use this technique when.** Longest consecutive grouping of values needed in O(n) (sorting disallowed) → dump into a Set and expand runs only from elements that have no predecessor.

**Complexity.** Time O(n) · Space O(n)

```js
function longestConsecutive(nums) {
  const set = new Set(nums);
  let best = 0;
  for (const n of set) {
    if (set.has(n - 1)) continue;      // only start from a run's beginning
    let len = 1, cur = n;
    while (set.has(cur + 1)) { cur++; len++; }
    best = Math.max(best, len);
  }
  return best;
}
```

**References.** [LeetCode 128 · Longest Consecutive Sequence](https://leetcode.com/problems/longest-consecutive-sequence/) · [Wikipedia · Hash table](https://en.wikipedia.org/wiki/Hash_table)

---

### 53. Longest Palindromic Substring  `Medium`

**Pattern:** Expand around center

**Problem.** Return the longest contiguous substring of s that is a palindrome. e.g. 'babad' → 'bab' (or 'aba'), 'cbbd' → 'bb'.

**What it tests.** The expand-around-center technique, and remembering there are 2n-1 centers because palindromes can be odd- or even-length.

**Approach & answer.** A palindrome is symmetric about a center, so instead of checking all O(n^2) substrings for the palindrome property (another O(n) each), fix the center and expand outward while the two sides match. There are 2n-1 centers: n single characters (odd-length palindromes) and n-1 gaps between adjacent characters (even-length). For each center grow left/right pointers while in-bounds and equal, tracking the longest span seen. That is O(n^2) time and O(1) space, and it is the expected interview answer; the O(n) Manacher's algorithm exists but is rarely required. Recognition signal: 'longest/all palindromic substrings' → expand around each of the 2n-1 centers (the same engine COUNTS palindromic substrings by summing every successful expansion). A dp[i][j] = 'is s[i..j] a palindrome' table also works but costs O(n^2) space.

**Use this technique when.** Finding or counting palindromic substrings → expand around all 2n-1 centers, handling odd and even lengths, tracking the best span.

**Complexity.** Time O(n²) · Space O(1)

```js
function longestPalindrome(s) {
  if (s.length < 2) return s;
  let start = 0, maxLen = 1;
  function expand(l, r) {
    while (l >= 0 && r < s.length && s[l] === s[r]) { l--; r++; }
    const len = r - l - 1;
    if (len > maxLen) { maxLen = len; start = l + 1; }
  }
  for (let i = 0; i < s.length; i++) {
    expand(i, i);       // odd-length center
    expand(i, i + 1);   // even-length center
  }
  return s.slice(start, start + maxLen);
}
```

**References.** [LeetCode 5 · Longest Palindromic Substring](https://leetcode.com/problems/longest-palindromic-substring/) · [Wikipedia · Longest palindromic substring](https://en.wikipedia.org/wiki/Longest_palindromic_substring)

---

### 54. Lowest Common Ancestor of a BST  `Medium`

**Pattern:** BST property walk

**Problem.** Given a binary search tree and two nodes p and q, return their lowest common ancestor — the deepest node that has both as descendants (a node can be its own ancestor).

**What it tests.** Exploiting the BST ordering — the LCA is the first node whose value sits between p and q — instead of a generic tree search.

**Approach & answer.** In a general binary tree LCA needs a full search, but a BST's ordering collapses it to a single root-to-leaf walk. Compare the current node's value with p and q: if both are smaller, the answer lies entirely in the left subtree; if both are larger, go right; the moment they split — one on each side, or one equals the current node — you are standing on the lowest common ancestor, because this is the first node from the top where p and q diverge. No extra space beyond the walk, and it is O(h): O(log n) balanced, O(n) worst. Recognition signal: 'lowest common ancestor in a BST' (or any 'first point where two ordered search paths diverge') → descend using the BST comparison and stop at the split point. For a plain binary tree instead, use the postorder 'found-in-left / found-in-right' recursion.

**Use this technique when.** LCA (or the split point of two search paths) in a BST → walk down comparing values, stop where the two targets fall on opposite sides of the current node.

**Complexity.** Time O(h) · Space O(1) iterative

```js
function lowestCommonAncestor(root, p, q) {
  let node = root;
  while (node) {
    if (p.val < node.val && q.val < node.val) node = node.left;
    else if (p.val > node.val && q.val > node.val) node = node.right;
    else return node;   // split point (or one equals node) = LCA
  }
  return null;
}
```

**References.** [LeetCode 235 · Lowest Common Ancestor of a BST](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/) · [Wikipedia · Lowest common ancestor](https://en.wikipedia.org/wiki/Lowest_common_ancestor)

---

### 55. Merge Sort & Quick Sort  `Medium`

**Pattern:** Divide & conquer (sorting)

**Problem.** Implement merge sort and quick sort from scratch, and be ready to explain their time/space trade-offs and when each is preferable.

**What it tests.** Fluency with the two archetypal divide-and-conquer sorts: stability, worst-case behaviour, in-place vs extra memory, and why library sorts pick one.

**Approach & answer.** Both are divide-and-conquer but split the work at opposite ends. Merge sort divides POSITIONALLY into halves, recursively sorts, then does the work in the MERGE — walk two sorted halves with two pointers into one sorted output. It is always O(n log n), stable, and easy to reason about, but needs O(n) auxiliary space (and is the basis of external and linked-list sorts). Quick sort divides by VALUE — pick a pivot, partition into < and > the pivot, recurse on each side — so the work is in the partition and the combine is free. It sorts in place with O(log n) stack, averages O(n log n), and is cache-friendly (why many array sorts use it), but a bad pivot gives O(n^2), fixed in practice by random or median-of-three pivots. Family recognition signal: 'sort, or a problem that becomes easy once split and recombined' — counting inversions → merge sort; kth element → quickselect (quick sort's partition without full recursion). Rule of thumb: need stability or a guaranteed bound → merge; need in-place and average speed → quick.

**Use this technique when.** Any divide-and-conquer over an array — sort, count inversions (merge), or select the kth element (quickselect). Choose merge for stability/guaranteed bound, quick for in-place average speed.

**Complexity.** Merge: O(n log n) time, O(n) space · Quick: O(n log n) avg / O(n²) worst, O(log n) space

```js
function mergeSort(a) {
  if (a.length <= 1) return a;
  const mid = a.length >> 1;
  const left = mergeSort(a.slice(0, mid));
  const right = mergeSort(a.slice(mid));
  const out = [];
  let i = 0, j = 0;
  while (i < left.length && j < right.length) {
    out.push(left[i] <= right[j] ? left[i++] : right[j++]);  // <= keeps it stable
  }
  while (i < left.length) out.push(left[i++]);
  while (j < right.length) out.push(right[j++]);
  return out;
}

function quickSort(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return a;
  const pivot = a[(lo + hi) >> 1];
  let i = lo, j = hi;
  while (i <= j) {
    while (a[i] < pivot) i++;
    while (a[j] > pivot) j--;
    if (i <= j) { [a[i], a[j]] = [a[j], a[i]]; i++; j--; }
  }
  quickSort(a, lo, j);
  quickSort(a, i, hi);
  return a;
}
```

**References.** [Wikipedia · Merge sort](https://en.wikipedia.org/wiki/Merge_sort) · [Wikipedia · Quicksort](https://en.wikipedia.org/wiki/Quicksort)

---

### 56. Sort Colors (Dutch National Flag)  `Medium`

**Pattern:** Three-pointer partition

**Problem.** Given an array of 0s, 1s, and 2s (red/white/blue), sort it in place in a single pass without a library sort or counting. e.g. [2,0,2,1,1,0] → [0,0,1,1,2,2].

**What it tests.** The three-pointer Dutch National Flag partition — one pass, O(1) space — versus the easy but two-pass counting-sort answer.

**Approach & answer.** The two-pass counting sort (tally 0/1/2, then overwrite) works, but the intended answer is Dijkstra's Dutch National Flag: partition into three regions in ONE pass with three pointers. Keep low (next slot for a 0), high (next slot for a 2), and a moving mid. When a[mid] is 0, swap it down to low and advance both low and mid; when it is 2, swap it up to high and shrink high only (do NOT advance mid, because the swapped-in value is still unexamined); when it is 1, just advance mid. Stop when mid passes high. The subtlety that trips people is exactly that 'don't advance mid after a swap-with-high'. Recognition signal: 'partition into three (or a few) categories around pivot values, in place, one pass' → three-pointer DNF. It generalises quicksort's partition to handle duplicate pivots (3-way quicksort). O(n) time, O(1) space.

**Use this technique when.** In-place one-pass partition into three ordered groups (or 3-way quicksort partitioning around equal keys) → low/mid/high three-pointer sweep.

**Complexity.** Time O(n) · Space O(1)

```js
function sortColors(nums) {
  let low = 0, mid = 0, high = nums.length - 1;
  while (mid <= high) {
    if (nums[mid] === 0) {
      [nums[low], nums[mid]] = [nums[mid], nums[low]];
      low++; mid++;
    } else if (nums[mid] === 2) {
      [nums[high], nums[mid]] = [nums[mid], nums[high]];
      high--;                        // don't advance mid: swapped-in value unseen
    } else {
      mid++;
    }
  }
  return nums;
}
```

**References.** [LeetCode 75 · Sort Colors](https://leetcode.com/problems/sort-colors/) · [Wikipedia · Dutch national flag problem](https://en.wikipedia.org/wiki/Dutch_national_flag_problem)

---

### 57. Minimum Window Substring  `Hard`

**Pattern:** Sliding Window

**Problem.** Given strings s and t, return the smallest substring of s that contains every character of t (with multiplicity). Return '' if none.

**What it tests.** The full expand-then-contract template with a 'how many chars still needed' counter.

**Approach & answer.** Count what t needs. Expand right, decrementing need when you cover a required char. Once all requirements are met (missing === 0), contract from the left to find the smallest valid window, recording the best. This 'grow to satisfy, shrink to minimize' shape is the general sliding-window template — every variable-window problem is a variation of it. The `missing` counter avoids re-scanning the need-map each step: it goes to 0 exactly when the window is valid. Note the asymmetry — you only shrink while valid, so left never overshoots, keeping it O(|s| + |t|).

**Use this technique when.** 'Smallest window containing all of X' — the canonical hard sliding-window; keep a requirement counter.

**Complexity.** Time O(|s| + |t|) · Space O(|t|)

```js
function minWindow(s, t) {
  if (!t || !s) return '';
  const need = new Map();
  for (const c of t) need.set(c, (need.get(c) || 0) + 1);
  let missing = t.length, left = 0, start = 0, min = Infinity;
  for (let right = 0; right < s.length; right++) {
    const c = s[right];
    if (need.get(c) > 0) missing--;
    need.set(c, (need.get(c) || 0) - 1);
    while (missing === 0) {                 // window is valid — shrink it
      if (right - left + 1 < min) { min = right - left + 1; start = left; }
      const lc = s[left];
      need.set(lc, need.get(lc) + 1);
      if (need.get(lc) > 0) missing++;
      left++;
    }
  }
  return min === Infinity ? '' : s.substr(start, min);
}
```

**References.** [LeetCode 76 · Minimum Window Substring](https://leetcode.com/problems/minimum-window-substring/)

---

### 58. Longest Increasing Subsequence  `Hard`

**Pattern:** Dynamic Programming

**Problem.** Return the length of the longest strictly increasing subsequence (not necessarily contiguous).

**What it tests.** Two solutions: the intuitive O(n²) DP, and the clever O(n log n) patience-sort with binary search.

**Approach & answer.** O(n²) DP: dp[i] = longest LIS ending at i = 1 + max(dp[j]) for j<i with nums[j]<nums[i]. The optimal O(n log n): maintain `tails`, where tails[k] is the smallest possible tail of an increasing subsequence of length k+1; binary-search each number's slot and replace/append. Length of tails is the answer. Mention both; code the elegant one. The trick to internalize: `tails` is not itself a valid subsequence — it's a set of best-case endings, kept sorted precisely so binary search is legal. Replacing an existing tail with a smaller value leaves more room for future extensions without changing the current best length. Use lower-bound (first tail ≥ x) for strictly-increasing; switch to upper-bound if the problem allows equal values (non-decreasing).

**Use this technique when.** Subsequence optimization; and when an O(n²) DP has a monotonic structure you can binary-search → patience sorting.

**Complexity.** Time O(n log n) · Space O(n)

```js
function lengthOfLIS(nums) {
  const tails = [];                 // tails[k] = smallest tail of LIS length k+1
  for (const x of nums) {
    let lo = 0, hi = tails.length;
    while (lo < hi) {               // binary search first tail >= x
      const mid = (lo + hi) >> 1;
      if (tails[mid] < x) lo = mid + 1;
      else hi = mid;
    }
    tails[lo] = x;                  // replace, or append if lo === length
  }
  return tails.length;
}
```

**References.** [LeetCode 300 · Longest Increasing Subsequence](https://leetcode.com/problems/longest-increasing-subsequence/) · [Wikipedia · Patience sorting](https://en.wikipedia.org/wiki/Patience_sorting)

---

### 59. Merge k Sorted Lists  `Hard`

**Pattern:** Heap / Top-K

**Problem.** Merge k sorted linked lists into one sorted list. e.g. [[1,4,5],[1,3,4],[2,6]] → 1,1,2,3,4,4,5,6.

**What it tests.** Using a min-heap to always know the smallest current head across k sources — the k-way merge.

**Approach & answer.** The naive 'concatenate then sort' is O(N log N) over all N nodes and ignores the sortedness. The heap answer: push the head of each of the k lists into a min-heap keyed by node value. Repeatedly pop the smallest, append it to the output, and push that node's `next` if it exists. The heap never holds more than k nodes, so each of the N pops/pushes costs O(log k) → O(N log k) total, strictly better than O(N log N) when k is small relative to N. This is the general k-way merge and the top-K family: 'pick the current best across many ordered sources' → a heap. An alternative with the same complexity is divide-and-conquer pairwise merging (merge lists two at a time, log k rounds), which needs no heap and is often what interviewers accept in JS where there's no built-in priority queue — you'd hand-roll a binary heap or the pairwise merge.

**Use this technique when.** Combine k already-sorted sources, or repeatedly need the current min/max across many streams → min-heap of the k heads.

**Complexity.** Time O(N log k) · Space O(k)

```js
// Divide-and-conquer pairwise merge (no built-in heap needed).
function mergeKLists(lists) {
  if (lists.length === 0) return null;
  while (lists.length > 1) {
    const merged = [];
    for (let i = 0; i < lists.length; i += 2) {
      merged.push(merge2(lists[i], lists[i + 1] || null));
    }
    lists = merged;
  }
  return lists[0];
}
function merge2(a, b) {
  const dummy = { next: null }; let tail = dummy;
  while (a && b) {
    if (a.val <= b.val) { tail.next = a; a = a.next; }
    else { tail.next = b; b = b.next; }
    tail = tail.next;
  }
  tail.next = a || b;
  return dummy.next;
}
```

**References.** [LeetCode 23 · Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/) · [Wikipedia · k-way merge algorithm](https://en.wikipedia.org/wiki/K-way_merge_algorithm)

---

### 60. Binary Tree Maximum Path Sum  `Hard`

**Pattern:** Tree DP (postorder)

**Problem.** A path is any sequence of nodes connected by edges (it need not pass through the root and can start/end anywhere). Return the maximum node-value sum over all paths. e.g. [-10,9,20,null,null,15,7] → 42 (15+20+7).

**What it tests.** Separating two different quantities in one postorder DFS: what a subtree can CONTRIBUTE upward (a single downward arm) versus the best path that can PEAK at a node (both arms).

**Approach & answer.** The hard part is that a node participates in the answer in two incompatible ways, and you must return one while recording the other. Do a postorder DFS. For a node, recursively get the best downward gain from each child, clamped at 0 (drop a negative arm). The best path that PEAKS at this node — allowed to bend through it using both children — is node.val + leftGain + rightGain; update a global maximum with it. But the value you RETURN to the parent can extend only one arm (a parent can't route through both of your children), so return node.val + max(leftGain, rightGain). Keeping these two computations distinct is the entire trick. Recognition signal: a tree problem where the global optimum can BEND at a node but the value propagated to the parent is a single path → postorder DFS returning the one-arm gain while side-updating a global best with the two-arm peak. O(n) time, O(h) stack.

**Use this technique when.** Tree optima where a node may join two subtree results for the answer but can pass only one upward → postorder DFS: return the best single arm, update a global with the both-arms combination.

**Complexity.** Time O(n) · Space O(h)

```js
function maxPathSum(root) {
  let best = -Infinity;
  function gain(node) {
    if (!node) return 0;
    const left = Math.max(gain(node.left), 0);        // drop negative arms
    const right = Math.max(gain(node.right), 0);
    best = Math.max(best, node.val + left + right);    // path peaking here (both arms)
    return node.val + Math.max(left, right);           // extend only one arm upward
  }
  gain(root);
  return best;
}
```

**References.** [LeetCode 124 · Binary Tree Maximum Path Sum](https://leetcode.com/problems/binary-tree-maximum-path-sum/) · [Wikipedia · Tree traversal](https://en.wikipedia.org/wiki/Tree_traversal)

---
