Home
⚛️
Segment 1 — JSX & Component Fundamentals
The building blocks of every React app. Understanding how JSX compiles, how components think about data, and how React's Virtual DOM works gives you the mental model to debug anything — from unexpected re-renders to mysterious key warnings.
JSX Components Props Virtual DOM Reconciliation Rendering
10
Questions
0
Opened
4
Topics
~50m
Study Time
🧠 Mental Model — React's Core Idea

React is built on one simple formula: UI = f(state). Your component is a pure function — given the same state and props, it always returns the same UI. React figures out the minimum DOM updates needed to get there.

The Old Way (Imperative)

You manually find DOM nodes and tell the browser how to change them. Error-prone — any code path can touch any element.

document.getElementById('count')
.textContent = count + 1;
The React Way (Declarative)

You describe what the UI should look like. React handles the DOM mutations. Your code only cares about state.

function Counter() {
return <p>{count}</p>;
}
🏗️ The Blueprint Analogy

Think of React components as blueprints, not buildings. When state changes, React creates a new blueprint and compares it to the previous one (diffing). Only the changed rooms get rebuilt in the actual DOM — React never tears down the whole house.

JSXSyntax extension that looks like HTML but compiles to React.createElement() calls.
ComponentA function (or class) that accepts props and returns React elements describing part of the UI.
PropsRead-only inputs passed from parent to child. The component's "arguments".
Virtual DOMA lightweight JS object tree React maintains as an in-memory mirror of the real DOM.
ReconciliationReact's process of diffing the new virtual DOM against the previous one to compute minimal DOM updates.
React FiberReact's internal reconciler engine (v16+). Enables incremental, interruptible rendering.
Topic A — JSX Fundamentals
1
What is JSX? How does Babel transform it under the hood?
Easy JSX

JSX is not HTML, not a template language, and not part of the JavaScript spec. It's a syntax extension that Babel (or the TypeScript compiler) transforms into plain JavaScript function calls before your browser ever sees it.

JSX → Compiled Output
// ── What you WRITE ──
const el = (
  <button className="btn" onClick={handleClick}>
    <span>Click me</span>
  </button>
);

// ── What Babel COMPILES it to (React 17+ new transform) ──
import { jsx as _jsx } from 'react/jsx-runtime';

const el = _jsx('button', {
  className: 'btn',
  onClick: handleClick,
  children: _jsx('span', { children: 'Click me' })
});

// ── Old transform (React <17, still works) ──
const el = React.createElement(
  'button',
  { className: 'btn', onClick: handleClick },
  React.createElement('span', null, 'Click me')
);

The result is a plain JavaScript object — a React element. It describes what to render, but it's not the DOM node itself.

React Element Object
{
  $$typeof: Symbol(react.element),  // XSS protection marker
  type:     'button',               // string for DOM, function for component
  key:      null,
  ref:      null,
  props:    { className: 'btn', children: ... }
}
Why React 17 changed the transform
Before React 17 you needed import React from 'react' in every JSX file — because Babel emitted React.createElement(). The new JSX transform auto-imports from react/jsx-runtime, so you no longer need that import just for JSX.
💬 Interview Tip

If asked "why do you need to import React?", explain the old vs new transform. If the interviewer mentions a project still requiring it, you know it's using React <17 or the old Babel config.

2
What are the JSX syntax rules every React developer must know?
Easy JSX
RuleWrongCorrectWhy
Single root<h1/><p/><><h1/><p/></>JSX compiles to one function call — one return value
Self-close tags<img><br><img /> <br />JSX is XML-strict — all tags must close
classNameclass="btn"className="btn"class is a reserved JS keyword
htmlForfor="email"htmlFor="email"for is a reserved JS keyword
camelCase attrsonclick tabindexonClick tabIndexJSX maps to JS object properties
Expressions in {}<p>Hello name</p><p>Hello {name}</p>Only {} creates a JS expression slot
JSX Expressions
// ✅ Valid inside { }
<p>{name}</p>                     // variable
<p>{2 + 2}</p>                   // expression
<p>{items.length}</p>             // property access
<p>{isLoggedIn ? 'Hi' : 'Login'}</p> // ternary
<p>{formatDate(date)}</p>          // function call

// ❌ NOT valid inside { } — these are statements, not expressions
<p>{if (x) ...}</p>              // if statement ❌
<p>{for (...) ...}</p>            // for loop ❌

// Comments inside JSX
<div>
  {/* This is a JSX comment */}
  <p>Content</p>
</div>
⚠ false, null, undefined don't render
{false}, {null}, {undefined}, and {0}* are valid JSX but render nothing. Watch out for {count && <Comp />} when count is 0 — it renders the literal 0, not nothing. Use {count > 0 && <Comp />} or a ternary instead.
3
What is the difference between a React element and a React component?
Easy Components
React Element

A plain JS object describing what should appear on screen. Immutable snapshot — like a frame in a movie.

  • Created by React.createElement() or JSX
  • Has type, props, key, ref
  • Cheap to create — no DOM involved
  • type is a string: "div", "button"
React Component

A function (or class) that returns elements. It's the blueprint; elements are the blueprint's output.

  • Accepts props, returns JSX/elements
  • Can hold state, run effects
  • Name must start with capital letter
  • type in the element is the function itself
JSX
// Lowercase → React treats it as a DOM string tag
<button>click</button>
// compiles to: React.createElement('button', ...)

// Uppercase → React treats it as a component reference
<Button>click</Button>
// compiles to: React.createElement(Button, ...)
// React calls Button() to get the elements it returns

// ⚠ This is why this BREAKS:
function myButton() { return <button />; }
const el = <myButton />;  // treated as unknown DOM tag, not a component!

// ✅ Fix: capital letter, OR assign to capital-letter variable
const MyButton = myButton;
const el = <MyButton />;  // ✅ now recognized as component
Mental Model
Component = recipe. Element = dish. Calling <MyCard /> asks React to "cook" MyCard and get back a description of the DOM nodes it wants. React then decides whether to create, update, or reuse real DOM nodes.
Topic B — Components & Props
4
Functional vs Class components — what are the real differences in 2024?
Medium Components
FeatureClass ComponentFunctional Component
Statethis.state / this.setStateuseState hook
LifecyclecomponentDidMount, componentDidUpdate, componentWillUnmountuseEffect (covers all three)
Side effectsScattered across lifecycle methodsCollocated in useEffect
Contextstatic contextType or <Context.Consumer>useContext hook
Code sharingHOCs / render props (verbose)Custom hooks (clean)
this bindingMust bind event handlers or use arrow methodsNo this — no binding bugs
ClosuresAlways reads from this — sees latest valueEach render closes over its own props/state snapshot
Error boundariesOnly class components can be error boundariesCannot be error boundaries (yet)
Stale Closure vs this
// CLASS: always reads the LATEST value via this
handleClick() {
  setTimeout(() => {
    alert(this.state.count); // shows current count at time of alert
  }, 3000);
}

// FUNCTIONAL: closes over the value at render time
function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setTimeout(() => {
      alert(count); // shows count at the time button was clicked, not 3s later
    }, 3000);
  };
}
When you'll still encounter class components
Legacy codebases, error boundaries (no functional equivalent yet), and getSnapshotBeforeUpdate. The React team has said they have no plans to remove class components, but all new React features (concurrent mode, hooks) are functional-only.
💬 Interview Tip

Don't just say "functional is better." Explain why: logic collocation with custom hooks, no this binding complexity, and the closure-per-render mental model that makes concurrent React safe.

5
What are props? Explain one-way data flow and the children prop.
Medium Props

Props (short for properties) are the mechanism for passing data from parent to child. They are read-only — a component must never modify its own props.

Props Patterns
// ── Passing props ──
<UserCard
  name="Yogesh"
  age={28}
  isAdmin={true}
  onClick={handleClick}
/>

// ── Receiving props (object destructure) ──
function UserCard({ name, age, isAdmin, onClick }) {
  return (
    <div onClick={onClick}>
      <h2>{name}</h2>
      {isAdmin && <span>Admin</span>}
    </div>
  );
}

// ── Default props via destructuring ──
function Button({ label = 'Submit', variant = 'primary' }) { ... }

// ── Spread props (use sparingly) ──
const props = { type: 'button', disabled: false };
<button {...props}>Go</button>

Anything placed between a component's opening and closing tags becomes props.children. It can be a string, an element, an array of elements, or even a function (render prop pattern).

children
// Parent uses children
<Card>
  <h3>Title</h3>
  <p>Body text</p>
</Card>

// Card component renders whatever is passed
function Card({ children }) {
  return <div className="card">{children}</div>;
}

// children can also be a function (render prop)
<DataProvider>
  {(data) => <Table rows={data} />}
</DataProvider>
🌊 One-Way Data Flow

Data flows down (parent → child via props) and events flow up (child → parent via callback props). Think of it like a waterfall — water only falls down. To tell a parent about a change, the child calls a callback the parent passed as a prop. This makes data flow predictable and debuggable.

6
What is component composition? How does it differ from inheritance?
Medium Components

The React docs explicitly recommend composition over inheritance. In 5+ years of building React at Meta, they found no use case where inheritance solved something composition couldn't solve better.

JSX
// ── Pattern 1: children (slot) ──
function Dialog({ title, children }) {
  return (
    <div className="dialog">
      <h1>{title}</h1>
      {children}
    </div>
  );
}
<Dialog title="Welcome">
  <p>Any content here</p>
</Dialog>

// ── Pattern 2: named slots (multiple prop slots) ──
function Layout({ sidebar, main, footer }) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <main>{main}</main>
      <footer>{footer}</footer>
    </div>
  );
}
<Layout
  sidebar={<Nav />}
  main={<Feed />}
  footer={<Footer />}
/>

// ── Pattern 3: specialization (compose general into specific) ──
function WelcomeDialog() {
  return (
    <Dialog title="Welcome!">
      <p>Thanks for visiting.</p>
    </Dialog>
  );
}
Why not inheritance?
Inheritance creates tight coupling between parent and child classes. Composition is more flexible: swap out any piece without touching the others. In React, passing JSX as props gives you the same "plug in any content" power without a class hierarchy.
Topic C — Rendering Patterns
7
What are all the patterns for conditional rendering in React? Trade-offs?
Medium Rendering
JSX
// ── 1. if/else (outside JSX) — best for complex conditions ──
function Page({ isLoading, data }) {
  if (isLoading) return <Spinner />;
  if (!data)    return <Empty />;
  return <DataView data={data} />;
}

// ── 2. Ternary — best for 2-branch inline choices ──
return (
  <div>
    {isLoggedIn ? <UserMenu /> : <LoginBtn />}
  </div>
);

// ── 3. Short-circuit && — best for "show or nothing" ──
return (
  <div>
    {hasError && <ErrorBanner msg={error} />}
    {items.length > 0 && <List items={items} />} // ✅ guard with > 0
  </div>
);

// ── 4. Return null — unmount / hide completely ──
function Tooltip({ visible, text }) {
  if (!visible) return null; // component is called but renders nothing
  return <div className="tooltip">{text}</div>;
}
PatternUse whenAvoid when
if/elseMultiple branches, early returns, complex logicSimple inline render
TernaryExactly 2 options inline in JSXNested ternaries — kills readability
&&Show-or-nothing, simple booleanLeft side is 0 or empty string
return nullComponent must not render anythingHiding via CSS is better (avoids remount cost)
⚠ null vs CSS display:none
return null fully unmounts the component — all state is destroyed. Using style={{ display: isVisible ? 'block' : 'none' }} keeps the component mounted. Choose based on whether you want state preserved.
8
Why do lists need keys? What makes a good key? What happens with bad keys?
Medium Rendering

When React re-renders a list, it needs to match the new list of elements against the previous one. Without keys, React uses array position — which breaks when items are inserted, deleted, or reordered. Keys give React a stable identity for each item.

🎒 The Classroom Analogy

Imagine 30 students sit in numbered seats. If you add a new student at the front and all seats shift, React (without keys) thinks every student changed. With keys (student ID cards), React knows exactly which students are new, moved, or removed — and only updates those seats.

JSX
// ✅ GOOD: stable, unique, from your data
{users.map(user => (
  <UserRow key={user.id} user={user} />
))}

// ⚠ ACCEPTABLE: index only if list is static, never reordered, never filtered
{staticTabs.map((tab, i) => (
  <Tab key={i} label={tab.label} />
))}

// ❌ BAD: random keys — new key every render = unnecessary remounts
{items.map(item => (
  <Item key={Math.random()} item={item} />
))}

// ❌ BAD: index on a mutable list
// Add item to front → every item's key shifts → all remount, input state lost
{todos.map((todo, i) => (
  <TodoItem key={i} /> // ❌ if todos can be prepended/deleted
))}
Bug Scenario
// Initial list: [{id:1, name:'Alice'}, {id:2, name:'Bob'}]
// Keys:  key=0 → Alice,  key=1 → Bob

// User types in Alice's input (local state inside TodoItem)
// We prepend {id:3, name:'Charlie'}

// New keys: key=0 → Charlie, key=1 → Alice, key=2 → Bob
// React reuses the key=0 DOM node — input now shows Alice's typed text
// but the label says "Charlie" 💥
Keys are not passed as props
key is a special React attribute — it's not passed as a prop to the component. If you need the id inside the component, pass it as a separate prop: <Item key={item.id} id={item.id} />.
Topic D — Virtual DOM & Advanced JSX
9
What is the Virtual DOM? How does React's reconciliation algorithm work?
Advanced Virtual DOM

The Virtual DOM is a lightweight JavaScript object tree that mirrors the structure of the real DOM. When state changes, React creates a new VDOM tree and compares it to the previous one — this comparison is called diffing. Only the actual differences get applied to the real DOM.

📄 The Document Edit Analogy

Imagine a word processor that keeps a copy of the "before" document and a "new" document. Instead of retyping the whole thing, it uses track-changes to find only the edits and applies them. React does the same — produce a new description, diff it, patch the DOM surgically.

1
Different type → destroy and rebuild. If a <div> becomes a <span>, React tears down the entire subtree and builds fresh. This is why conditionally swapping component types is expensive.
2
Same type → update in place. If the type is the same, React updates only the changed props/attributes. State is preserved because the component instance is reused.
3
Keys for lists. React uses key to match children across renders. Without keys it falls back to position — which breaks on reorder/insert.

React 16 replaced the old recursive reconciler with Fiber. Key benefits:

  • Incremental rendering: work can be split into chunks and spread across multiple frames
  • Prioritization: urgent updates (user input) can interrupt low-priority updates (data fetching renders)
  • Concurrent mode: enables React 18 features like startTransition, Suspense with data, and streaming SSR
⚠ "Virtual DOM is faster" is a myth
The Virtual DOM is not faster than direct DOM manipulation. It's a consistent, safe abstraction that avoids accidental full re-renders. Hand-optimised vanilla JS will always be faster than React for small tasks. React's value is in developer experience and correctness at scale.
💬 Interview Tip

Mention Fiber to show depth. Interviewers love hearing the distinction between the old stack reconciler (synchronous, all-or-nothing) and Fiber (incremental, prioritised). Connect it to why concurrent React features are possible.

10
Explain React.Fragment, Portals, and StrictMode — when and why?
Advanced Advanced JSX

JSX requires a single root. Wrapping everything in a <div> adds unnecessary DOM nodes that can break CSS layouts (flex, grid, table). Fragment groups children without adding a DOM node.

JSX
// Full syntax — supports key prop (needed in lists)
{items.map(item => (
  <React.Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.def}</dd>
  </React.Fragment>
))}

// Shorthand syntax — no key support, but cleaner
function Columns() {
  return (
    <>
      <td>Name</td>
      <td>Age</td>
    </>
  );
}

A Portal renders a component's output into a different DOM node than where the component lives in the React tree. Crucial for modals, tooltips, and dropdowns that need to visually escape overflow:hidden or z-index stacking contexts.

JSX
import { createPortal } from 'react-dom';

function Modal({ children }) {
  return createPortal(
    <div className="modal-overlay">{children}</div>,
    document.getElementById('modal-root') // DOM node outside React root
  );
}

// Portal still behaves like a normal React child:
// - events bubble through React tree (not DOM tree)
// - context works as normal
// - unmounts when parent unmounts

<React.StrictMode> is a development-only tool. It double-invokes render functions, state initialisers, and reducers to surface side effects hidden in them. It also warns about deprecated APIs.

JSX
// Wrap your entire app (or part of it)
createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// What StrictMode does in development:
// ✅ Renders components twice — surfaces impure renders
// ✅ Runs effects twice (mount → unmount → remount) — surfaces missing cleanup
// ✅ Warns about deprecated findDOMNode, legacy context API, string refs
// ✅ No effect in production builds
Why StrictMode double-renders in React 18
React 18's concurrent mode can unmount and remount components to restore UI state (e.g. back-forward navigation). StrictMode simulates this in dev to ensure your effects clean up properly. If your component breaks when remounted, it will also break in production with concurrent features.
🔄
Segment 2 — State & Lifecycle
State is what makes React components alive. Understanding how useState schedules re-renders, how useEffect models side effects, and how React 18 changed batching is the foundation for writing correct, bug-free components.
useState useEffect Lifecycle Batching Cleanup Derived State
10
Questions
0
Opened
4
Topics
~55m
Study Time
🧠 Mental Model — State as a Snapshot

Each render is a snapshot in time. When you call setState, React doesn't mutate the current state — it schedules a new render with new state. The current render's variables are frozen for that render's lifetime.

📸 The Photography Analogy

Every render is like taking a photograph. The photo captures exactly what the scene looked like at that moment — you can't change it. State is the scene. Calling setState rearranges the scene and takes a new photo. Event handlers inside a render always see the values from their photo, not a later one.

StateData that, when changed, causes React to re-render the component and produce a new snapshot.
Re-renderReact calling the component function again to produce a new element tree from the new state.
Side effectAnything that reaches outside the render — fetching data, setting timers, subscriptions, manual DOM mutations.
BatchingReact grouping multiple setState calls into a single re-render for performance.
Stale closureAn event handler or effect that captured an old value of state/props and still uses it after it has changed.
CleanupThe function returned from useEffect that runs before the next effect or when the component unmounts.
Topic A — useState
1
How does useState work internally? Why can't you read the new state immediately after calling the setter?
Easy useState
JSX
const [count, setCount] = useState(0);
//     ↑ current value   ↑ setter function   ↑ initial value (only used on first render)

// The setter does NOT mutate count — it schedules a new render
function handleClick() {
  setCount(5);
  console.log(count); // still 0 — this render's snapshot is frozen
}

// ✅ To use the new value, use the updater function form
setCount(prev => prev + 1); // always based on latest queued state

// ── Why updater form matters ──
function increment3Times() {
  setCount(count + 1); // ❌ all 3 calls read the same stale count
  setCount(count + 1); // ❌ result: count + 1, not count + 3
  setCount(count + 1); // ❌

  setCount(c => c + 1); // ✅ updater queued: prev → prev+1
  setCount(c => c + 1); // ✅ chains on previous result
  setCount(c => c + 1); // ✅ result: count + 3
}

React maintains a list of state slots per component instance (fiber node). Hooks are called in the same order every render, so slot 0 always corresponds to the first useState, slot 1 to the second, etc. This is why the Rules of Hooks exist — calling hooks conditionally would shift the slots.

Same value = no re-render
If you call setState with the same value as current state (by Object.is comparison), React bails out — no re-render, no children re-render. This is why mutating state directly doesn't work: obj.x = 1; setState(obj) — same reference, React skips the render.
2
What is state batching? How did React 18 change it?
Easy Batching

Batching is React grouping multiple setState calls from the same event handler into a single re-render. Without it, three setStates would trigger three separate renders — expensive and potentially showing intermediate states.

Batching Behaviour
// ── React 17 and earlier ──
// Batched ONLY inside React event handlers:
handleClick() {
  setCount(c => c + 1);  // } batched → 1 render
  setFlag(true);         // }
}

// NOT batched — setTimeout, Promises, native event listeners in React 17:
setTimeout(() => {
  setCount(c => c + 1);  // render 1
  setFlag(true);         // render 2  ← two separate renders!
}, 1000);

// ── React 18 — Automatic Batching everywhere ──
setTimeout(() => {
  setCount(c => c + 1);  // } batched → 1 render ✅
  setFlag(true);         // }
}, 1000);

// Opt out of batching in React 18 (rare):
import { flushSync } from 'react-dom';

flushSync(() => setCount(c => c + 1)); // forces render immediately
flushSync(() => setFlag(true));          // then another render
💬 Interview Tip

React 18 automatic batching is a common question. Key point: it's enabled by default when using createRoot (not legacy ReactDOM.render). Mention flushSync as the escape hatch when you need synchronous DOM updates (e.g., before reading a layout measurement).

3
How do you correctly update objects and arrays in state?
Easy useState

React uses reference equality (Object.is) to detect state changes. If you mutate an object and pass the same reference, React sees no change and skips the render. You must always create a new object/array.

Immutable Updates
// ── Objects ──
const [user, setUser] = useState({ name: 'Yogesh', age: 28 });

// ❌ Mutating — React won't re-render
user.age = 29;
setUser(user); // same reference!

// ✅ Spread to create new object
setUser({ ...user, age: 29 });

// ── Arrays ──
const [items, setItems] = useState(['a', 'b', 'c']);

// ❌ Mutating methods — avoid in state updates
items.push('d');   // mutates!
items.splice(1,1); // mutates!

// ✅ Non-mutating equivalents
setItems([...items, 'd']);               // add
setItems(items.filter(i => i !== 'b'));   // remove
setItems(items.map(i => i === 'b' ? 'B' : i)); // update
setItems([...items].sort());             // sort (spread first!)

// ── Nested objects — spread each level ──
const [profile, setProfile] = useState({
  name: 'Yogesh',
  address: { city: 'Mumbai', pin: '400001' }
});

setProfile({
  ...profile,
  address: { ...profile.address, city: 'Pune' }
});
Immer for deep updates
For deeply nested state, manual spreading gets verbose. The Immer library (used inside Redux Toolkit) lets you write mutating syntax that produces an immutable update under the hood: produce(state, draft => { draft.a.b.c = 1 }).
Topic B — useEffect
4
Explain useEffect and its dependency array — all three forms.
Medium useEffect
useEffect
// ── Form 1: No dependency array — runs after EVERY render ──
useEffect(() => {
  console.log('Runs after every render');
});
// Use: rare. Logging, measurements that must run on every update.

// ── Form 2: Empty array [] — runs once on mount only ──
useEffect(() => {
  fetchData();
  const sub = subscribe();
  return () => sub.unsubscribe(); // cleanup on unmount
}, []);
// Use: initial data fetch, setting up subscriptions, one-time setup.

// ── Form 3: Dependency array — runs when deps change ──
useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]); // only re-runs when count changes
// Use: syncing state to an external system, reacting to prop/state changes.
1
React renders the component (calls the function, produces JSX)
2
React commits the changes to the real DOM
3
The browser paints the screen
4
useEffect fires — after paint, asynchronously. This is why it doesn't block the browser.
useLayoutEffect
Fires synchronously before the browser paints — same phase as componentDidMount. Use only when you need to read/write DOM layout before the user sees the paint (measuring element sizes, avoiding flicker). Overuse blocks painting.
Dep Comparison
// ⚠ Object/array deps created inline re-trigger every render!
useEffect(() => {
  fetchUser(options);
}, [{ id: userId }]); // ❌ new object every render → infinite loop risk

// ✅ Use primitive values, or useMemo/useCallback to stabilise
useEffect(() => {
  fetchUser({ id: userId });
}, [userId]); // ✅ primitive dep
5
What is the useEffect cleanup function? When is it called? Give real examples.
Medium Cleanup

The cleanup function returned from useEffect runs in two situations:

  1. Before the next effect fires — when deps change, React cleans up the previous effect, then runs the new one
  2. When the component unmounts

Think of it as: setup → (deps change) → cleanup → setup → (unmount) → cleanup

useEffect Cleanup
// ── 1. AbortController — cancel in-flight fetch ──
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/user/${userId}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(e => { if (e.name !== 'AbortError') setError(e); });
  return () => controller.abort(); // cancel if userId changes or unmounts
}, [userId]);

// ── 2. Event listener ──
useEffect(() => {
  const handleResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

// ── 3. setInterval ──
useEffect(() => {
  const id = setInterval(() => setTime(Date.now()), 1000);
  return () => clearInterval(id);
}, []);

// ── 4. WebSocket / subscription ──
useEffect(() => {
  const ws = new WebSocket(`ws://chat/${roomId}`);
  ws.onmessage = e => setMessages(m => [...m, e.data]);
  return () => ws.close();
}, [roomId]);
⚠ StrictMode double-fires effects
In React 18 StrictMode (development only), effects run mount → cleanup → mount. This deliberately exposes missing cleanups. If your effect breaks on the second mount, you have a cleanup bug — fix the cleanup, don't remove StrictMode.
6
What are the most common useEffect bugs — infinite loops, stale closures, missing deps?
Medium useEffect
Infinite Loop
// ❌ setState inside effect with no deps (or state in deps)
useEffect(() => {
  setData(fetchSomething()); // setData → re-render → effect runs → setData → ∞
}); // no dep array!

// ❌ Object dep created inside render
const options = { page: 1 }; // new reference every render
useEffect(() => { fetch(options); }, [options]); // always "changed" → loop

// ✅ Fix: stabilise or use primitives
const page = 1;
useEffect(() => { fetch({ page }); }, [page]); // primitive dep
Stale Closure
// ❌ count is frozen at 0 in this closure
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1); // always reads count=0, increments to 1 forever
  }, 1000);
  return () => clearInterval(id);
}, []); // empty deps — effect never re-runs, count never updates

// ✅ Fix 1: use updater function (no need to read count)
setCount(c => c + 1); // always gets latest value from React

// ✅ Fix 2: add count to deps (re-creates interval on each change)
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, [count]);
Missing Deps
// ❌ Suppressing the ESLint warning hides real bugs
useEffect(() => {
  doSomething(value);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // value is stale — this is a bug, not a fix

// ✅ Proper fix options:
// 1. Add missing dep
}, [value]);

// 2. Move the dep inside the effect (so it's not a dependency)
useEffect(() => {
  const latest = getLatestValue(); // read inside effect, not from closure
  doSomething(latest);
}, []);

// 3. Use useRef to store a mutable value without triggering re-runs
const valueRef = useRef(value);
useEffect(() => { valueRef.current = value; }); // keep ref in sync
useEffect(() => {
  doSomething(valueRef.current); // reads latest without being a dep
}, []);
Topic C — Class Lifecycle
7
What are the three phases of a class component's lifecycle?
Medium Lifecycle
PhaseMethodWhen it runsCommon use
MountingconstructorBefore first renderInitialise state, bind methods
renderProduces virtual DOMReturn JSX — must be pure
componentDidMountAfter first real DOM paintFetch data, subscribe, set up timers
UpdatingshouldComponentUpdateBefore re-renderPerformance — return false to skip
renderNew virtual DOM
getSnapshotBeforeUpdateBefore DOM is updatedCapture scroll position before change
componentDidUpdateAfter DOM updatedRespond to prop/state changes
UnmountingcomponentWillUnmountBefore component removedCancel timers, unsubscribe, abort fetches
componentDidUpdate
// ⚠ Always guard with a condition — or you'll get an infinite loop
componentDidUpdate(prevProps) {
  if (this.props.userId !== prevProps.userId) {
    this.fetchUser(this.props.userId);
  }
}
// Without the guard: setState → componentDidUpdate → setState → ∞
8
How do you map class lifecycle methods to useEffect equivalents?
Medium Lifecycle
Class methodHook equivalent
constructor (initial state)useState(initialValue)
componentDidMountuseEffect(() => { ... }, [])
componentDidUpdate on specific depuseEffect(() => { ... }, [dep])
componentWillUnmountuseEffect(() => { return () => cleanup() }, [])
shouldComponentUpdateReact.memo + useMemo / useCallback
getDerivedStateFromPropsCalculate during render (derived state pattern)
getSnapshotBeforeUpdateNo direct equivalent — use useLayoutEffect with a ref
Class vs Hooks
// ── Class component ──
componentDidMount() {
  this.fetch(this.props.id);
}
componentDidUpdate(prev) {
  if (prev.id !== this.props.id) this.fetch(this.props.id);
}
componentWillUnmount() {
  this.controller.abort();
}

// ── Functional component — same logic, less code ──
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/${id}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setData);
  return () => controller.abort();
}, [id]); // runs on mount AND whenever id changes — covers both CDM and CDU
Topic D — Advanced State Patterns
9
What is the useState initializer function and lazy initialization? When do you need it?
Advanced useState
Lazy Initializer
// ❌ Expensive computation runs on EVERY render (result is discarded after mount)
const [state, setState] = useState(parseHeavyJSON(rawData)); // called every render!
const [items, setItems] = useState(JSON.parse(localStorage.getItem('cart') || '[]'));

// ✅ Pass a FUNCTION — React calls it only on first render
const [state, setState] = useState(() => parseHeavyJSON(rawData));
const [items, setItems] = useState(() => {
  const saved = localStorage.getItem('cart');
  return saved ? JSON.parse(saved) : [];
});

// Same pattern for useReducer
const [state, dispatch] = useReducer(reducer, undefined, initFromStorage);
// initFromStorage() called once; result becomes initial state
Rule of thumb
If your initial state comes from localStorage, parsing a string, heavy computation, or anything that takes non-trivial time — always use the function form. The function is the contract: "compute this once."
10
What is "derived state"? Why is syncing state with useEffect an anti-pattern?
Advanced Derived State
Anti-pattern
// ❌ Anti-pattern: syncing prop → state via useEffect
function SearchBox({ query }) {
  const [value, setValue] = useState(query);

  useEffect(() => {
    setValue(query); // "sync" query to value
  }, [query]);
  // Problem: one render with stale value, then another with correct value → flash
  // Also: loses user's in-progress typing whenever parent re-renders
}
Derived State Pattern
// ✅ If value is always derived from query — no state needed at all
function SearchBox({ query }) {
  const upperQuery = query.toUpperCase(); // computed during render — always in sync
  return <p>{upperQuery}</p>;
}

// ✅ Expensive derived value — useMemo (not useEffect)
function FilteredList({ items, filter }) {
  const filtered = useMemo(
    () => items.filter(i => i.includes(filter)),
    [items, filter]
  );
  return <List items={filtered} />;
}

// ✅ When you DO need editable state seeded from a prop:
// Use the lazy initializer — initialise once, user can then edit independently
function EditableField({ initialValue }) {
  const [value, setValue] = useState(initialValue); // seed once, then own it
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}
⚠ The golden rule
If you can compute it from existing props or state during render — don't put it in state and don't use useEffect to sync it. useEffect is for synchronising React with external systems (APIs, DOM, timers). Using it to sync two pieces of React state is almost always wrong.
💬 Interview Tip

This is one of the most common React mistakes at all levels. Saying "I derive it during render instead of syncing it in useEffect" signals that you understand React's rendering model deeply. Bonus: mention the React docs article "You Might Not Need an Effect."

📝
Segment 3 — Events & Forms
Forms are where React's declarative model meets real user input. This segment covers how React's synthetic event system works, the controlled vs uncontrolled decision, every input type pattern, validation, debouncing, and advanced techniques like useRef-driven forms and performance-safe event handlers.
Synthetic Events Controlled Inputs Form Patterns Validation Debouncing useRef Forms
10
Questions
0
Opened
4
Topics
~50m
Study Time
🧠 Mental Model — React Owns the Form

In plain HTML, the DOM owns form state — the <input> element tracks what the user typed. React flips this: your component's state owns the value, and the input just displays it. Every keystroke fires an event → you update state → React re-renders → input shows new value. You are always in control.

Controlled Input

Value driven by React state. You always know what's in the field. Good for: validation as you type, disabling submit, dependent fields.

<input value={val}
  onChange={e => setVal(e.target.value)} />
Uncontrolled Input

DOM owns the value. You read it only when needed (on submit). Good for: file inputs, third-party widgets, performance-sensitive large forms.

const ref = useRef();
<input ref={ref} />
// read: ref.current.value
SyntheticEventReact's cross-browser wrapper around the native DOM event. Same API everywhere.
Event delegationReact attaches one listener at the root instead of one per element — efficient and automatic.
Controlled componentInput whose value is driven by React state via the value prop.
DebounceDelay running a function until the user stops triggering it — avoids firing on every keystroke.
ValidationChecking input correctness — can be real-time (onChange), on-blur, or on-submit.
e.preventDefault()Stops the browser's default action — essential on form submit to prevent page reload.
Topic A — Synthetic Events
1
What are React's SyntheticEvents? How do they differ from native DOM events?
Easy Synthetic Events

React wraps every native browser event in a SyntheticEvent — a cross-browser normalisation layer. The wrapper exposes the same interface as the native event (target, currentTarget, preventDefault(), stopPropagation()), but behaves consistently across all browsers.

JSX
function Form() {
  const handleSubmit = (e) => {
    e.preventDefault();         // stops browser page reload
    console.log(e.type);        // "submit"
    console.log(e.target);       // the <form> DOM node
    console.log(e.nativeEvent);  // the real browser event
  };

  const handleChange = (e) => {
    console.log(e.target.value);  // typed value
    console.log(e.target.name);   // input name attribute
    console.log(e.target.checked); // for checkboxes
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" onChange={handleChange} />
    </form>
  );
}
AspectNative DOMReact SyntheticEvent
Namingonclick (lowercase)onClick (camelCase)
Handler valueString: "handleClick()"Function reference: {handleClick}
Default preventionreturn false worksMust call e.preventDefault()
ConsistencyBrowser-specific quirksNormalised — same in all browsers
Pooling (React ≤16)N/AEvent was reused — e.persist() was needed. Removed in React 17
Event pooling is gone (React 17+)
In React ≤16, SyntheticEvent objects were pooled and reused — accessing e.target.value inside a setTimeout would give null. You needed e.persist(). React 17 removed pooling entirely — you can safely read event properties asynchronously.
2
How does React handle event delegation? How does bubbling and stopPropagation work in React?
Easy Event Delegation

React does not attach event listeners to individual DOM nodes. Instead, it attaches one listener per event type at the root container (document in React ≤16, the root div in React 17+). When an event fires anywhere in the tree, it bubbles up to that root listener, and React figures out which handlers to call.

🏢 The Receptionist Analogy

Instead of every room in a building having its own phone line, there's one receptionist at the front desk. All calls go to the receptionist first, who then routes them to the right room. React is the receptionist — one listener, routes events to the right component.

Why React 17 moved delegation from document to root
Attaching to document broke when multiple React versions ran on the same page (micro-frontends). Moving delegation to the root container means each React tree manages its own events independently.
JSX
// Bubbling — inner click fires child handler, then parent handler
<div onClick={() => console.log('parent')}>
  <button onClick={() => console.log('child')}>Click</button>
</div>
// Logs: "child" → "parent"

// Stop bubbling — prevent parent from firing
<button onClick={(e) => {
  e.stopPropagation();
  handleDelete();
}}>Delete</button>

// Capture phase — fires top-down, before bubbling
<div onClickCapture={() => console.log('captured first')}>
  <button onClick={() => console.log('bubble')}>Click</button>
</div>
// Logs: "captured first" → "bubble"

// Common pattern: close dropdown when clicking outside
useEffect(() => {
  const close = (e) => {
    if (!ref.current.contains(e.target)) setOpen(false);
  };
  document.addEventListener('mousedown', close);
  return () => document.removeEventListener('mousedown', close);
}, []);
3
How do you pass arguments to event handlers without causing unnecessary re-renders?
Easy Event Handlers
JSX
// ── Pattern 1: Inline arrow function (simplest) ──
<button onClick={() => deleteUser(user.id)}>Delete</button>
// ⚠ Creates a new function reference every render — can break React.memo

// ── Pattern 2: data attribute (avoids new function per item) ──
<button
  data-id={user.id}
  onClick={handleDelete}  // same reference every render
>Delete</button>

const handleDelete = (e) => {
  const id = e.currentTarget.dataset.id;
  deleteUser(id);
};

// ── Pattern 3: useCallback for memoised handler ──
const handleDelete = useCallback((id) => {
  deleteUser(id);
}, []); // stable reference

{users.map(u => (
  <UserRow
    key={u.id}
    user={u}
    onDelete={handleDelete}  // stable — UserRow wrapped in React.memo won't re-render
  />
))}

// ── Pattern 4: curried handler factory ──
const handleDelete = (id) => (e) => {
  e.stopPropagation();
  deleteUser(id);
};
<button onClick={handleDelete(user.id)}>Delete</button>
⚠ When inline arrows actually matter
Inline () => fn(arg) creates a new function on every render. This only causes real performance issues when the child is wrapped in React.memo — otherwise React would re-render it anyway. Don't prematurely optimise; only reach for useCallback when you've profiled a real problem.
Topic B — Controlled vs Uncontrolled
4
Controlled vs Uncontrolled components — deep dive with real trade-offs.
Medium Controlled
Controlled Input
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}            // React controls the value
        onChange={e => setEmail(e.target.value)}
      />

      // ✅ Powered by: validate on every keystroke
      // ✅ Powered by: disable submit until valid
      <button disabled={!email || !password}>Login</button>
    </form>
  );
}

// ⚠ Gotcha: value without onChange = read-only input (React warns)
<input value="fixed" />           // read-only — user can't type
<input defaultValue="initial" />  // uncontrolled with initial value
Uncontrolled Input
function UploadForm() {
  const fileRef = useRef(null);
  const nameRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const name = nameRef.current.value;  // read on submit
    const file = fileRef.current.files[0]; // file input MUST be uncontrolled
    upload({ name, file });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} defaultValue="" />
      <input type="file" ref={fileRef} />  // file inputs CANNOT be controlled
      <button>Upload</button>
    </form>
  );
}
NeedUse
Validate on every keystrokeControlled
Disable submit button conditionallyControlled
Dynamic/dependent fieldsControlled
File uploadUncontrolled (always)
Simple submit-only form with no validationUncontrolled (simpler)
Integrating a third-party rich text editorUncontrolled + ref
Large form with 50+ fields (performance)Uncontrolled or React Hook Form
5
How do you handle every input type — text, checkbox, radio, select, textarea?
Medium Input Types
All Input Types
function AllInputs() {
  const [form, setForm] = useState({
    name:     '',
    agree:    false,
    gender:   '',
    country:  'in',
    bio:      '',
    skills:   [],     // multiple checkboxes
  });

  // Universal handler — reads name + type to handle all inputs
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setForm(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };

  // Multi-checkbox handler (array of selected values)
  const handleSkill = (e) => {
    const { value, checked } = e.target;
    setForm(prev => ({
      ...prev,
      skills: checked
        ? [...prev.skills, value]
        : prev.skills.filter(s => s !== value)
    }));
  };

  return (
    <form>
      {/* Text input */}
      <input type="text" name="name"
        value={form.name} onChange={handleChange} />

      {/* Single checkbox — reads checked, not value */}
      <input type="checkbox" name="agree"
        checked={form.agree} onChange={handleChange} />

      {/* Radio buttons — each has same name, different value */}
      <input type="radio" name="gender" value="male"
        checked={form.gender === 'male'} onChange={handleChange} />
      <input type="radio" name="gender" value="female"
        checked={form.gender === 'female'} onChange={handleChange} />

      {/* Select — uses value prop, not selected attribute */}
      <select name="country" value={form.country} onChange={handleChange}>
        <option value="in">India</option>
        <option value="us">USA</option>
      </select>

      {/* Textarea — uses value prop, NOT children */}
      <textarea name="bio"
        value={form.bio} onChange={handleChange} />

      {/* Multi-checkbox (array state) */}
      {['React', 'Node', 'CSS'].map(skill => (
        <label key={skill}>
          <input type="checkbox" value={skill}
            checked={form.skills.includes(skill)}
            onChange={handleSkill} />
          {skill}
        </label>
      ))}
    </form>
  );
}
⚠ The 3 gotchas
  • Checkbox: use checked + read e.target.checked, not value
  • Textarea: in HTML you set content as children; in React use the value prop
  • Select: in HTML you add selected to an option; in React put value on the <select> itself
6
How do you implement form validation in React — real-time, on-blur, and on-submit?
Medium Validation
Form Validation
function SignupForm() {
  const [fields, setFields] = useState({ email: '', password: '' });
  const [errors, setErrors]   = useState({});
  const [touched, setTouched] = useState({});

  // ── Validate function (pure) ──
  const validate = ({ email, password }) => {
    const e = {};
    if (!email)                          e.email    = 'Required';
    else if (!email.includes('@'))       e.email    = 'Invalid email';
    if (!password)                       e.password = 'Required';
    else if (password.length < 8)      e.password = 'Min 8 chars';
    return e;
  };

  // ── Strategy 1: Real-time (onChange) ──
  const handleChange = (e) => {
    const next = { ...fields, [e.target.name]: e.target.value };
    setFields(next);
    setErrors(validate(next)); // validate every keystroke
  };

  // ── Strategy 2: On-blur (show error only after user leaves the field) ──
  const handleBlur = (e) => {
    setTouched(t => ({ ...t, [e.target.name]: true }));
    setErrors(validate(fields));
  };

  // ── Strategy 3: On-submit only ──
  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate(fields);
    if (Object.keys(errs).length) {
      setErrors(errs);
      setTouched({ email: true, password: true }); // show all errors
      return;
    }
    submitToApi(fields);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" value={fields.email}
        onChange={handleChange} onBlur={handleBlur} />
      {/* Show error only after field was touched */}
      {touched.email && errors.email &&
        <p className="error">{errors.email}</p>}
      <button disabled={Object.keys(validate(fields)).length > 0}>
        Submit
      </button>
    </form>
  );
}
💬 Interview Tip

The touched pattern is the professional approach — errors appear only after the user has interacted with a field, not before they've even tried. Always mention the onBlur + touched combination. For production apps, mention React Hook Form or Zod for schema-based validation.

Topic C — Form Patterns
7
What is debouncing in React? How do you implement it for a search input?
Medium Debouncing

When a user types "React interview", that's 16 keystrokes. Without debouncing, you'd fire 16 API requests. Debouncing waits until the user stops typing for N milliseconds before running the expensive operation.

🔔 The Elevator Analogy

A debounced elevator doesn't close its doors the moment someone steps in. It waits a few seconds to see if anyone else is coming. Only after no new arrivals for a set time does it close and move. Each new arrival resets the timer.

Debounce Patterns
// ── Approach 1: useEffect + setTimeout (no library) ──
function SearchBox() {
  const [input, setInput]     = useState(''); // updates on every keystroke
  const [query, setQuery]     = useState(''); // debounced value sent to API

  useEffect(() => {
    const timer = setTimeout(() => setQuery(input), 400);
    return () => clearTimeout(timer); // cancel if input changes before 400ms
  }, [input]);

  useEffect(() => {
    if (query) searchAPI(query); // only fires when user stops typing
  }, [query]);

  return <input value={input} onChange={e => setInput(e.target.value)} />;
}

// ── Approach 2: Custom useDebounce hook (reusable) ──
function useDebounce(value, delay = 400) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// Usage:
const debouncedSearch = useDebounce(input, 400);
useEffect(() => {
  if (debouncedSearch) searchAPI(debouncedSearch);
}, [debouncedSearch]);

// ── Approach 3: useRef to hold stable debounced function ──
const timerRef = useRef();
const handleChange = (e) => {
  clearTimeout(timerRef.current);
  timerRef.current = setTimeout(() => searchAPI(e.target.value), 400);
};
Debounce vs Throttle
Debounce: wait until activity stops — good for search, autocomplete, resize handlers. Throttle: run at most once per interval regardless — good for scroll handlers, real-time cursors. Both are in lodash: _.debounce / _.throttle.
8
How do you manage a multi-field form with a single state object? What about reset?
Medium Form Patterns
Multi-field Form
const INITIAL = { name: '', email: '', role: 'user', active: true };

function UserForm({ onSave }) {
  const [form, setForm]     = useState(INITIAL);
  const [loading, setLoading] = useState(false);
  const [error, setError]     = useState(null);

  // Universal change handler using computed property name
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setForm(f => ({
      ...f,
      [name]: type === 'checkbox' ? checked : value
    }));
  };

  // Programmatic field update (useful for dependent fields)
  const setField = (key, val) =>
    setForm(f => ({ ...f, [key]: val }));

  // Reset to initial state
  const handleReset = () => setForm(INITIAL);

  // Submit with async + error handling
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    try {
      await onSave(form);
      handleReset(); // clear form on success
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name"  value={form.name}  onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      {error && <p className="error">{error}</p>}
      <button type="submit" disabled={loading}>
        {loading ? 'Saving...' : 'Save'}
      </button>
      <button type="button" onClick={handleReset}>Reset</button>
    </form>
  );
}
Topic D — Advanced Form Techniques
9
How does useRef work for DOM access? Explain forwardRef for exposing input refs to parents.
Advanced useRef / forwardRef

useRef returns { current: value }. Unlike state, changing .current does not trigger a re-render. It has two main uses: holding a reference to a DOM node, and holding a mutable value that should persist across renders without causing re-renders.

useRef DOM Access
function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // DOM method — runs after mount
  }, []);

  const handleClear = () => {
    inputRef.current.value = ''; // direct DOM mutation (uncontrolled)
    inputRef.current.focus();
  };

  return <input ref={inputRef} placeholder="type here" />;
}

// useRef for mutable value (no re-render on change)
function Stopwatch() {
  const [time, setTime]     = useState(0);
  const intervalRef         = useRef(null); // holds timer id — not state

  const start = () => {
    intervalRef.current = setInterval(
      () => setTime(t => t + 1), 1000
    );
  };
  const stop = () => clearInterval(intervalRef.current);
}

By default, ref is not passed as a prop — React intercepts it. forwardRef lets a component forward the ref it receives to one of its internal DOM nodes, enabling the parent to directly control a child's input.

forwardRef
// ── Child: forward the ref to the actual <input> ──
const FancyInput = forwardRef(({ label, ...props }, ref) => (
  <div>
    <label>{label}</label>
    <input ref={ref} {...props} />  // ref points to this DOM node
  </div>
));

// ── Parent: gets direct access to the input DOM node ──
function Parent() {
  const ref = useRef(null);

  return (
    <>
      <FancyInput label="Name" ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
    </>
  );
}

// ── useImperativeHandle — expose a custom API instead of the raw DOM node ──
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; }
  }));
  return <input ref={inputRef} {...props} />;
});
// Parent calls: ref.current.focus() or ref.current.clear()
10
Why use React Hook Form? How does it avoid re-renders on every keystroke?
Advanced React Hook Form

Every keystroke in a controlled input calls setState → re-renders the entire form component. On a form with 50 fields, this means 50 re-renders per keystroke across all fields — most of them doing nothing useful.

React Hook Form solves this by using uncontrolled inputs + refs internally. It only triggers re-renders when the form-level validation state changes, not on every keystroke.

React Hook Form
import { useForm } from 'react-hook-form';

function RegisterForm() {
  const {
    register,       // connects input to RHF
    handleSubmit,   // wraps your submit, provides validated data
    formState: { errors, isSubmitting },
    reset,          // reset to default values
    watch,          // watch a field's value reactively
    setValue,       // programmatically set a field
  } = useForm({ defaultValues: { name: '', email: '' } });

  const onSubmit = async (data) => {
    await saveUser(data); // data is already validated
    reset();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('name', {
          required: 'Name is required',
          minLength: { value: 2, message: 'Min 2 chars' }
        })}
      />
      {errors.name && <p>{errors.name.message}</p>}

      <input
        {...register('email', {
          required: 'Email required',
          pattern: { value: /^\S+@\S+\.\S+$/, message: 'Invalid email' }
        })}
      />
      {errors.email && <p>{errors.email.message}</p>}

      <button disabled={isSubmitting}>
        {isSubmitting ? 'Saving...' : 'Register'}
      </button>
    </form>
  );
}

// With Zod schema validation (production pattern)
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  name:  z.string().min(2),
  email: z.string().email(),
});

const { register, handleSubmit } = useForm({
  resolver: zodResolver(schema)
});
ScenarioUse
Simple form, 2–5 fieldsNative controlled (useState)
Real-time dependent fields (field A affects field B)Controlled + watch()
Large form, many fields, performance mattersReact Hook Form
Schema-driven validation, TypeScript types from schemaRHF + Zod/Yup
Dynamic field arrays (add/remove rows)RHF useFieldArray
💬 Interview Tip

Show you know the trade-off: "For simple forms I use controlled components because the code is straightforward and explicit. For forms with many fields or complex validation, I reach for React Hook Form because it's uncontrolled under the hood — zero re-renders per keystroke — and pairs cleanly with Zod for type-safe validation."

🪝
Segment 4 — Hooks Deep Dive
Hooks are React's most interviewed topic. This segment goes beyond "what does it do" and builds the mental model for why each hook exists, when to reach for it, and — critically — when NOT to. Master useRef, useMemo, useCallback, useReducer, useContext, and the art of building reusable custom hooks.
Rules of Hooks useMemo useCallback useReducer useContext Custom Hooks
10
Questions
0
Opened
4
Topics
~60m
Study Time
🧠 Mental Model — Hooks Are Linked List Slots

React stores hooks in an ordered linked list attached to each component's fiber node. The first call to useState reads slot 0, the second reads slot 1, and so on. React relies entirely on call order to match each hook call to its stored state — this is why hooks cannot be called conditionally.

🗃️ The Filing Cabinet Analogy

Imagine a filing cabinet with numbered drawers. React opens drawer 1 for your first hook, drawer 2 for the second. If you skip drawer 2 sometimes (conditional hook), React puts the wrong data in every subsequent drawer. The cabinet only works if you always open drawers in the same order.

Referential equalityTwo values are equal if they point to the exact same object in memory. {} === {} is false even if contents match.
MemoisationCaching the result of a computation so it's only recalculated when inputs change.
Stable referenceA value whose memory address doesn't change across renders — critical for React.memo and useEffect deps.
ReducerA pure function: (currentState, action) → nextState. Predictable, testable, no side effects.
ContextA way to share values through the component tree without passing props at every level.
Custom hookA plain JS function whose name starts with "use" that calls other hooks — reusable stateful logic.
Topic A — Rules & useRef
1
What are the Rules of Hooks? Why does React enforce them?
Easy Rules of Hooks
Rule 1

Only call hooks at the top level. Never inside loops, conditions, or nested functions. Always in the same order every render.

Rule 2

Only call hooks from React functions. Either function components or other custom hooks — never from regular JS functions, class methods, or event handlers.

Rules of Hooks
// ❌ Conditional hook — breaks the slot order
function BadComponent({ isAdmin }) {
  const [name, setName] = useState('');    // slot 0 ✅

  if (isAdmin) {
    const [role, setRole] = useState(''); // slot 1 — but ONLY sometimes!
  }

  const [age, setAge] = useState(0);      // slot 1 or 2 depending on isAdmin 💥
}
// When isAdmin flips, "age" reads from the wrong slot → corrupted state

// ✅ Condition goes INSIDE the hook, not around it
function GoodComponent({ isAdmin }) {
  const [name, setName] = useState(''); // always slot 0
  const [role, setRole] = useState(''); // always slot 1
  const [age, setAge]  = useState(0);  // always slot 2

  // Condition is inside useEffect, not around the hook
  useEffect(() => {
    if (isAdmin) fetchAdminRole().then(setRole);
  }, [isAdmin]);
}

// ❌ Hook inside event handler
const handleClick = () => {
  const [x, setX] = useState(0); // ❌ not called during render
};
eslint-plugin-react-hooks
Install eslint-plugin-react-hooks — it enforces both rules automatically and catches violations before runtime. The exhaustive-deps rule also flags missing useEffect dependencies. This plugin ships with Create React App and Vite's React template.
2
useRef deep dive — two distinct use cases and when to use ref vs state.
Easy useRef

useRef(initial) returns { current: initial }. The object itself is stable — same reference every render. Mutating .current is not a state update: it doesn't schedule a re-render, it doesn't trigger effects, React doesn't know it changed.

useRef DOM
function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  const play  = () => videoRef.current.play();
  const pause = () => videoRef.current.pause();

  return (
    <>
      <video ref={videoRef} src={src} />
      <button onClick={play}>Play</button>
      <button onClick={pause}>Pause</button>
    </>
  );
  // videoRef.current is null until after first render (before mount)
  // Always null-check: videoRef.current?.play()
}
useRef Mutable
// Track how many times a component rendered (for debugging)
function RenderCounter() {
  const renderCount = useRef(0);
  renderCount.current++; // no re-render triggered
  return <p>Rendered {renderCount.current} times</p>;
}

// Store previous value of a prop/state
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value; // runs after render — stores this render's value for next
  });
  return ref.current; // returns value from PREVIOUS render
}

// Store a callback without stale closure (useEvent pattern)
function useStableCallback(fn) {
  const ref = useRef(fn);
  useEffect(() => { ref.current = fn; }); // always latest fn
  return useCallback((...args) => ref.current(...args), []); // stable identity
}
NeedUse
Value that triggers re-render when changeduseState
Value that persists across renders but never triggers re-renderuseRef
Access a DOM node after renderuseRef
Track interval/timeout IDsuseRef
Store previous prop/state valueuseRef
3
useMemo — what it does, when it helps, and when it's a waste.
Medium useMemo

useMemo caches the return value of a function between renders. It only recalculates when one of its dependencies changes. Think of it as "memoised computation."

useMemo
// ── Without useMemo — recalculates on every render ──
function ProductList({ products, filter, theme }) {
  // Runs on EVERY render, even when only theme changes
  const filtered = products
    .filter(p => p.name.includes(filter))
    .sort((a, b) => a.price - b.price);

  return <List items={filtered} theme={theme} />;
}

// ── With useMemo — only recalculates when products or filter change ──
function ProductList({ products, filter, theme }) {
  const filtered = useMemo(() =>
    products
      .filter(p => p.name.includes(filter))
      .sort((a, b) => a.price - b.price),
  [products, filter]); // theme change = skip recalc ✅

  return <List items={filtered} theme={theme} />;
}

// ── Stabilise object/array reference for useEffect deps ──
// Without: new object every render → effect re-fires every render
const options = useMemo(
  () => ({ endpoint: url, method: 'GET' }),
  [url]
);
useEffect(() => { fetch(options); }, [options]); // stable ✅
⚠ useMemo has a cost too

Every useMemo adds memory (cache storage) and overhead (dependency comparison on every render). The React team explicitly says: don't add useMemo everywhere "just in case." Only use it when:

  • The computation is measurably slow (>1ms — use React Profiler to verify)
  • You need a stable reference to prevent downstream re-renders or effect re-runs
  • The computation is called many times per second (e.g., inside a scroll handler)

Simple operations like array.filter() on a 10-item list are faster without useMemo than with it.

💬 Interview Tip

The best answer pairs the use case with a profiling step: "I'd first profile with React DevTools Profiler to confirm the computation is actually slow before adding useMemo. Premature memoisation makes code harder to read for zero gain."

Topic B — useCallback & useReducer
4
useCallback — stable function references, when it actually matters.
Medium useCallback

useCallback(fn, deps) returns a memoised function — the same function reference across renders as long as deps don't change. It's essentially useMemo(() => fn, deps) but specifically for functions.

useCallback
// ── The problem: new function = React.memo child re-renders anyway ──
function Parent() {
  const [count, setCount] = useState(0);

  // New function object every render → ExpensiveChild always re-renders
  const handleSubmit = (data) => saveData(data);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <ExpensiveChild onSubmit={handleSubmit} /> // re-renders on every count change
    </>
  );
}

// ── Fix: useCallback + React.memo on child ──
function Parent() {
  const [count, setCount] = useState(0);

  // Stable reference — only changes if saveData changes
  const handleSubmit = useCallback((data) => {
    saveData(data);
  }, []); // empty deps = never changes

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <ExpensiveChild onSubmit={handleSubmit} /> // skips re-render ✅
    </>
  );
}

// ExpensiveChild must be wrapped in React.memo to benefit
const ExpensiveChild = React.memo(({ onSubmit }) => {
  console.log('ExpensiveChild rendered');
  return <form onSubmit={onSubmit}>...</form>;
});

// ── useCallback for stable useEffect dependency ──
const fetchUser = useCallback(() => {
  api.get(`/users/${userId}`).then(setUser);
}, [userId]);

useEffect(() => { fetchUser(); }, [fetchUser]); // safe — only re-runs when userId changes
⚠ useCallback needs React.memo to have any effect on rendering
useCallback alone does nothing to prevent child re-renders. The child must be wrapped in React.memo (or PureComponent). Without that, the child re-renders regardless of prop stability. Always ask: "Is this child memoised? Does this function appear in a useEffect dep array?"
5
useReducer — when to choose it over useState, and how the reducer pattern works.
Medium useReducer

useReducer(reducer, initialState) returns [state, dispatch]. Instead of calling setX(newVal) directly, you dispatch an action — a plain object describing what happened. The reducer function decides how state should change.

🏦 The Bank Teller Analogy

You don't walk into a bank and directly modify the vault. You submit a transaction slip (action) to the teller (reducer). The teller applies the rules and updates the balance (state). You never touch the vault directly — all changes go through one controlled function.

useReducer
// ── State shape ──
const initial = { items: [], total: 0, loading: false };

// ── Reducer — pure function, no side effects ──
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.item],
        total: state.total + action.item.price
      };
    case 'REMOVE_ITEM': {
      const removed = state.items.find(i => i.id === action.id);
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.id),
        total: state.total - (removed?.price ?? 0)
      };
    }
    case 'CLEAR':
      return initial;
    case 'SET_LOADING':
      return { ...state, loading: action.loading };
    default:
      return state; // always return current state for unknown actions
  }
}

// ── Component ──
function Cart() {
  const [cart, dispatch] = useReducer(cartReducer, initial);

  return (
    <div>
      <p>Total: {cart.total}</p>
      <button onClick={() => dispatch({ type: 'ADD_ITEM', item: { id: 1, price: 10 } })}>
        Add
      </button>
      <button onClick={() => dispatch({ type: 'CLEAR' })}>Clear</button>
    </div>
  );
}
SituationUse
Single independent valueuseState
Multiple related values that change togetheruseReducer
Next state depends on previous in complex waysuseReducer
Many event types affecting the same state objectuseReducer
Need to test state transitions in isolationuseReducer (reducer is pure, easy to unit test)
State changes are simple and independentuseState
6
useMemo vs useCallback — what's the real difference? Common confusions cleared.
Medium useMemo / useCallback
Comparison
// useMemo — caches the RETURN VALUE of a function
const sortedList = useMemo(
  () => [...items].sort(),  // ← this function runs, result is cached
  [items]
);
// sortedList is an ARRAY (the return value)

// useCallback — caches the FUNCTION ITSELF
const handleSort = useCallback(
  () => setItems(i => [...i].sort()), // ← this function is cached as-is
  []
);
// handleSort is a FUNCTION

// They are equivalent:
const handleSort = useCallback(fn, [deps]);
// is exactly the same as:
const handleSort = useMemo(() => fn, [deps]);
HookCachesUse for
useMemoComputed value (array, object, number…)Expensive calculation, stable object reference for deps
useCallbackFunction referenceStable callback for memoised children or useEffect deps
The one-sentence rule
useMemo = cache a value. useCallback = cache a function. Both only matter when something downstream checks for referential equality — either a React.memo component or a useEffect / useMemo dependency array.
Topic C — useContext
7
How does useContext work? Build the full provider pattern from scratch.
Medium useContext

Prop drilling: passing a value through many intermediate components that don't need it, just to reach a deeply nested child. Context creates a "teleport" — any component inside the provider can read the value directly, no matter how deep.

📡 The WiFi Analogy

Instead of running an ethernet cable from your router through every room to reach the device that needs it (prop drilling), you broadcast a WiFi signal (context). Any device in range can connect directly — the rooms in between don't need to participate.

Context Pattern
// ── 1. Create context with default (for use outside provider) ──
const ThemeContext = createContext(null);

// ── 2. Provider component encapsulates state ──
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggle = useCallback(
    () => setTheme(t => t === 'light' ? 'dark' : 'light'),
    []
  );

  // Memoize value object — avoids all consumers re-rendering on unrelated parent renders
  const value = useMemo(() => ({ theme, toggle }), [theme, toggle]);

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

// ── 3. Custom hook — throws if used outside provider ──
export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
  return ctx;
}

// ── 4. Usage anywhere in the tree ──
function ThemeButton() {
  const { theme, toggle } = useTheme();
  return <button onClick={toggle}>Mode: {theme}</button>;
}

// ── 5. Wire at app root ──
function App() {
  return (
    <ThemeProvider>
      <Router>
        <Layout />   // ThemeButton anywhere inside works
      </Router>
    </ThemeProvider>
  );
}
8
Context performance — why does it cause re-renders and how do you fix it?
Medium Context Performance

Every component that calls useContext(MyCtx) will re-render whenever the context value changes — even if the part of the value it uses didn't change. Context uses reference equality: a new object reference (even with same contents) triggers all consumers.

Context Re-render Problem + Fixes
// ❌ Problem: new object on every render → all consumers re-render
function BadProvider({ children }) {
  const [user, setUser]   = useState(null);
  const [theme, setTheme] = useState('dark');

  return (
    // New object literal every render → reference changes → all consumers re-render
    <AppCtx.Provider value={{ user, setUser, theme, setTheme }}>
      {children}
    </AppCtx.Provider>
  );
}

// ✅ Fix 1: useMemo on the value object
const value = useMemo(
  () => ({ user, setUser, theme, setTheme }),
  [user, theme] // setters are stable, don't need to be deps
);

// ✅ Fix 2: Split context by update frequency (best pattern)
const UserCtx  = createContext(null); // changes when user logs in/out
const ThemeCtx = createContext(null); // changes on theme toggle

// Now: toggling theme only re-renders ThemeCtx consumers, not user consumers
function Providers({ children }) {
  const [user, setUser]   = useState(null);
  const [theme, setTheme] = useState('dark');
  return (
    <UserCtx.Provider  value={useMemo(() => ({ user, setUser }), [user])}>
    <ThemeCtx.Provider value={useMemo(() => ({ theme, setTheme }), [theme])}>
      {children}
    </ThemeCtx.Provider>
    </UserCtx.Provider>
  );
}
⚠ Context is not a global state manager
Context is great for low-frequency updates (theme, locale, auth user). For high-frequency state (search input, cart items updating on every keystroke), use Zustand, Redux, or Jotai — they have fine-grained subscription, so only components that use a specific slice re-render.
Topic D — Custom Hooks
9
How do you build custom hooks? Walk through 3 real-world examples.
Advanced Custom Hooks

A custom hook is a plain JavaScript function whose name starts with use and that calls other hooks inside it. That's it. No special API, no registration. The use prefix tells React (and the linter) to apply the Rules of Hooks to it.

The superpower: extract stateful logic shared across components without changing the component tree. No HOC wrapper, no render prop — just a function call.

useFetch
function useFetch(url) {
  const [data, setData]       = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState(null);

  useEffect(() => {
    if (!url) return;
    const controller = new AbortController();
    setLoading(true);
    fetch(url, { signal: controller.signal })
      .then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
      .then(setData)
      .catch(e => { if (e.name !== 'AbortError') setError(e); })
      .finally(() => setLoading(false));
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage — clean component, zero fetch logic
function UserProfile({ id }) {
  const { data, loading, error } = useFetch(`/api/users/${id}`);
  if (loading) return <Spinner />;
  if (error)   return <Error msg={error.message} />;
  return <p>{data.name}</p>;
}
useLocalStorage
function useLocalStorage(key, defaultValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : defaultValue;
    } catch { return defaultValue; }
  });

  const setStored = useCallback((next) => {
    const toStore = typeof next === 'function' ? next(value) : next;
    setValue(toStore);
    localStorage.setItem(key, JSON.stringify(toStore));
  }, [key, value]);

  return [value, setStored];
}
// Usage: const [theme, setTheme] = useLocalStorage('theme', 'dark');
useWindowSize
function useWindowSize() {
  const [size, setSize] = useState({
    width:  window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handle = () => setSize({
      width:  window.innerWidth,
      height: window.innerHeight
    });
    window.addEventListener('resize', handle);
    return () => window.removeEventListener('resize', handle);
  }, []);

  return size;
}
// Usage: const { width } = useWindowSize();
// const isMobile = width < 768;
10
How does React track hook call order internally? What happens during re-renders?
Advanced Hook Internals

Each React component instance has a corresponding Fiber node. Attached to that fiber is a linked list of "hook objects." Each call to a hook during render reads from (or writes to) the next node in this list, in order.

Hook List — Conceptual
function Counter() {
  const [count, setCount] = useState(0); // Hook node 1: { state: 0, queue: [] }
  const [name,  setName]  = useState(''); // Hook node 2: { state: '', queue: [] }
  useEffect(() => {}, []);               // Hook node 3: { effect, deps: [] }
  const memoVal = useMemo(() => count * 2, [count]); // Hook node 4: { value, deps }
}

// Fiber's hook list (conceptual linked list):
// node1 → node2 → node3 → node4 → null

// On RE-RENDER: React walks the SAME list in order
// useState call 1 → reads node1.state → returns [0, setter]
// useState call 2 → reads node2.state → returns ['', setter]
// useEffect call  → reads node3.deps → compares, schedules if changed
// useMemo call    → reads node4.deps → recomputes if changed

// MOUNT vs UPDATE — different dispatcher
// First render: "mount dispatcher" — creates nodes, stores initial values
// Subsequent: "update dispatcher" — reads existing nodes, applies queued updates
1
setCount(5) is called — React adds an update object to hook node 1's queue: { action: 5, next: null }
2
React schedules a re-render of Counter (marks the fiber as "dirty")
3
During re-render, the update dispatcher walks the queue, applies updates: node1.state = 5
4
Component function runs again. useState reads node1.state (now 5) and returns [5, setter]
5
New virtual DOM is produced, diffed against previous, real DOM patched
Two dispatchers — mount vs update
React uses two different hook implementations internally. The mount dispatcher creates hook nodes for the first time. The update dispatcher reads from existing nodes. This is why calling a hook for the first time on a re-render (conditional hook) crashes — there's no node at that position to read from.
🧩
Segment 5 — Component Patterns
Every senior React interview goes deep on patterns. HOCs, render props, compound components, error boundaries — these are the techniques that separate developers who write React from developers who design React. Understand the trade-offs, know when each pattern fits, and explain why hooks replaced most of them.
HOC Render Props Compound Components React.memo Error Boundaries Suspense
10
Questions
0
Opened
4
Topics
~55m
Study Time
🧠 Mental Model — Patterns Evolve With React

React patterns exist to solve one core problem: how do you share logic and UI structure between components? The answer changed three times as React matured:

1
Mixins (React ~2014) — shared logic via mixin objects. Caused naming conflicts and implicit dependencies. Removed when classes arrived.
2
HOC + Render Props (2015–2018) — wrapped components or passed render functions. Powerful but created "wrapper hell" — deeply nested component trees.
3
Custom Hooks (React 16.8, 2019–present) — share logic as plain functions. No wrapper, no prop gymnastics, clean component tree.
When patterns still matter
HOCs are still used by libraries (Redux connect, React Router withRouter). Compound components are the gold standard for UI kits (Tabs, Accordion, Select). Error boundaries must be class components. Knowing these patterns is essential for reading and contributing to real codebases.
Topic A — HOC & Render Props
1
What is a Higher-Order Component (HOC)? Pattern, use cases, and pitfalls.
Easy HOC

A HOC is a function that takes a component and returns a new enhanced component. It's a pure function — it doesn't modify the original component, it wraps it. The name conventionally starts with with.

🎁 The Gift Wrap Analogy

A HOC is like gift-wrapping a present. The original gift (component) is unchanged inside. The wrapping paper (HOC) adds extra presentation — a bow, a tag, protection. You can wrap the same gift in different paper for different occasions.

HOC Pattern
// ── withAuth HOC — redirect if not logged in ──
function withAuth(WrappedComponent) {
  // Return a new component that wraps the original
  function AuthGuard(props) {
    const { user } = useAuth();

    if (!user) {
      return <Navigate to="/login" />;
    }
    // Pass ALL original props through — transparency is key
    return <WrappedComponent {...props} user={user} />;
  }
  // Preserve displayName for React DevTools
  AuthGuard.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;
  return AuthGuard;
}

// Usage
const ProtectedDashboard = withAuth(Dashboard);

// ── withLogger HOC — log renders ──
function withLogger(WrappedComponent) {
  return function(props) {
    useEffect(() => {
      console.log(`${WrappedComponent.name} mounted`, props);
      return () => console.log(`${WrappedComponent.name} unmounted`);
    }, []);
    return <WrappedComponent {...props} />;
  };
}

// ── HOCs are composable ──
const EnhancedPage = withLogger(withAuth(Dashboard));
// Renders: EnhancedPage → withLogger wrapper → withAuth wrapper → Dashboard
PitfallProblemFix
Prop collisionHOC injects a prop with same name as originalDocument injected props; use namespacing
Ref not forwardedref attaches to wrapper, not inner componentUse forwardRef
Static methods lostStatic methods on wrapped component don't copy overUse hoist-non-react-statics library
DevTools namingComponent shows as "Component" not "withAuth(Dashboard)"Set displayName explicitly
Wrapper hell3+ HOCs = deeply nested tree, hard to debugRefactor to custom hook
2
What is the render props pattern? When does it shine over HOCs?
Easy Render Props

A component accepts a function as a prop (usually named render or children) and calls it to produce its output. The component shares its internal state/logic by passing it as arguments to that function — giving the caller full control over rendering.

Render Props
// ── Mouse tracker with render prop ──
function MouseTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });

  return (
    <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
      {render(pos)}  // caller decides what to do with pos
    </div>
  );
}

// Caller 1: show coordinates
<MouseTracker render={({ x, y }) => <p>{x}, {y}</p>} />

// Caller 2: move an image with the cursor
<MouseTracker render={({ x, y }) => (
  <img src="/cat.png" style={{ position: 'fixed', left: x, top: y }} />
)} />

// ── children as a function (same pattern, cleaner syntax) ──
function Toggle({ children }) {
  const [on, setOn] = useState(false);
  return <{children({ on, toggle: () => setOn(v => !v) })}>;
}

<Toggle>
  {({ on, toggle }) => (
    <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
  )}
</Toggle>
When render props beat HOCs
Render props are more explicit — you see exactly what data is being shared at the call site. HOCs hide the injected props. However, render props with inline functions create a new function on every render, which can break shouldComponentUpdate / React.memo. Both are mostly replaced by custom hooks today — but knowing them is essential for understanding library code.
3
HOC vs Render Props vs Custom Hooks — why did hooks win?
Easy Pattern Evolution

All three solve "share stateful logic between components." The logic: track the current window width and provide it to any component that needs it.

Three Approaches
// ── HOC approach ──
function withWindowWidth(Component) {
  return function(props) {
    const [width, setWidth] = useState(window.innerWidth);
    useEffect(() => {
      const h = () => setWidth(window.innerWidth);
      window.addEventListener('resize', h);
      return () => window.removeEventListener('resize', h);
    }, []);
    return <Component {...props} windowWidth={width} />;
  };
}
const MyPage = withWindowWidth(Page); // extra wrapper in DevTools

// ── Render prop approach ──
function WindowWidth({ children }) {
  const [width, setWidth] = useState(window.innerWidth);
  // ...same effect...
  return children(width);
}
// extra nesting in JSX:
<WindowWidth>
  {(width) => <Page width={width} />}
</WindowWidth>

// ── Custom hook approach ── (winner)
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const h = () => setWidth(window.innerWidth);
    window.addEventListener('resize', h);
    return () => window.removeEventListener('resize', h);
  }, []);
  return width;
}
// Usage — zero component tree impact:
function Page() {
  const width = useWindowWidth(); // clean! no wrappers, no prop injection
  return <p>{width < 768 ? 'Mobile' : 'Desktop'}</p>;
}
HOCRender PropsCustom Hook
Component tree pollutionAdds wrapper nodeAdds wrapper nodeNone ✅
Prop source clarityHidden — where did this prop come from?Explicit at call siteExplicit ✅
ComposabilityMessy with 3+ HOCsCallback nestingJust call multiple hooks ✅
TypeScriptHard to type correctlyVerboseNatural ✅
Still used in librariesYes (Redux, Router)RareDefault choice ✅
Topic B — Compound Components & React.memo
4
What are compound components? Build a Tabs component using the pattern.
Medium Compound Components

A group of components that work together and share implicit state via context — like HTML's <select> and <option>. The parent manages state; children read from it without needing explicit props for coordination. The user of the API gets full compositional control over structure.

🍽️ The Restaurant Menu Analogy

A compound component is like a restaurant menu. The menu (parent) holds the current selection state. The sections — Starters, Mains, Desserts (children) — each know what's selected without you telling each section separately. You compose the menu layout freely, and the state just works.

Compound Components
// ── 1. Shared context ──
const TabsCtx = createContext(null);

// ── 2. Parent manages state ──
function Tabs({ children, defaultTab = 0 }) {
  const [active, setActive] = useState(defaultTab);
  return (
    <TabsCtx.Provider value={{ active, setActive }}>
      <div className="tabs">{children}</div>
    </TabsCtx.Provider>
  );
}

// ── 3. Sub-components read from context ──
function TabList({ children }) {
  return <div role="tablist" className="tab-list">{children}</div>;
}

function Tab({ index, children }) {
  const { active, setActive } = useContext(TabsCtx);
  return (
    <button
      role="tab"
      aria-selected={active === index}
      className={active === index ? 'tab active' : 'tab'}
      onClick={() => setActive(index)}
    >
      {children}
    </button>
  );
}

function TabPanels({ children }) {
  const { active } = useContext(TabsCtx);
  return <div className="panels">{React.Children.toArray(children)[active]}</div>;
}

function TabPanel({ children }) {
  return <div role="tabpanel">{children}</div>;
}

// ── 4. Attach sub-components as static properties (optional, clean API) ──
Tabs.List   = TabList;
Tabs.Tab    = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel  = TabPanel;

// ── 5. API the consumer sees — flexible, self-documenting ──
<Tabs defaultTab={0}>
  <Tabs.List>
    <Tabs.Tab index={0}>Overview</Tabs.Tab>
    <Tabs.Tab index={1}>Details</Tabs.Tab>
    <Tabs.Tab index={2}>Reviews</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel>Overview content</Tabs.Panel>
    <Tabs.Panel>Details content</Tabs.Panel>
    <Tabs.Panel>Reviews content</Tabs.Panel>
  </Tabs.Panels>
</Tabs>
5
React.memo — how does it work, what does it compare, and when is it useless?
Medium React.memo

React.memo wraps a functional component and skips re-rendering if the parent re-renders but the component's props haven't changed. It performs a shallow comparison of each prop using Object.is.

React.memo
// Without memo — re-renders every time parent renders
function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered');
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

// With memo — skips render if items reference didn't change
const ExpensiveList = React.memo(function({ items }) {
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
});

// Custom comparison for deep equality or partial checks
const UserCard = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prev, next) => prev.user.id === next.user.id  // only re-render if id changes
);
ScenarioWhy memo fails
Prop is an inline object: style={{ color: 'red' }}New object reference every render → always "changed"
Prop is an inline function: onClick={() => fn()}New function reference every render → always "changed"
Component has no props at allNothing to compare — but it still prevents re-render if parent renders, which is the point
Parent passes children JSXchildren is a new React element every render — memo breaks unless you also memoize the children
The trio: React.memo + useCallback + useMemo
These three work together. React.memo prevents child re-render only if all props are stable. Stabilise function props with useCallback. Stabilise object props with useMemo. Without the trio working together, memoisation doesn't help.
6
Controlled vs uncontrolled component design — from a library author's perspective.
Medium Component Design

When building a reusable component (e.g., a <Select> dropdown), you face a choice: does the component manage its own open/closed state (uncontrolled), or does the parent control it via props (controlled)? The best components support both modes — like HTML's <input>.

Dual-Mode Component
function Dropdown({
  // Controlled mode props
  open:      controlledOpen,   // if provided, parent controls it
  onOpenChange,                 // parent's setter
  // Uncontrolled mode props
  defaultOpen = false,          // initial value (uncontrolled)
  children
}) {
  // Internal state — used only in uncontrolled mode
  const [internalOpen, setInternalOpen] = useState(defaultOpen);

  // Is this component controlled? Check if parent passed the open prop
  const isControlled = controlledOpen !== undefined;
  const open = isControlled ? controlledOpen : internalOpen;

  const handleToggle = () => {
    if (isControlled) {
      onOpenChange?.(!open); // notify parent — parent decides the new state
    } else {
      setInternalOpen(v => !v); // manage our own state
    }
  };

  return (
    <div>
      <button onClick={handleToggle}>Toggle</button>
      {open && <div className="panel">{children}</div>}
    </div>
  );
}

// Uncontrolled usage (simpler)
<Dropdown defaultOpen={false}>Content</Dropdown>

// Controlled usage (parent owns state)
<Dropdown open={isMenuOpen} onOpenChange={setIsMenuOpen}>
  Content
</Dropdown>
💬 Interview Tip

This pattern is used by every serious UI library: Radix UI, Headless UI, Ant Design. Naming convention: value/onChange for controlled, defaultValue for uncontrolled. Mentioning you've thought about API design from a library perspective signals senior-level thinking.

Topic C — Error Boundaries & Portals
7
What are Error Boundaries? What do they catch — and what don't they catch?
Medium Error Boundaries

An Error Boundary is a class component that implements componentDidCatch and/or getDerivedStateFromError. It catches JavaScript errors thrown during rendering, in lifecycle methods, and in constructors of child components — then shows a fallback UI instead of crashing the whole app.

🔌 The Circuit Breaker Analogy

An error boundary is an electrical circuit breaker. When a fault (error) occurs in one circuit (component subtree), the breaker trips and isolates it — the rest of the building (app) stays powered. Without it, one fault blows the whole fuse box.

Error Boundary
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  // Runs during render phase — update state to show fallback
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  // Runs after render — good for logging to Sentry etc.
  componentDidCatch(error, info) {
    logToSentry(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? <h2>Something went wrong.</h2>;
    }
    return this.props.children;
  }
}

// Usage — wrap any subtree
<ErrorBoundary fallback={<ErrorPage />}>
  <UserDashboard />
</ErrorBoundary>

// Multiple granular boundaries — isolate failures
<Layout>
  <ErrorBoundary fallback={<SidebarError />}><Sidebar /></ErrorBoundary>
  <ErrorBoundary fallback={<FeedError />}><Feed /></ErrorBoundary>
</Layout>
Not caughtWhy / Alternative
Event handler errorsNot part of rendering — use try/catch inside the handler
Async code (setTimeout, Promises)Not thrown during render — use try/catch + setState
Server-side rendering errorsError boundaries are client-only
Errors in the boundary itselfBoundary can't catch its own errors — needs a parent boundary
react-error-boundary library
The community react-error-boundary package provides a functional-friendly API with a useErrorBoundary hook and an onReset callback for retry logic — avoids writing class components yourself.
Topic D — Lazy Loading & Suspense
8
What is React.lazy and Suspense? How does code splitting work in React?
Medium Lazy / Suspense

Without code splitting, your entire app ships in one JS bundle. Users on the login page download code for the admin dashboard they'll never see. React.lazy + dynamic import() splits the bundle — each lazy component becomes a separate chunk downloaded only when needed.

Lazy Loading
import { lazy, Suspense } from 'react';

// lazy() takes a function returning a dynamic import
// The component is loaded only when first rendered
const AdminDashboard = lazy(() => import('./AdminDashboard'));
const UserProfile    = lazy(() => import('./UserProfile'));
const Analytics      = lazy(() => import('./Analytics'));

function App() {
  return (
    // Suspense catches lazy components "suspending" and shows fallback
    <Suspense fallback={<PageSpinner />}>
      <Routes>
        <Route path="/admin"     element={<AdminDashboard />} />
        <Route path="/profile"   element={<UserProfile />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

// Nested Suspense — granular loading states
<Suspense fallback={<LayoutSkeleton />}>    // outer: layout loading
  <Layout>
    <Suspense fallback={<ChartSkeleton />}>  // inner: chart loading
      <HeavyChart />
    </Suspense>
  </Layout>
</Suspense>

// Combine with Error Boundary for robust async UI
<ErrorBoundary fallback={<ErrorPage />}>
  <Suspense fallback={<Spinner />}>
    <LazyPage />
  </Suspense>
</ErrorBoundary>
⚠ React.lazy only works with default exports
lazy(() => import('./Comp')) expects the module to have a default export. For named exports: lazy(() => import('./Comp').then(m => ({ default: m.NamedComp })))
9
What is the Provider Pattern? How do you combine it with useReducer for scalable state?
Advanced Provider Pattern

Combining useReducer with Context gives you a scalable state management pattern without any external library. The reducer handles state transitions; the context broadcasts state + dispatch to the whole tree.

Provider + useReducer
// ── store.js ──
const StateCtx    = createContext(null);
const DispatchCtx = createContext(null); // separate ctx — dispatch is always stable

const initial = { user: null, cart: [], notifications: [] };

function appReducer(state, action) {
  switch (action.type) {
    case 'LOGIN':        return { ...state, user: action.user };
    case 'LOGOUT':       return { ...state, user: null, cart: [] };
    case 'ADD_TO_CART':  return { ...state, cart: [...state.cart, action.item] };
    default: return state;
  }
}

export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initial);

  return (
    <DispatchCtx.Provider value={dispatch}>
      <StateCtx.Provider value={state}>
        {children}
      </StateCtx.Provider>
    </DispatchCtx.Provider>
  );
}

// Separate hooks — consumers only re-render when their slice changes
export const useAppState    = () => useContext(StateCtx);
export const useAppDispatch = () => useContext(DispatchCtx);

// ── Usage in any component ──
function AddToCartButton({ item }) {
  const dispatch = useAppDispatch(); // stable — never re-renders from state changes
  return (
    <button onClick={() => dispatch({ type: 'ADD_TO_CART', item })}>
      Add to Cart
    </button>
  );
}
Why separate StateCtx and DispatchCtx
dispatch from useReducer is always the same function reference — it never changes. By putting it in a separate context, components that only dispatch actions (buttons, forms) never re-render when state changes. They only subscribed to DispatchCtx which never changes value.
10
What are React's reconciliation heuristics for components — type, key, position?
Advanced Reconciliation
Reconciliation
// ── Rule 1: Different type = full rebuild ──
// Before: <Counter />   After: <p>...</p>
// React unmounts Counter (destroys state), mounts <p>
// This is why conditionally swapping components resets state!

function App({ isAdmin }) {
  return isAdmin
    ? <AdminPanel />   // toggling between these: full unmount/remount each time
    : <UserPanel />;   // state inside AdminPanel is destroyed when switching
}

// ── Rule 2: Same type, same position = update in place ──
// Both are <Counter /> — React reuses the instance, updates props
function App({ isPremium }) {
  return isPremium
    ? <Counter color="gold" />   // same position, same type
    : <Counter color="grey" />;  // → state PRESERVED, only color prop changes
}

// ── Trick: reset state by changing key ──
// React treats different key = different component = full remount
<ProfileForm key={userId} />
// When userId changes, ProfileForm fully remounts — state is fresh ✅
// This replaces the anti-pattern of getDerivedStateFromProps / useEffect sync

// ── Position matters, not just type ──
function App({ showBanner }) {
  return (
    <div>
      {showBanner && <Banner />}  // position 0 — conditionally rendered
      <Counter />                   // when Banner appears, Counter shifts from pos 0 to 1
    </div>                           // → Counter remounts! State lost. Use key to stabilise.
  );
}
Using key to reset component state

The key prop isn't only for lists. You can put it on any component to tell React "treat this as a completely new component when the key changes." This is the correct way to reset a component's internal state when a prop changes — far cleaner than useEffect + setState or getDerivedStateFromProps.

Example: <CommentBox key={postId} /> — when the user navigates to a new post, the comment box is brand new with empty state, no manual clearing needed.

Segment 6 — Performance Optimisation
Performance is where good developers separate from great ones. This segment teaches you to diagnose before you optimise — profile first, fix second. Covers re-render mechanics, the memoisation trilogy, code splitting, list virtualisation, and React 18's concurrent features that make heavy UIs feel instant.
Re-renders Profiler Code Splitting Virtualisation startTransition Web Vitals
10
Questions
0
Opened
4
Topics
~55m
Study Time
🧠 Mental Model — Profile First, Optimise Second

The #1 performance mistake in React is optimising blindly. Adding useMemo and React.memo everywhere makes code harder to read and can even slow things down. The correct workflow is:

1
Measure — open React DevTools Profiler, record a slow interaction, find which component takes the most time
2
Identify — is it too many re-renders? A slow computation? A large bundle? A long list?
3
Fix the right thing — apply the targeted fix (memo, virtualise, split, defer) not a blanket one
4
Measure again — confirm the fix actually improved the metric
Re-renderReact calling a component function again. Not always expensive — only expensive if it does heavy work or causes a cascade.
Commit phaseAfter diffing, React writes changes to the real DOM. This is the expensive part — minimise DOM mutations.
Code splittingBreaking the JS bundle into chunks loaded on demand — reduces initial load time.
VirtualisationOnly rendering DOM nodes for items currently visible in the viewport — essential for lists of 100+ items.
startTransitionReact 18 API to mark a state update as non-urgent — lets React interrupt it to handle user input first.
Core Web VitalsGoogle's real-user performance metrics: LCP (load), FID/INP (interactivity), CLS (layout stability).
Topic A — Re-render Mechanics
1
What are the 4 triggers that cause a React component to re-render?
Easy Re-renders
#TriggerExampleNotes
1State changesetState(newVal)Only if new value !== old value (Object.is)
2Parent re-renderParent calls setStateChildren re-render by default even if their props didn't change — use React.memo to opt out
3Context value changeProvider's value prop changesAll consumers re-render regardless of which part of context they use
4forceUpdate (class)this.forceUpdate()No functional equivalent — use setState with a counter as a workaround
Re-render vs DOM update
// Re-render = React calls your function again = cheap JS work
// DOM update = React writes to real DOM = expensive

// Even if a component re-renders 100 times, React only touches the DOM
// for the parts that actually changed. So a "wasted" re-render that produces
// the same JSX is cheap — React diffs it and does zero DOM work.

// ⚠ Re-renders ARE a problem when:
// 1. The component does expensive computation inside the render
// 2. It causes a cascade of many child re-renders
// 3. It creates new object references that invalidate downstream memos

// ✅ Check if a re-render is wasted:
// React DevTools → Profiler → check "Highlight updates when components render"
// Components that flash unexpectedly are candidates for React.memo

// Opt out of parent-triggered re-render:
const Child = React.memo(({ name }) => <p>{name}</p>);
// Child only re-renders if name prop changes, even if parent re-renders constantly
The cascading re-render problem
When a top-level component re-renders, every component in the subtree re-renders by default. A single useState call at App level can trigger hundreds of re-renders. This is why component structure matters — keep frequently changing state low in the tree, close to where it's used.
2
What is "lifting state up" and "pushing state down"? How do they affect performance?
Easy State Location

Where you put state determines which components re-render when it changes. State at the top re-renders the whole tree. State at the bottom re-renders only that leaf — and nothing else.

State Location
// ❌ State too high — search re-renders the whole app on every keystroke
function App() {
  const [search, setSearch] = useState(''); // every keystroke re-renders App
  return (
    <>
      <Header />        // ← re-renders unnecessarily
      <Sidebar />       // ← re-renders unnecessarily
      <SearchBox value={search} onChange={setSearch} />
      <Results query={search} />
    </>
  );
}

// ✅ Push state down — only SearchBox + Results re-render
function App() {
  return (
    <>
      <Header />   // never re-renders from search
      <Sidebar />  // never re-renders from search
      <SearchSection />  // self-contained: owns search state internally
    </>
  );
}
function SearchSection() {
  const [search, setSearch] = useState(''); // scoped here
  return (
    <>
      <SearchBox value={search} onChange={setSearch} />
      <Results query={search} />
    </>
  );
}

// ── Lifting content out of a re-rendering parent ──
// "children as props" trick — children don't re-render with parent
function AnimatedWrapper({ children }) {
  const [tick, setTick] = useState(0); // updates every frame
  return <div style={{ opacity: tick % 2 }}>{children}</div>;
}
// children are created in the PARENT of AnimatedWrapper
// → children's identity is stable → they don't re-render with tick!
<AnimatedWrapper>
  <ExpensiveTree />  // ✅ won't re-render on tick changes
</AnimatedWrapper>
💬 Interview Tip

The "children as props" trick is a lesser-known gem that interviewers love. Explain: children are created by the parent of the wrapper, so when the wrapper re-renders, the children JSX elements aren't recreated — their reference is stable, so React skips them.

3
How do you use React DevTools Profiler to find and fix performance bottlenecks?
Easy Profiler
1
Open DevTools → Profiler tab. Enable "Record why each component rendered" in settings — this tells you why a component re-rendered (state change, prop change, context, forced update).
2
Click Record, perform the slow interaction, click Stop. You now have a flame chart of every render in that interaction.
3
Read the flame chart. Wide bars = slow components. Grey = didn't render. Yellow = rendered. Click a bar to see why it rendered and how long it took (ms).
4
Check the "Ranked" view — lists components sorted by render time. The top entries are your targets.
5
Apply fix, re-profile, confirm improvement. Never assume — always measure before and after.
Profiler API
import { Profiler } from 'react';

function onRender(
  id,           // "id" prop of the Profiler
  phase,        // "mount" | "update" | "nested-update"
  actualDuration, // time spent rendering
  baseDuration,   // estimated time without memoisation
  startTime,
  commitTime
) {
  if (actualDuration > 16) { // 16ms = 1 frame at 60fps
    console.warn(`${id} took ${actualDuration.toFixed(1)}ms`);
  }
}

<Profiler id="ProductList" onRender={onRender}>
  <ProductList items={items} />
</Profiler>
// Profiler is stripped from production builds automatically
Topic B — Memoisation & Code Splitting
4
The complete memoisation trilogy — React.memo + useMemo + useCallback working together.
Medium Memoisation
Memoisation Trilogy
// ── Parent component with frequent state changes ──
function Dashboard() {
  const [filter, setFilter] = useState('');   // changes on every keystroke
  const [sortBy, setSortBy] = useState('name'); // changes rarely
  const rawData = useData();                    // large dataset, changes rarely

  // 1. useMemo — expensive computation, skip when filter/sortBy/data unchanged
  const processedRows = useMemo(() => {
    return rawData
      .filter(row => row.name.toLowerCase().includes(filter))
      .sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1);
  }, [rawData, filter, sortBy]);

  // 2. useCallback — stable function ref so DataGrid (memoised) doesn't re-render
  const handleRowClick = useCallback((rowId) => {
    navigate(`/detail/${rowId}`);
  }, [navigate]); // navigate is stable from react-router

  const handleSort = useCallback((col) => {
    setSortBy(col);
  }, []); // no deps — setSortBy is always stable

  return (
    <>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {/* 3. React.memo — DataGrid skips render when rows/handlers didn't change */}
      <DataGrid
        rows={processedRows}     // stable via useMemo
        onRowClick={handleRowClick} // stable via useCallback
        onSort={handleSort}         // stable via useCallback
      />
    </>
  );
}

// DataGrid is wrapped in React.memo — only re-renders when its props change
const DataGrid = React.memo(function DataGrid({ rows, onRowClick, onSort }) {
  console.log('DataGrid rendered'); // now only logs when rows/handlers change
  return (/* grid JSX */);
});
⚠ The memo chain must be complete
If DataGrid is memoised but one of its props is an unstabilised inline function or object, all three tools are wasted. Audit every prop passed to a memoised component: primitives ✅, stable refs via useCallback/useMemo ✅, inline {} or () => {} ❌.
5
Code splitting strategies — route-based, component-based, and third-party chunks.
Medium Code Splitting

Every kilobyte of JavaScript must be downloaded, parsed, and compiled before your app becomes interactive. On a mid-range mobile device on 3G, 1MB of JS ≈ 10 seconds to interactive. Code splitting defers loading code until it's actually needed.

Route Splitting
// Each route loads its own chunk — user on /login never downloads /admin code
const Home       = lazy(() => import('./pages/Home'));
const Dashboard  = lazy(() => import('./pages/Dashboard'));
const Admin      = lazy(() => import('./pages/Admin'));
const Settings   = lazy(() => import('./pages/Settings'));

<Suspense fallback={<PageLoader />}>
  <Routes>
    <Route path="/"          element={<Home />}      />
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/admin"     element={<Admin />}     />
  </Routes>
</Suspense>
Component Splitting
// Split heavy components that aren't needed immediately
const RichTextEditor = lazy(() => import('./RichTextEditor'));
const HeavyChart     = lazy(() => import('./HeavyChart'));
const PDFViewer      = lazy(() => import('./PDFViewer'));

// Only loads RichTextEditor bundle when user opens the editor
{isEditorOpen && (
  <Suspense fallback={<EditorSkeleton />}>
    <RichTextEditor />
  </Suspense>
)}

// Preload on hover for near-instant feel
const preloadEditor = () => import('./RichTextEditor'); // starts download early
<button
  onMouseEnter={preloadEditor}  // preload on hover
  onClick={() => setEditorOpen(true)}
>
  Open Editor
</button>
Vite Config
// vite.config.js — split vendor libraries into separate cached chunks
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react'))       return 'vendor-react';
            if (id.includes('chart.js'))   return 'vendor-charts';
            if (id.includes('lodash'))     return 'vendor-utils';
          }
        }
      }
    }
  }
};
// React chunks are cached by browser — only app code re-downloads on deploy
6
How do you optimise images and assets in a React app? Skeleton screens and loading UX.
Medium Asset Optimisation
Image Optimisation
// ── 1. Lazy load below-the-fold images ──
<img
  src="/product.jpg"
  loading="lazy"       // native browser lazy loading
  decoding="async"     // decode off main thread
  alt="Product name"
/>

// ── 2. Responsive images with srcSet ──
<img
  src="/hero-800.webp"
  srcSet="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px"
  alt="Hero"
/>

// ── 3. Intersection Observer for custom lazy loading ──
function useLazyImage(src) {
  const imgRef = useRef(null);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setLoaded(true);
        observer.disconnect();
      }
    });
    if (imgRef.current) observer.observe(imgRef.current);
    return () => observer.disconnect();
  }, []);

  return { imgRef, src: loaded ? src : undefined };
}
Skeleton
// Skeleton matches the shape of the content — reduces layout shift
function UserCardSkeleton() {
  return (
    <div className="card">
      <div className="skeleton circle" style={{ width: 48, height: 48 }} />
      <div style={{ flex: 1 }}>
        <div className="skeleton" style={{ width: '60%', height: 16 }} />
        <div className="skeleton" style={{ width: '40%', height: 12, marginTop: 8 }} />
      </div>
    </div>
  );
}

// CSS for the shimmer animation
// .skeleton { background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
//   background-size: 200% 100%; animation: shimmer 1.5s infinite; }

function UserCard({ userId }) {
  const { data, loading } = useFetch(`/api/users/${userId}`);
  if (loading) return <UserCardSkeleton />; // shows while loading
  return <div className="card">{data.name}</div>;
}
Topic C — Virtualisation & Heavy Lists
7
What is list virtualisation? When do you need it and how does it work?
Medium Virtualisation

Rendering 10,000 rows means 10,000 DOM nodes — all in memory, all painted, all styled. Even if only 15 are visible. This causes slow initial render, high memory usage, and janky scrolling.

🎬 The Film Reel Analogy

A cinema projector doesn't store every frame of a 2-hour film in the projector at once. It only loads the current frame plus a few ahead. Virtualisation does the same — only render the rows currently in the viewport, plus a small buffer. As the user scrolls, swap old rows out and new rows in.

Virtualisation
// ── How it works ──
// 1. Container has fixed height + overflow: scroll
// 2. Inner div has total height = rowHeight × rowCount (creates scroll space)
// 3. On scroll, calculate which rows are visible: start = scrollTop / rowHeight
// 4. Only render those rows + overscan buffer, positioned absolutely

// ── react-window (lightweight, most popular) ──
import { FixedSizeList } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>  // style from react-window — MUST be applied for correct positioning
    Row {index}: {data[index].name}
  </div>
);

<FixedSizeList
  height={600}         // container height in px
  width="100%"
  itemCount={10000}   // total rows
  itemSize={50}        // each row height in px
  overscanCount={5}    // extra rows above/below viewport
>
  {Row}
</FixedSizeList>
// Renders ~15 DOM nodes regardless of 10,000 items ✅

// ── @tanstack/react-virtual (headless, more flexible) ──
const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => containerRef.current,
  estimateSize: () => 50,
  overscan: 5
});
// Gives you virtualItems array — you control the DOM structure completely
List sizeRecommendation
< 50 itemsDon't bother — standard rendering is fine
50–200 itemsOnly if each row is complex/heavy — profile first
200–1000 itemsStrongly consider virtualising
> 1000 itemsAlways virtualise
8
How do you move heavy computation off the main thread in React?
Medium Web Workers

The browser's main thread handles JavaScript execution, React rendering, layout, and painting — all on one thread. A computation that blocks for 200ms freezes the UI: no animations, no input response, no scroll. Moving heavy work to a Web Worker runs it in a background thread so the UI stays responsive.

Web Worker
// ── worker.js ──
self.onmessage = (e) => {
  const { data } = e.data;
  const result = heavyComputation(data); // runs in background
  self.postMessage({ result });
};

// ── useWorker custom hook ──
function useWorker(workerUrl) {
  const workerRef = useRef(null);

  useEffect(() => {
    workerRef.current = new Worker(new URL(workerUrl, import.meta.url));
    return () => workerRef.current.terminate();
  }, [workerUrl]);

  const postMessage = useCallback((msg) => {
    return new Promise((resolve) => {
      workerRef.current.onmessage = (e) => resolve(e.data);
      workerRef.current.postMessage(msg);
    });
  }, []);

  return { postMessage };
}

// ── Component ──
function DataProcessor() {
  const [result, setResult]   = useState(null);
  const [loading, setLoading] = useState(false);
  const { postMessage } = useWorker('./worker.js');

  const process = async () => {
    setLoading(true);
    const { result } = await postMessage({ data: bigDataset });
    // UI stayed responsive during computation!
    setResult(result);
    setLoading(false);
  };

  return (
    <>
      <button onClick={process} disabled={loading}>
        {loading ? 'Processing...' : 'Process Data'}
      </button>
      {result && <Results data={result} />}
    </>
  );
}
Comlink — cleaner Worker API
The Comlink library (by Google Chrome Labs) wraps Web Workers with a Proxy, letting you call worker functions as if they were async functions — no manual message passing. Works great with React.
Topic D — Concurrent React & Web Vitals
9
startTransition and useDeferredValue — React 18's tools for keeping the UI responsive.
Advanced Concurrent React

A search input that filters 10,000 items: every keystroke triggers an expensive re-render of the list. The input feels laggy because React is busy re-rendering the list before it can update the input field. React 18 lets you mark the list update as non-urgent — input stays snappy, list updates when there's spare time.

startTransition
import { startTransition, useTransition } from 'react';

function SearchPage() {
  const [input, setInput]   = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const val = e.target.value;

    // Urgent — update input immediately (high priority)
    setInput(val);

    // Non-urgent — defer list update (can be interrupted)
    startTransition(() => {
      setResults(filterItems(val));
    });
  };

  return (
    <>
      <input value={input} onChange={handleChange} />
      {/* Show stale results with opacity while new results compute */}
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        {results.map(r => <ResultRow key={r.id} item={r} />)}
      </div>
    </>
  );
}
useDeferredValue
import { useDeferredValue } from 'react';

function SearchPage({ query }) {  // query comes from parent (prop)
  // Deferred copy — lags behind query intentionally
  const deferredQuery = useDeferredValue(query);

  // isStale = true while deferred lags behind
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      {/* ExpensiveList re-renders with deferredQuery, not the latest query */}
      <ExpensiveList query={deferredQuery} />
    </div>
  );
}

// ── Key difference ──
// startTransition: you control WHEN the update fires (wrap the setter)
// useDeferredValue: you control WHAT value the component sees (wrap the value)
// Use startTransition when you own the state setter
// Use useDeferredValue when you receive the value via props
10
Core Web Vitals — what they are and how React impacts each one.
Advanced Web Vitals
MetricMeasuresGood thresholdReact impact
LCP
Largest Contentful Paint
How fast the main content loads< 2.5sLarge JS bundles delay LCP. Fix: code splitting, SSR/SSG, preloading critical resources
INP
Interaction to Next Paint
Responsiveness to clicks/taps< 200msLong render cycles block INP. Fix: startTransition, virtualisation, debouncing, Web Workers
CLS
Cumulative Layout Shift
Visual stability — elements jumping around< 0.1Dynamic content without reserved space. Fix: skeleton screens, fixed dimensions on images/iframes
Web Vitals in React
// ── web-vitals library (from Google) ──
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics({ name, value, rating, id }) {
  fetch('/analytics', {
    method: 'POST',
    body: JSON.stringify({ name, value, rating, id })
  });
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

// ── reportWebVitals — generated by CRA/Vite ──
// index.js:
reportWebVitals(console.log); // logs metrics to console during dev

// ── Performance.mark for custom timing ──
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');
performance.measure('fetch-duration', 'fetch-start', 'fetch-end');
// Visible in DevTools → Performance → Timings lane
💬 Interview Tip

Connect React decisions to real metrics: "I use code splitting to improve LCP because it reduces the main-thread blocking time before the browser can paint the largest element. For INP, I use startTransition to keep input handlers fast while deferring expensive list re-renders." This shows you think in user-visible outcomes, not just React internals.

📡
Context API In Depth
When to use context, performance pitfalls, and patterns to avoid re-render explosions
1
When should you reach for Context vs prop drilling vs external state?
Easy Context API
ScenarioBest choiceWhy
1–2 levels deepPropsSimplest, explicit data flow, easy to trace
3+ levels, low-change dataContextAuth, theme, locale — read often, update rarely
3+ levels, high-change dataExternal store (Zustand/Redux)Context re-renders all consumers on every change
Server/async data (fetch results)React Query / SWRBuilt-in caching, background refresh, deduplication
Complex local state (wizard, cart)useReducer + ContextPredictable transitions, single source of truth

Prop drilling is explicit — you can trace data flow with grep. It becomes a problem when intermediary components receive props they don't use, only to pass them down. That's the signal to reach for Context or component composition.

Composition over drilling
// ❌ Prop drilling — Button doesn't care about user, just passes it down
<App user={user} />
  <Layout user={user} />     // Layout doesn't use user
    <Header user={user} />   // Header doesn't use user
      <Avatar user={user} /> // Finally used here

// ✅ Composition — pass Avatar as a prop (children or named slot)
function App() {
  return <Layout header={<Header right={<Avatar user={user} />} />} />;
}
// Layout and Header never see user — zero coupling

// ✅ Context — for truly global data (theme, auth, i18n)
const UserCtx = createContext(null);
function App() {
  return (
    <UserCtx.Provider value={user}>
      <Layout />  // No user prop needed anywhere
    </UserCtx.Provider>
  );
}
function Avatar() {
  const user = useContext(UserCtx); // reads directly
  return <img src={user.avatar} />;
}
Context is not a performance tool
Context solves the ergonomics of deep prop threading. It does not make state sharing faster — in fact it can be slower for high-frequency updates (like mouse position) because every consumer re-renders. Use it for low-frequency global data; reach for a dedicated store for high-frequency updates.
2
How do you prevent Context from causing unnecessary re-renders across a large component tree?
Medium Context Performance

React re-renders every component that calls useContext(Ctx) whenever the Provider's value prop changes reference. If you pass an object literal directly, it's a new reference on every parent render — so all consumers re-render even when data is identical.

The object literal trap
// ❌ New object every render → all consumers re-render on every App render
function App() {
  const [user, setUser] = useState(null);
  return (
    <AuthCtx.Provider value={{ user, setUser }}>  // ← new {} each render!
      <Router />
    </AuthCtx.Provider>
  );
}

// ✅ Fix 1: Memoize the value object
function App() {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  // Now value only changes reference when user changes — not on every App render
  return <AuthCtx.Provider value={value}><Router /></AuthCtx.Provider>;
}
Split state and dispatch
// Split context into what changes (state) and what doesn't (dispatch)
// dispatch from useReducer is always the same reference — safe to share standalone

const CartStateCtx  = createContext(null); // changes when cart updates
const CartDispatchCtx = createContext(null); // stable — never changes

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, []);
  return (
    <CartStateCtx.Provider value={state}>
      <CartDispatchCtx.Provider value={dispatch}>
        {children}
      </CartDispatchCtx.Provider>
    </CartStateCtx.Provider>
  );
}

// AddToCartButton only reads dispatch → NEVER re-renders when cart state changes
function AddToCartButton({ item }) {
  const dispatch = useContext(CartDispatchCtx); // stable ref
  return <button onClick={() => dispatch({ type: 'ADD', item })}>Add</button>;
}

// CartBadge reads state → re-renders only when cart changes
function CartBadge() {
  const cart = useContext(CartStateCtx);
  return <span>{cart.length}</span>;
}
Per-feature contexts
// Instead of one giant AppContext, create narrow contexts
const ThemeCtx    = createContext('light'); // changes ~never
const UserCtx     = createContext(null);    // changes on login/logout
const NotifCtx    = createContext([]);      // changes frequently

// Components subscribe only to what they need
// A new notification won't re-render the entire app — only NotifCtx consumers

// Wrap in a single AppProviders component to avoid nesting hell:
function AppProviders({ children }) {
  return (
    <ThemeCtx.Provider value={'dark'}>
      <UserProvider>
        <NotifProvider>
          {children}
        </NotifProvider>
      </UserProvider>
    </ThemeCtx.Provider>
  );
}
Interview tip: the three-fix checklist
When asked "how do you optimise Context?", give all three: (1) memoize the value with useMemo, (2) split state from dispatch — dispatch is always stable, (3) split by update frequency — granular contexts so unrelated consumers don't re-render together.
🏦
useReducer + Context as Lightweight Redux
Building scalable state with reducers, actions, and the provider pattern
3
When should you use useReducer instead of useState? What makes a reducer state machine powerful?
Easy useReducer

useState is like a variable — you set it directly. useReducer is like a traffic light controller — you send it an event ("NEXT_PHASE") and the controller decides the next state based on the current state. You never update state directly; you describe what happened and the reducer figures out what should be next.

Use useState when…Use useReducer when…
Simple scalar: boolean, string, numberMultiple related values that update together
Next state doesn't depend on current stateNext state depends on current state (toggle, counter)
One component owns the stateState is shared via Context across many components
Few transitions (2–3 ways to update)Complex transitions with many action types
Simple logicLogic needs to be tested in isolation (pure function)
Fetch state machine
// State shape — all related fields together
const initialState = {
  status: 'idle',   // 'idle' | 'loading' | 'success' | 'error'
  data: null,
  error: null,
};

// Reducer: pure function (state, action) → new state
function fetchReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, status: 'loading', error: null };
    case 'FETCH_SUCCESS':
      return { status: 'success', data: action.payload, error: null };
    case 'FETCH_ERROR':
      return { status: 'error', data: null, error: action.payload };
    default:
      return state;
  }
}

function UserProfile({ userId }) {
  const [state, dispatch] = useReducer(fetchReducer, initialState);

  useEffect(() => {
    dispatch({ type: 'FETCH_START' });
    fetchUser(userId)
      .then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
      .catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
  }, [userId]);

  if (state.status === 'loading') return <Spinner />;
  if (state.status === 'error')   return <Error msg={state.error} />;
  if (state.status === 'success') return <Profile data={state.data} />;
  return <p>Ready</p>;
}
Key advantage: impossible states become impossible
With useState you could have loading: true, error: 'Something failed' simultaneously — a contradiction. With a reducer, the state machine only allows valid transitions. status: 'loading' means error is null, always.
4
Walk through building a full cart feature using useReducer + Context with separate state and dispatch contexts.
Advanced Provider Pattern

Separate state and dispatch into two contexts. Components that only dispatch (buttons) subscribe to the stable dispatch context — they never re-render when cart items change. Components that read data (badge, total) subscribe to state context only.

cart/cartContext.js — full implementation
import { createContext, useContext, useReducer, useMemo } from 'react';

// 1. Create separate contexts
const CartStateCtx    = createContext(null);
const CartDispatchCtx = createContext(null);

// 2. Initial state
const initial = { items: [], coupon: null };

// 3. Pure reducer — easy to unit test
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD': {
      const existing = state.items.find(i => i.id === action.item.id);
      if (existing) {
        return {
          ...state,
          items: state.items.map(i =>
            i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
          ),
        };
      }
      return { ...state, items: [...state.items, { ...action.item, qty: 1 }] };
    }
    case 'REMOVE':
      return { ...state, items: state.items.filter(i => i.id !== action.id) };
    case 'CLEAR':
      return initial;
    case 'APPLY_COUPON':
      return { ...state, coupon: action.code };
    default: return state;
  }
}

// 4. Provider — dispatch is always stable (no useMemo needed for it)
export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initial);
  // Memoize state to avoid inline object creation
  const cartState = useMemo(() => state, [state]);
  return (
    <CartStateCtx.Provider value={cartState}>
      <CartDispatchCtx.Provider value={dispatch}>
        {children}
      </CartDispatchCtx.Provider>
    </CartStateCtx.Provider>
  );
}

// 5. Custom hooks — throw if used outside provider
export function useCartState() {
  const ctx = useContext(CartStateCtx);
  if (!ctx) throw new Error('useCartState must be inside CartProvider');
  return ctx;
}
export function useCartDispatch() {
  const ctx = useContext(CartDispatchCtx);
  if (!ctx) throw new Error('useCartDispatch must be inside CartProvider');
  return ctx;
}

// 6. Selector hook — derived state computed close to the consumer
export function useCartTotal() {
  const { items } = useCartState();
  return useMemo(
    () => items.reduce((sum, i) => sum + i.price * i.qty, 0),
    [items]
  );
}

// 7. Usage: components pick what they need
function CartBadge() {
  const { items } = useCartState();        // re-renders on cart change
  return <span>{items.length}</span>;
}
function AddButton({ item }) {
  const dispatch = useCartDispatch();     // NEVER re-renders from cart state
  return (
    <button onClick={() => dispatch({ type: 'ADD', item })}>Add</button>
  );
}
Why dispatch is always stable
React guarantees that the dispatch function returned by useReducer has a stable reference across renders — it never changes. This makes it safe to put in a separate context without memoization, and safe to use in useEffect dependency arrays without including it.
🗄️
Redux Toolkit (RTK)
Modern Redux — slices, thunks, and why RTK fixes vanilla Redux's pain points
5
What problems does Redux solve? Why was Redux Toolkit (RTK) created, and what does it fix?
Easy Redux Toolkit

Redux solves three problems: (1) a predictable single source of truth for application state, (2) time-travel debugging via action history, (3) state sharing across distant component trees without prop drilling. The pattern — store, actions, reducers — makes state transitions explicit and traceable.

Vanilla Redux problemRTK solution
Boilerplate: action types + action creators + reducer = 3 files for one featurecreateSlice generates all three from one object
Immutability: must spread everything manuallyImmer built in — write mutating code, RTK converts to immutable
Async: need redux-thunk + custom middleware setupcreateAsyncThunk + auto pending/fulfilled/rejected actions
Store setup: middleware, devtools, enhancersconfigureStore sets up everything with sensible defaults
Selector memoization: need reselect separatelyRTK Query includes built-in normalized cache; can use built-in createSelector
useReducer + ContextRedux Toolkit
Bundle sizeZero (built-in)~11kb (redux + RTK)
DevToolsNone built-inExcellent time-travel debugging
AsyncDIY (useEffect + dispatch)createAsyncThunk + RTK Query
MiddlewareNot supportedFirst-class (logging, analytics)
ScaleGood for small-medium appsProven at large enterprise scale
Learning curveLowMedium (concepts + RTK API)
6
Show the complete RTK pattern: createSlice, createAsyncThunk, configureStore, and useSelector/useDispatch.
Advanced RTK Slice Pattern
features/cart/cartSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// Async thunk — handles pending/fulfilled/rejected automatically
export const fetchProducts = createAsyncThunk(
  'cart/fetchProducts',                    // action type prefix
  async (categoryId, { rejectWithValue }) => {
    try {
      const res = await fetch(`/api/products?cat=${categoryId}`);
      return res.json();
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);

const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    products: [],
    loading: false,
    error: null,
  },
  reducers: {
    // Immer lets you write "mutating" code — RTK converts to immutable updates
    addItem(state, action) {
      const existing = state.items.find(i => i.id === action.payload.id);
      if (existing) {
        existing.qty++;            // direct mutation — Immer handles immutability
      } else {
        state.items.push({ ...action.payload, qty: 1 });
      }
    },
    removeItem(state, action) {
      state.items = state.items.filter(i => i.id !== action.payload);
    },
    clearCart(state) {
      state.items = [];
    },
  },
  extraReducers(builder) {
    // Handles the 3 auto-generated actions from createAsyncThunk
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.loading = true; state.error = null;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.loading = false; state.products = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.loading = false; state.error = action.payload;
      });
  },
});

export const { addItem, removeItem, clearCart } = cartSlice.actions;
export default cartSlice.reducer;

// Selectors — co-located with the slice
export const selectItems   = (state) => state.cart.items;
export const selectTotal   = (state) =>
  state.cart.items.reduce((s, i) => s + i.price * i.qty, 0);
app/store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from '../features/cart/cartSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    // add other slices here
  },
  // configureStore auto-adds redux-thunk middleware + devtools
});

// TypeScript: export types derived from store
export type RootState   = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
CartBadge.jsx
import { useSelector, useDispatch } from 'react-redux';
import { addItem, selectItems, fetchProducts } from './cartSlice';

function CartBadge() {
  // useSelector subscribes to only this slice of state
  const items = useSelector(selectItems);
  return <span>{items.length}</span>;
}

function ProductCard({ product }) {
  const dispatch = useDispatch();
  return (
    <button onClick={() => dispatch(addItem(product))}>Add</button>
  );
}

function ProductList() {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(fetchProducts('electronics')); // fires the async thunk
  }, [dispatch]);
  // ...
}
Modern State Libraries & Derived State Patterns
Zustand, server state (React Query/SWR), derived state, and the decision framework
7
What is Zustand? How does it compare to Redux and Context? Show its core pattern.
Medium Zustand

Zustand (German for "state") wraps a vanilla JS store in a React hook. No Provider. No action types. No reducers. You define state and updaters together in one create() call and subscribe to exactly the parts you need via a selector — so components only re-render when the slices they pick change.

ContextRedux ToolkitZustand
BoilerplateLowMediumMinimal
Provider neededYesYes (<Provider>)No
Re-render controlAll consumers re-renderSelector-basedSelector-based
DevToolsNoneExcellentGood (via middleware)
BundleZero~11kb~1kb
Best forInfrequent global dataLarge apps, teamsSimple–medium apps
stores/cartStore.js
import { create } from 'zustand';

const useCartStore = create((set, get) => ({
  items: [],

  addItem: (item) =>
    set((state) => {
      const exists = state.items.find(i => i.id === item.id);
      return {
        items: exists
          ? state.items.map(i => i.id === item.id ? { ...i, qty: i.qty+1 } : i)
          : [...state.items, { ...item, qty: 1 }],
      };
    }),

  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter(i => i.id !== id),
    })),

  clearCart: () => set({ items: [] }),

  // Derived value as a getter — computed on every call
  getTotal: () =>
    get().items.reduce((s, i) => s + i.price * i.qty, 0),
}));

// Usage — subscribe to a slice with a selector
// Component only re-renders when items.length changes, not on every state update
function CartBadge() {
  const count = useCartStore((s) => s.items.length);
  return <span>{count}</span>;
}

function AddButton({ item }) {
  const addItem = useCartStore((s) => s.addItem);
  // Actions are stable — no re-renders from state changes
  return <button onClick={() => addItem(item)}>Add</button>;
}

// Middleware: persist to localStorage
import { persist } from 'zustand/middleware';
const useCartStore = create(persist(
  (set) => ({ /* ... store definition ... */ }),
  { name: 'cart-storage' }  // localStorage key
));
Zustand's killer feature: outside React
Because Zustand is a vanilla JS store with a React adapter, you can read and update state outside of components — useful for WebSockets, global event handlers, or programmatic navigation: useCartStore.getState().addItem(item) and useCartStore.subscribe(state => console.log(state)).
8
What is "server state" vs "client state"? Why do libraries like React Query / TanStack Query exist?
Medium Server State
Client StateServer State
OwnerYour appThe server / database
Source of truthIn-memoryRemote — can be stale at any time
ExamplesModal open, form input, theme, selected tabUser profile, product list, order history
ChallengesSharing, avoiding prop drillingCaching, invalidation, background refresh, deduplication, loading/error states
Best tooluseState, useReducer, Zustand, RTKReact Query (TanStack), SWR, RTK Query
Manual data fetching — all the boilerplate
// Without React Query — you own all this complexity:
function ProductList() {
  const [data,    setData]    = useState(null);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch('/api/products', { signal: controller.signal })
      .then(r => r.json())
      .then(setData)
      .catch(e => { if (!e.name === 'AbortError') setError(e); })
      .finally(() => setLoading(false));
    return () => controller.abort();
  }, []);
  // No caching — every mount re-fetches from scratch
  // No background refresh — stale data while user is on page
  // No deduplication — two components fetch same endpoint twice
  // No optimistic updates — complex to implement manually
}
Same task with React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Reading data
function ProductList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['products'],   // cache key — same key = same cache entry
    queryFn: () => fetch('/api/products').then(r => r.json()),
    staleTime: 5 * 60 * 1000, // cached for 5 min — won't refetch on remount
  });
  if (isLoading) return <Spinner />;
  if (error)     return <Error />;
  return <ul>{data.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

// Writing data (mutations)
function AddProductForm() {
  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: (newProduct) =>
      fetch('/api/products', {
        method: 'POST',
        body: JSON.stringify(newProduct),
      }),
    onSuccess: () => {
      // Invalidate cache → triggers background refetch
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });
  // ...
}

// React Query handles: caching, deduplication, background refresh,
// refetch on window focus, retry on error, request cancellation
Don't put server state in Redux
Storing fetched API data in Redux creates an ad-hoc cache with none of the cache management features (staleness, invalidation, background updates). Use React Query or SWR for server state; use Redux/Zustand/Context only for true client state. Many Redux codebases could remove 60–70% of their store once server state is extracted.
9
What is "derived state" and where should you compute it? What are the common anti-patterns?
Medium Derived State

If a value can be calculated from existing state or props, it's derived state — you should not store it separately. Storing derived state in its own useState creates a synchronization problem: the copy and the source can get out of sync, leading to bugs and extra re-renders.

The sync bug
// ❌ Stale copy — if userProp changes, localUser won't update!
function UserCard({ user }) {
  const [localUser, setLocalUser] = useState(user); // ← mirror
  return <p>{localUser.name}</p>;
  // If parent passes new user, this still shows the old one
}

// ✅ Just use the prop directly
function UserCard({ user }) {
  return <p>{user.name}</p>;
}
Choosing the right derivation spot
// Scenario: filtered + sorted product list

// Option 1: derive during render (fine for cheap computations)
function ProductList({ products, filter }) {
  const visible = products.filter(p => p.category === filter); // inline
  return visible.map(p => <Product key={p.id} item={p} />);
}

// Option 2: useMemo (for expensive computation + stable reference)
function ProductList({ products, filter, sort }) {
  const processed = useMemo(() =>
    [...products]
      .filter(p => p.category === filter)
      .sort((a, b) => a.price - b.price),
    [products, filter]                       // only recompute if these change
  );
  return processed.map(p => <Product key={p.id} item={p} />);
}

// Option 3: selector in Redux/Zustand (memoized, reusable across components)
import { createSelector } from 'reselect';

const selectVisibleProducts = createSelector(
  [(state) => state.products.all, (state) => state.products.filter],
  (products, filter) => products.filter(p => p.category === filter)
  // memoized — returns cached result until products or filter change
);
Common over-engineering pattern
// ❌ Syncing derived state with useEffect — two renders, risk of loops
const [filtered, setFiltered] = useState([]);
useEffect(() => {
  setFiltered(products.filter(p => p.active));
}, [products]);  // extra render every time products changes

// ✅ Derive inline — single render, no extra state
const filtered = products.filter(p => p.active); // during render
10
Walk me through the complete state management decision framework. How do you choose the right tool for any situation?
Advanced Decision Framework
State taxonomy
// Ask: "Who owns this data?"

// 🖥 Server state — lives on a server, fetched asynchronously
// → React Query / TanStack Query / SWR / RTK Query
// Examples: user list, products, orders, comments

// 🌐 Global client state — shared across many components
// → Zustand / Redux Toolkit / Context
// Examples: auth user, theme, shopping cart, language

// 🧩 Feature/module state — shared within a feature subtree
// → useReducer + Context (scoped Provider)
// Examples: multi-step wizard state, data table (sort+filter+page)

// 🏠 Local component state — owned by one component
// → useState / useReducer
// Examples: form field, modal open, hover state, toggle

// 📌 URL state — shareable, bookmarkable, browser-back-compatible
// → URL search params (useSearchParams in React Router v6)
// Examples: search query, filters, pagination, selected tab
QuestionYes → useNo → continue
Is it fetched from an API?React Query / SWR
Should it be bookmarkable / in URL?URLSearchParams
Is it used by only one component?useState / useReducer
Is it used within one feature subtree?useReducer + Context (scoped)
Is it global but changes rarely?Context + useMemo
Is it global and changes often?Zustand (small) / RTK (large)
Needs time-travel debugging / middleware?Redux ToolkitZustand
Layered state architecture
// Recommended stack — each layer has a clear job

// Layer 1: Server state → TanStack Query
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });

// Layer 2: Global UI state → Zustand (or RTK for large teams)
const theme   = useThemeStore((s) => s.theme);
const isOpen  = useModalStore((s) => s.isOpen);

// Layer 3: Feature state → useReducer + Context (scoped Provider)
const wizardState = useWizardState(); // custom hook over scoped context

// Layer 4: Local state → useState
const [isEditing, setIsEditing] = useState(false);

// Layer 5: URL state → useSearchParams (React Router)
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';
The one-sentence answer for interviews
"I categorise state into server, global UI, feature-scoped, and local — use React Query for server state, Zustand/RTK for global UI state, scoped useReducer+Context for feature state, useState for local, and URL params for bookmarkable UI state."
⚙️
React 18 Concurrent Features
Interruptible rendering, the scheduler, useTransition, and Suspense for data
1
What is "concurrent mode" and how does React 18's scheduler change the rendering model?
Medium Concurrent React

Before React 18, rendering was like a phone call you couldn't interrupt — once React started rendering a tree it would block the main thread until done. Concurrent React is like a phone with call-waiting: React can start rendering, pause when something more urgent arrives (a user click, an animation frame), handle the urgent work, then resume.

Legacy (synchronous)Concurrent (React 18)
RenderingBlocking — can't be interruptedInterruptible — can pause and resume
Work priorityAll updates have equal priorityUrgent vs non-urgent lanes (scheduler)
UI during long renderFreezes / jankyStays responsive — urgent work runs first
Opt-in?Default in React <18Opt-in via createRoot — legacy root still works
React 18 root setup
// React 17 — legacy blocking mode
ReactDOM.render(<App />, document.getElementById('root'));

// React 18 — concurrent mode (createRoot)
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);
// All concurrent features (useTransition, useDeferredValue, Suspense for data)
// are unlocked by createRoot — they're no-ops on legacy ReactDOM.render
Scheduler priority model
// React 18 scheduler uses "lanes" — a bitmask system for priority
// Priority order (highest → lowest):
//   1. SyncLane        — discrete user input (click, keypress, focus)
//   2. InputContinuous — continuous input (drag, scroll, wheel)
//   3. DefaultLane     — data fetching, setTimeout callbacks
//   4. TransitionLane  — startTransition / useTransition (your hint)
//   5. IdleLane        — prefetching, background work

// Example: user types in a search box while a list is rendering
// Without concurrent: list render blocks the keystroke → UI freezes
// With concurrent + startTransition:
function Search() {
  const [input, setInput]     = useState('');
  const [query, setQuery]     = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    setInput(e.target.value);        // Sync — input updates immediately
    startTransition(() => {
      setQuery(e.target.value);      // Transition — can be interrupted
      // If user types again before list re-render finishes,
      // React abandons this transition and starts fresh
    });
  }
  return (
    <>
      <input value={input} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultsList query={query} />  // expensive — renders thousands of items
    </>
  );
}
Concurrent React doesn't make renders faster
It makes the app feel faster by keeping urgent work (user input) responsive while deprioritising non-urgent work (big list re-render). The total CPU time is the same — you're just changing which work runs first.
2
What is Suspense for data fetching? How does it work, and how does it differ from loading state pattern?
Advanced Suspense
isLoading patternSuspense
Where loading livesInside the component (if isLoading…)Outside — in the component tree (boundary)
CoordinationEach component handles its own spinnerParent boundary shows one fallback for all children
Code complexityEvery component has loading/error stateComponent assumes data is ready — clean
Works withAny async patternMust use Suspense-compatible libraries (React Query, Next.js, use())

When a component needs data that isn't ready, it throws a Promise. React catches it, renders the nearest <Suspense fallback>, and when the Promise resolves, re-renders the component. You never write the throw — the data library does it for you.

Suspense for data (React Query + use())
// Pattern 1: React Query with suspense: true
function UserProfile() {
  // No isLoading check — component only renders when data is ready
  const { data: user } = useSuspenseQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
  });
  return <h1>{user.name}</h1>;  // data is always defined here
}

// Pattern 2: React 19 use() hook — reads a promise or context
function UserProfile({ userPromise }) {
  const user = use(userPromise);  // suspends until promise resolves
  return <h1>{user.name}</h1>;
}

// Parent — wraps with Suspense + ErrorBoundary
function App() {
  return (
    <ErrorBoundary fallback={<p>Error loading user</p>}>
      <Suspense fallback={<Spinner />}>
        <UserProfile />  // shows Spinner until ready
        <UserPosts />    // both suspend → one spinner for both
      </Suspense>
    </ErrorBoundary>
  );
}

// Nested Suspense — waterfall vs parallel
function Dashboard() {
  return (
    <Suspense fallback={<PageSkeleton />}>       // outer: page-level
      <Header />
      <Suspense fallback={<CardSkeleton />}>   // inner: card-level
        <MetricsCard />
      </Suspense>
    </Suspense>
  );
  // Header resolves fast → shows immediately
  // MetricsCard still loading → shows CardSkeleton, not PageSkeleton
}
Suspense + startTransition = non-blocking navigation
Wrap route changes in startTransition so React keeps showing the current page until the next page's data is ready, rather than flashing a loading skeleton on every navigation. This is the "content-first" pattern used by Next.js App Router.
🌐
React Server Components (RSC)
The new component model — what runs where, the "use client" boundary, and the RSC mental model
3
What are React Server Components? How do they differ from SSR? What are their constraints?
Medium Server Components
SSR (Server-Side Rendering)React Server Components
What runs on server?Everything (full render to HTML)Only Server Components
Client hydration?Yes — full JS bundle re-runsOnly for Client Components ("use client")
JS sent to browser?Full component codeServer Component code is never sent
Can access DB/FS?Only in getServerSideProps (Next.js)Yes — directly in the component
State / hooks?After hydration on clientNever — Server Components have no state
Re-renders?Client re-renders after hydrationServer re-renders on server — no client update
Server Component capabilities
// ✅ Server Component CAN do:
async function ProductPage({ id }) {
  // 1. Direct DB access — no API layer needed
  const product = await db.product.findById(id);

  // 2. Read filesystem
  const readme = await fs.readFile('./docs.md', 'utf8');

  // 3. Access secrets (env vars) — never exposed to client
  const price = await stripe.getPrice(product.priceId);

  // 4. Import heavy libs — NOT sent to browser bundle
  const { marked } = await import('marked'); // stays on server

  return (
    <div>
      <h1>{product.name}</h1>
      <AddToCartButton id={product.id} />  {/* Client Component */}
    </div>
  );
}

// ❌ Server Component CANNOT do:
async function Bad() {
  useState(0);           // ❌ no hooks
  useEffect(() => {});   // ❌ no effects
  onClick = () => {};   // ❌ no event handlers
  return <button onClick={fn}>Click</button>; // ❌ event handlers need client
}
Marking a Client Component
'use client'; // This file and everything it imports runs on the client

// AddToCartButton.jsx — needs onClick state → must be Client Component
export function AddToCartButton({ id }) {
  const [added, setAdded] = useState(false);
  return (
    <button onClick={() => { addToCart(id); setAdded(true); }}>
      {added ? 'Added!' : 'Add to Cart'}
    </button>
  );
}

// ✅ A Server Component CAN render a Client Component:
// ProductPage (Server) → renders AddToCartButton (Client) ✅

// ❌ A Client Component CANNOT import a Server Component:
// 'use client' file that imports a file doing DB queries → error

// ✅ But a Client Component CAN receive Server Components as children:
function ClientShell({ children }) {   // 'use client'
  return <div className={theme}>{children}</div>;
}
// In a Server Component:
<ClientShell>
  <ServerFetchedContent />  {/* passes as children — allowed! */}
</ClientShell>
Key mental model: "Server by default, Client by exception"
In Next.js App Router all components are Server Components by default. Add 'use client' only when you need interactivity (state, effects, browser APIs, event handlers). Push the boundary down as far as possible — keep expensive data-fetching on the server, isolate interactivity in small leaf components.
4
What are Server Actions? How do forms and mutations work in the RSC model (Next.js App Router)?
Advanced Server Actions

Server Actions are async functions marked with 'use server' that run on the server even when called from a Client Component. They replace the need for a separate API route for form submissions and mutations — the function is the endpoint.

Server Action — form mutation
// app/actions.ts — Server Action (can access DB directly)
'use server';

export async function createPost(formData: FormData) {
  const title   = formData.get('title') as string;
  const content = formData.get('content') as string;

  if (!title) return { error: 'Title is required' };

  await db.post.create({ data: { title, content } });
  revalidatePath('/posts');  // invalidate Next.js cache for /posts
  redirect('/posts');        // redirect after success
}

// Option 1: Use in a Server Component form (no JS required — progressive enhancement)
export default function NewPostPage() {
  return (
    <form action={createPost}>    {/* action prop accepts Server Action */}
      <input name="title" />
      <textarea name="content" />
      <button>Publish</button>
    </form>
  );
}

// Option 2: In a Client Component with useActionState (React 19)
'use client';
import { useActionState } from 'react';

function PostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);
  return (
    <form action={formAction}>
      {state?.error && <p className="error">{state.error}</p>}
      <input name="title" />
      <button disabled={isPending}>
        {isPending ? 'Publishing…' : 'Publish'}
      </button>
    </form>
  );
}
Why Server Actions are significant
Before Server Actions, every mutation needed: (1) an API route, (2) a fetch call from the client, (3) error handling on both sides, (4) cache invalidation. Server Actions collapse this into one async function — the form action attribute works without JavaScript (progressive enhancement), making apps resilient and accessible.
🗺️
React Router v6
Nested routes, loaders, lazy-loaded routes, and navigation patterns
5
Explain React Router v6 core concepts: nested routes, Outlet, loaders, and useNavigate.
Medium React Router v6
Featurev5v6
Route nestingManually compose <Route> everywhereDeclarative nested routes + <Outlet />
NavigationuseHistory().push()useNavigate()('/path')
Route paramsuseParams() (same)useParams() (same)
Data loadingComponent did own fetchingloader function (parallel, route-level)
MatchingFirst-match winsBest-match wins (no order dependency)
Relative linksBroken in v5Fixed — relative to current route
React Router v6 setup
import { createBrowserRouter, RouterProvider, Outlet, useNavigate, useParams, useLoaderData } from 'react-router-dom';

// Route config — tree mirrors URL structure
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,   // renders Outlet for child routes
    errorElement: <ErrorPage />, // catches loader/render errors
    children: [
      { index: true, element: <Home /> },   // matches "/"
      {
        path: 'users',
        element: <UsersLayout />,
        loader: usersLoader,               // runs before render
        children: [
          { index: true, element: <UsersList /> },
          {
            path: ':userId',
            element: <UserDetail />,
            loader: userDetailLoader,
          },
        ],
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

// RootLayout — Outlet renders the matched child route
function RootLayout() {
  return (
    <div>
      <Nav />
      <main>
        <Outlet />  {/* child route renders here */}
      </main>
    </div>
  );
}

// Loader — runs parallel to sibling loaders before render (no waterfall)
async function userDetailLoader({ params }) {
  const user = await fetchUser(params.userId);
  if (!user) throw new Response('Not Found', { status: 404 });
  return user; // available via useLoaderData()
}

// Component — reads loader data and navigates
function UserDetail() {
  const user     = useLoaderData(); // data from loader above
  const navigate = useNavigate();

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => navigate('..')}>Back</button>
      <button onClick={() => navigate(`/users/${user.id}/edit`)}>Edit</button>
    </div>
  );
}
Route-level code splitting
// lazy() + dynamic import = separate chunk per route
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        path: 'dashboard',
        element: (
          <Suspense fallback={<PageSpinner />}>
            <Dashboard />
          </Suspense>
        ),
      },
    ],
  },
]);

// Or use React Router's built-in lazy (v6.9+)
{
  path: 'dashboard',
  lazy: async () => ({
    Component: (await import('./pages/Dashboard')).default,
    loader:    (await import('./pages/Dashboard')).loader,
  }),
}
🧪
Testing with React Testing Library
RTL philosophy, queries, async interactions, MSW, and testing hooks
6
What is React Testing Library's testing philosophy? How does it differ from Enzyme?
Easy RTL Philosophy

"The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds

RTL renders components in a real DOM (via jsdom) and queries the output the way a user would — by visible text, accessible role, label, or placeholder — not by CSS class names or component instance internals. Tests that break when you rename a CSS class or restructure a component aren't testing behavior; they're testing implementation.

EnzymeReact Testing Library
PhilosophyTest implementationTest user behavior
Finds elements byComponent name, props, stateText, role, label, testid
Shallow renderingYes (isolates components)No — always full render
Accesses internalsYes: wrapper.state(), wrapper.instance()No — black-box
Refactor safetyLow — implementation changes break testsHigh — only behavior changes break tests
React 18 supportPoor (abandoned)First-class
Good test vs bad test
// ❌ Testing implementation (brittle)
it('sets isOpen to true on click', () => {
  const wrapper = shallow(<Dropdown />);
  wrapper.find('button').simulate('click');
  expect(wrapper.state('isOpen')).toBe(true); // ← tests internal state
});
// If you rename isOpen → expanded, test breaks even though behavior is the same

// ✅ Testing behavior (resilient)
it('shows menu items when trigger is clicked', async () => {
  render(<Dropdown options={['Edit', 'Delete']} />);

  // Closed initially
  expect(screen.queryByRole('menuitem')).not.toBeInTheDocument();

  // User clicks trigger
  await userEvent.click(screen.getByRole('button'));

  // Menu items visible
  expect(screen.getByText('Edit')).toBeInTheDocument();
  expect(screen.getByText('Delete')).toBeInTheDocument();
});
// Works regardless of whether the state is called isOpen or expanded
7
Explain RTL's query types: getBy, queryBy, findBy. What is the query priority order?
Easy RTL Queries
VariantWhen element is missingWhen to use
getBy*Throws error immediatelyElement must be in the DOM right now
queryBy*Returns nullAsserting element is not present
findBy*Rejects (async) after timeoutElement appears after async work (fetch, animation)
getAllBy*Throws if none foundMultiple elements, all must exist
queryAllBy*Returns empty arrayMultiple — might not exist
findAllBy*Rejects after timeoutMultiple async elements
PriorityQueryWhy
1 (best)getByRoleMatches ARIA role + accessible name — mirrors screen reader experience
2getByLabelTextForms — finds input by its <label>
3getByPlaceholderTextWhen no label exists
4getByTextNon-interactive elements — divs, paragraphs
5getByDisplayValueCurrent value of form element
6getByAltTextImages
7getByTitletitle attribute
8 (last)getByTestIdEscape hatch — only when no semantic query works
getByRole examples
// Roles match semantic HTML automatically:
// <button> → role="button"     <input> → role="textbox"
// <a href> → role="link"       <h1-6>  → role="heading"
// <ul>     → role="list"       <table> → role="table"
// <dialog> → role="dialog"     <nav>   → role="navigation"

render(<LoginForm />);

// Finds <input type="text"> or <input> labelled "Username"
screen.getByRole('textbox', { name: /username/i });

// Finds <button>Submit</button>
screen.getByRole('button', { name: /submit/i });

// Find by heading level
screen.getByRole('heading', { level: 1 });

// Using queryBy to assert absence
expect(screen.queryByRole('alert')).not.toBeInTheDocument();

// Using findBy for async appearance
const successMsg = await screen.findByRole('status'); // waits up to 1000ms
expect(successMsg).toHaveTextContent('Saved!');
8
How do you test async interactions? Show userEvent, waitFor, and mocking API calls with MSW.
Medium Async Testing
fireEvent@testing-library/user-event
SimulatesOne DOM event at a timeFull user interaction sequence (focus → hover → keydown → keyup → click…)
TypingfireEvent.change(input, {target:{value:'a'}})await userEvent.type(input, 'hello')
AsyncSynchronousReturns a Promise — must await
PreferLow-level, rarely neededDefault choice for all user interactions
userEvent + waitFor example
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

it('submits form and shows success message', async () => {
  const user = userEvent.setup(); // v14+: create instance first
  render(<ContactForm />);

  // Simulate typing — fires focus, keydown, keypress, input, keyup events
  await user.type(screen.getByLabelText(/name/i), 'Alice');
  await user.type(screen.getByLabelText(/email/i), 'alice@example.com');

  // Click submit button
  await user.click(screen.getByRole('button', { name: /send/i }));

  // waitFor — retries assertion until it passes (or timeout)
  await waitFor(() => {
    expect(screen.getByText(/message sent/i)).toBeInTheDocument();
  });

  // Or equivalently: findBy (finds + waits in one call)
  await screen.findByText(/message sent/i);
});

MSW intercepts actual HTTP requests at the network level — not by monkey-patching fetch. This means your tests use the real fetch/axios code and only the server response is mocked, giving much more confidence than jest.mock('axios').

MSW v2 setup
// mocks/handlers.ts — define request handlers
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);
  }),
  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: 3, ...body }, { status: 201 });
  }),
];

// mocks/server.ts — Node.js server for Jest/Vitest
import { setupServer } from 'msw/node';
export const server = setupServer(...handlers);

// setupTests.ts — runs before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());  // clean up per-test overrides
afterAll(() => server.close());

// In a test — override handler for error scenario
it('shows error when API fails', async () => {
  server.use(
    http.get('/api/users', () => new HttpResponse(null, { status: 500 }))
  );
  render(<UserList />);
  await screen.findByText(/failed to load/i);
});
9
How do you test custom hooks with renderHook? How do you test components that use Context?
Medium Testing Hooks & Context
Testing a custom hook
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

// The hook under test
function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = useCallback(() => setCount(c => c + 1), []);
  const reset     = useCallback(() => setCount(initial), [initial]);
  return { count, increment, reset };
}

describe('useCounter', () => {
  it('starts at initial value', () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it('increments the count', () => {
    const { result } = renderHook(() => useCounter());
    act(() => {
      result.current.increment(); // must wrap state updates in act()
    });
    expect(result.current.count).toBe(1);
  });

  it('resets to initial', () => {
    const { result } = renderHook(() => useCounter(10));
    act(() => { result.current.increment(); });
    act(() => { result.current.reset(); });
    expect(result.current.count).toBe(10);
  });

  it('updates when initial prop changes (rerender)', () => {
    const { result, rerender } = renderHook((initial) => useCounter(initial), {
      initialProps: 0,
    });
    rerender(100); // simulate parent passing new prop
    act(() => { result.current.reset(); });
    expect(result.current.count).toBe(100);
  });
});
Custom render wrapper
// test-utils.tsx — custom render that wraps with all providers
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function AllProviders({ children }: { children: React.ReactNode }) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } }, // no retries in tests
  });
  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme="light">
        <CartProvider>
          {children}
        </CartProvider>
      </ThemeProvider>
    </QueryClientProvider>
  );
}

const customRender = (ui: ReactElement, options?: RenderOptions) =>
  render(ui, { wrapper: AllProviders, ...options });

// Re-export everything from RTL, override render
export * from '@testing-library/react';
export { customRender as render };

// In tests — import render from test-utils instead of @testing-library/react
import { render, screen } from '../test-utils'; // ← not from RTL directly

it('shows cart count', async () => {
  render(<CartBadge />); // automatically wrapped with CartProvider
  expect(screen.getByText('0')).toBeInTheDocument();
});
10
What are the most common RTL mistakes and anti-patterns? What makes a React test suite maintainable?
Advanced Testing Best Practices
Anti-patterns and fixes
// ❌ 1. Using container.querySelector — back to implementation testing
const { container } = render(<Button />);
container.querySelector('.btn-primary'); // breaks if CSS class renamed
// ✅ Fix: use screen.getByRole('button')

// ❌ 2. Overusing data-testid
screen.getByTestId('submit-btn');
// ✅ Fix: use getByRole('button', { name: /submit/i }) — tests accessibility too

// ❌ 3. Not wrapping state updates in act()
it('...', () => {
  render(<Counter />);
  screen.getByRole('button').click(); // raw .click() bypasses act
  // → "not wrapped in act" warning
});
// ✅ Fix: use userEvent.click() — it handles act() for you

// ❌ 4. Using waitFor incorrectly
await waitFor(() => {
  userEvent.click(button); // ❌ side effects inside waitFor
  expect(screen.getByText('Done')).toBeInTheDocument();
});
// ✅ Fix: fire the event before waitFor, only assert inside it
await user.click(button);
await waitFor(() => expect(screen.getByText('Done')).toBeInTheDocument());

// ❌ 5. Testing every render detail (snapshot abuse)
expect(render(<Button />).container).toMatchSnapshot();
// Snapshots that include the full rendered HTML break on any HTML change,
// even cosmetic — developers start blindly updating them
// ✅ Fix: snapshot only small, stable things (icon SVG paths, not full pages)
PrincipleHow to apply
Test behavior, not implementationQuery by role/text, not CSS class or component prop
One assertion focus per testEach test name = one behavior scenario; split if you're asserting too much
Use a custom render wrapperSingle test-utils.tsx wraps all providers — no repetition in tests
Mock at the network layerMSW over jest.mock('axios') — tests real HTTP code
Avoid unnecessary mockingMock only I/O boundaries (API, timers, randomness) — not internal modules
Keep setup minimalbeforeEach with complex setup = test pollution; inline setup in each test
Accessibility-first queriesIf you can't getByRole, your component may have an accessibility gap
The testing trophy — where to invest
Don't aim for 100% unit test coverage. Follow the Testing Trophy: most tests should be integration tests (render a feature with real hooks + MSW) — they give the most confidence per maintenance cost. Unit tests for pure logic (reducers, utils). E2E (Playwright/Cypress) for critical user journeys only. Avoid excessive unit tests of individual components — they couple your tests to implementation.