Lesson 2 of 8 · 65 min

React + Next.js core and interview questions

Deep React mental model (render/commit, Fiber, keys, hooks, effects, concurrent, React 19) and Next App Router realities (RSC vs client, revalidate layers, hydration, streaming, PPR) plus a 42-question multi-sentence interview battery.

Not create-next-app — the questions that hire

This lesson is the dedicated React mental model + Next.js App Router interview battery. Interviewers do not grade whether you can scaffold a project. They grade whether you can explain re-renders, keys, effects, RSC vs client boundaries, hydration, caching/revalidate, and pick SSR/SSG/ISR/CSR for a product constraint — with proof, not slogans.
Stack reality: React + TypeScript is the lingua franca for product FE interviews. Framework is the vehicle; fundamentals (render, identity, data, boundaries) transfer. Pages Router appears only as a contrast table — App Router is primary. Answer every question in 60–90s: mechanism → production implication → how you prove it (Profiler, Network, bundle analyzer).

React render model: render → commit

React is a UI runtime. On an update it renders (calls your components as pure functions of props/state to produce a description of the UI) then commits (applies DOM changes, runs layout effects, then passive effects). Interviewers open with: “When does this component re-render?” Answer: when its state changes, when its parent re-renders and passes new props (by reference identity), or when a consumed context value changes — unless you interrupt that with memoization that actually matches your data.
Fiber (the reconciler) schedules work in units that can be interrupted during render; commit is synchronous and not discarded. That is why side effects belong in effects or event handlers, never in render body. Strict Mode in development intentionally double-invokes render and remounts effects to surface impure code and missing cleanups.
tsx
1// Purity rule: render must be pure2// BAD: mutate during render3function Bad({ items }: { items: Item[] }) {4	items.sort(...) // mutates props — bugs + concurrent breaks5	return ...6}7// GOOD: derive8function Good({ items }: { items: Item[] }) {9	const sorted = [...items].sort(...)10	return ...11}12// Two-phase model13// RENDER: pure, may be discarded/re-run (Strict Mode, concurrent interrupt)14// COMMIT: mutates DOM, runs useLayoutEffect, then useEffect

Reconciliation and keys

Reconciliation heuristics (legacy.reactjs.org reconciliation docs): different element type ⇒ unmount + remount; same type ⇒ update props in place. Keys tell React which list child is which identity across renders. Stable keys (ids) preserve state; index keys break when the list reorders/inserts. Unstable keys (Math.random(), new object each render) remount every time — state and focus reset, animations restart, inputs lose characters.
tsx
1// Classic bug: unstable key remounts inputs2{todos.map(t => (3	 // remounts every parent render4))}5// Fix: stable identity6{todos.map(t => )}7// Index keys OK only for static lists that never reorder/filter/insert.8// Interview follow-up: “What state is tied to the fiber identity?” — local state, DOM focus.

Hooks, effects, concurrent — justified use

  1. 01useState — local UI/ephemeral. Equality bail-out uses Object.is. Prefer functional updates when next depends on prev.
  2. 02useReducer — multi-field event→state machines; clearer than five related useStates.
  3. 03useRef — mutable, no re-render: DOM, timers, AbortControllers, latest values, “isMounted” style flags.
  4. 04useMemo / useCallback — expensive pure work or stable identity for memoized children — measure first. React Compiler auto-memoizes when enabled.
  5. 05useContext — theme/auth shell; never as a network cache (use Query/RSC).
  6. 06useEffect — sync external systems after paint; cleanup on re-run/unmount. Not for derived state.
  7. 07useLayoutEffect — pre-paint DOM measure/write (tooltip, focus). Overuse hurts INP.
  8. 08useTransition / useDeferredValue / Suspense — keep input responsive; non-urgent heavy UI; fallbacks while suspend.
  9. 09useId — stable SSR/CSR ids for a11y wiring (label/input). Never Math.random() for ids.
  10. 10useSyncExternalStore — subscribe to external stores with consistent getSnapshot (Redux, browser APIs).
  11. 11use / useActionState / useFormStatus (React 19) — read promises/context mid-render; form action state; parent form pending.
tsx
1// Stale closure + concurrent input patterns interviewers probe2useEffect(() => {3	const id = setInterval(() => setCount(c => c + 1), 1000) // functional update4	return () => clearInterval(id)5}, [])6// Controlled input lagging: local text + deferred heavy list7const [text, setText] = useState('')8const deferred = useDeferredValue(text)9//   10// React 18+ batches updates in timeouts/promises too11// Strict Mode double-mounts effects in dev — write idempotent cleanups12// Fetch-in-effect: legal but races/cache/SSR → prefer RSC / TanStack Query
Context re-renders all consumers when value identity changes — split contexts, memo values, or compose with children. Portals for modals (overflow/stacking). Error boundaries catch render errors in a subtree, not event handlers or async code (those need try/catch + local state).

Next.js App Router: conventions, RSC, data, hydration

Filesystem routing under app/: layout nests shared UI; page is the route; loading/error are segment Suspense/error boundaries; route groups (folder) skip the URL. Default components are Server Components — await data, no hooks, no browser APIs, less client JS. 'use client' creates a client boundary for that module and its imports. Pass serializable props only (Server Actions / Server Functions are the special callable exception).
tsx
1// app/ tree + RSC shell with client island2export default async function ProductPage({ params }) {3	const product = await db.product.get(params.id) // server only4	return (5		<main>6			<h1>{product.title}</h1>7			 {/* 'use client' child */}8			}>9				10			11		</main>12	)13}14// Cache intent (state this — defaults evolved across Next versions):15await fetch(url, { next: { revalidate: 60 } })           // time-based ISR-style16await fetch(url, { next: { tags: ['product'] } })       // tag-based17// After Server Action mutation:18revalidateTag('product'); revalidatePath('/products/[id]', 'page')19// Server cache ≠ client React Query/SWR — invalidate both layers when needed20// Actions = internal mutations; Route Handlers = HTTP API/webhooks21// Middleware: light auth redirects; heavy data stays in Server Components22// PPR: static shell + streamed dynamic holes (Suspense)
Streaming: loading.tsx + Suspense send HTML as chunks resolve. Hydration attaches handlers to server HTML — mismatches from Date.now()/Math.random() in render, window in RSC output, invalid HTML nesting, or extension-mutated DOM. Fix with stable first paint; move browser reads to client effects.
text
1// Hydration mismatch sources to recite2// 1. Non-deterministic render (Date.now, random)3// 2. browser-only APIs during server render4// 3. Invalid HTML the browser “fixes” before hydrate5// 4. Extensions mutating DOM early6// Fix: stable markup → enhance in useEffect; don't nuke SSR for one widget.78// Rendering decision tree (interview)9// Who owns data? Server DB/FS → RSC; Browser-only → Client10// Cache? SSG build · ISR revalidate · SSR per request · PPR shell11// SEO needed on client path? if yes and dynamic → SSR; if no → CSR ok
  1. 01CSR — private app shells; SEO irrelevant; cost: blank until JS.
  2. 02SSR — personalized + SEO; cost: server per request.
  3. 03SSG — marketing/docs; CDN speed; rebuild for changes.
  4. 04ISR / revalidate — static speed + timed/on-demand freshness (PDP scale).
  5. 05RSC + streaming / PPR — default modern Next: server data + client islands + static shell.

Interview Q&A battery — 42 questions (practise out loud)

Dense drill bank. Real rounds sample a subset. Each answer is multi-sentence on purpose: mechanism, production implication, and proof. Speak them cold until you do not need notes.

Hooks (Q1–Q10)

  1. 01Q: Why does useState use Object.is for equality, not ===? React bails out of a re-render when the next state is the same as the previous by Object.is. Object.is treats NaN as equal to NaN and distinguishes +0 from -0, which matches the identity React wants for “did this value change.” Using === would re-render when both values are NaN (NaN !== NaN). Cite: react.dev reference for useState. In production this almost never bites you with NaN, but the answer proves you read the bailout contract.
  2. 02Q: When does useEffect run vs useLayoutEffect? useEffect runs after paint, asynchronously, which is correct for most subscriptions and network sync that should not block the user seeing UI. useLayoutEffect runs synchronously after DOM mutations but before the browser paints — use it when you must measure layout or write to the DOM without a visible flash (tooltip position, focus restore, scroll restore). Overusing layout effects blocks paint and hurts INP on mid-tier devices. Cite: react.dev useLayoutEffect.
  3. 03Q: Can you put useEffect inside an if? No. Hooks must be called in the same order on every render because React stores hook state in a list keyed by call order. Conditionals, loops, and early returns that skip hooks break that list and corrupt state. Put the condition inside the effect body instead. Enforce with eslint-plugin-react-hooks (rules-of-hooks). Interviewers ask this as a quick filter for fundamentals.
  4. 04Q: Why is useEffect(fn, []) not the same as componentDidMount? Strict Mode in development intentionally mounts, unmounts, and remounts to surface missing cleanups, so effects with [] still run their setup twice in dev. Concurrent rendering can also discard a render path. Effects must be idempotent and always clean up. Also, class componentDidMount did not re-run on “deps change” because there were no deps — the mental models differ. Cite: StrictMode docs.
  5. 05Q: When does useMemo actually help? When the pure computation cost dominates re-render cost and inputs change less often than parent renders, or when you need referential stability for a memoized child. Otherwise useMemo is an allocation and comparison tax. Profile first. With React Compiler enabled, most manual memo becomes unnecessary — say that as a forward-looking senior note. Cite: react.dev useMemo and React Compiler docs.
  6. 06Q: useRef vs useState for a mutable flag? useRef holds a mutable .current that does not trigger re-render when changed — perfect for timers, AbortControllers, previous values, and “is pointer down” flags between events. useState triggers re-render and is for values that should appear in the UI. Using state for high-frequency flags causes useless commits; using ref for visible UI leaves the screen stale. Cite: useRef docs.
  7. 07Q: Why does useTransition exist when useDeferredValue exists? useTransition wraps an update you control (startTransition(() => setHeavy(x))) and gives you isPending for pending UI. useDeferredValue wraps a value you read and produces a lagging copy for heavy children while the urgent value (input) stays snappy. Use transitions for actions you initiate; deferred for values that flow in from props/state you do not wrap yourself. Cite: useTransition / useDeferredValue.
  8. 08Q: What does useId do and why not Math.random()? useId generates a stable unique id per component instance that matches between server HTML and client hydration — required for accessibility wiring (htmlFor/id, aria-controls). Math.random() or Date.now() in render produce different values on server vs client and cause hydration mismatches plus broken label associations. Cite: useId docs.
  9. 09Q: Explain useSyncExternalStore and why Suspense care matters. It is the correct way to subscribe to external stores (Redux, browser media queries, legacy global stores) with a getSnapshot that must be synchronous and consistent between server and client. If a render suspends, a naive subscription can tear; this hook forces consistent reads and supports server snapshots. Prefer it over ad-hoc useEffect + setState for external stores. Cite: useSyncExternalStore.
  10. 10Q: React 19’s use() — what is it for? use() lets a component read a Promise or Context during render in a Suspense-aware way — when the Promise is pending, the nearest Suspense boundary shows fallback. Unlike hooks, it can be called conditionally under constraints documented for Suspense. It is not a replacement for all data fetching architecture; it is a primitive for integrating async values into the render model. Cite: react.dev use.

Reconciliation & rendering (Q11–Q18)

  1. 01Q: Why does React not fully diff attribute order or deep-compare trees naively? A general tree diff is O(n³). React uses heuristics: different types remount; same type updates; list children use keys for identity. Attribute order and whitespace are not treated as semantic identity the way a full XML diff might. The production implication is that you must give React good identity signals (keys, stable types) rather than expecting magical deep equality.
  2. 02Q: When does React batch updates? Since React 18, updates inside event handlers, promises, timeouts, and native handlers are auto-batched into fewer renders. That reduces intermediate UI flashes and work. If you truly need to read the DOM after a forced flush, flushSync exists but is an escape hatch that hurts performance when overused. Cite: React 18 batching blog.
  3. 03Q: What does “render may be discarded, commit is not” mean for your code? Concurrent features and Strict Mode may re-run render without committing those intermediate trees. Therefore render must be pure: no side effects, no mutating external stores, no non-deterministic values that must match hydration. Put side effects in effects or event handlers. Reading external mutable state during render is a classic source of subtle bugs. Cite: keeping components pure.
  4. 04Q: Why do lists need key? key is React’s identity hint among siblings so fibers can be reused correctly when order changes. Without stable keys, React falls back to index matching, which moves state to the wrong row when items insert, delete, or reorder — checkboxes stay checked on the wrong todo. Cite: reconciliation docs. Index keys are only safe for static never-reordered lists.
  5. 05Q: What happens if key is Math.random()? Every render generates new keys, so React unmounts and remounts every item. Local state resets, focus is lost mid-typing, animations restart, and performance collapses under constant remount cost. Interviewers use this as a quick “do you understand identity” probe.
  6. 06Q: Controlled vs uncontrolled inputs? Controlled: value lives in React state; onChange updates it — full control for validation, disabling, derived UI. Uncontrolled: DOM owns the value; ref or FormData reads it — less re-render cost for huge forms, but harder to drive UI from the value. Prefer controlled when the value drives other logic; hybrid patterns appear in form libraries. Cite: react.dev input docs.
  7. 07Q: Why can you not mutate state directly? React detects changes by Object.is on the stored state reference. Mutating an object or array in place keeps the same reference, so React bails out and the UI does not update — or worse, updates inconsistently under concurrent rendering. Always replace with a new reference (spread, map, immutable helpers). Cite: useState immutability rules.
  8. 08Q: What is lifting state up? When two siblings need the same value, move state to the nearest common parent and pass data down and callbacks up. Alternatives when the tree is deep: composition (children slots), URL state for shareable values, or a thin context/store for true cross-tree UI. Lifting too high causes re-render fanout; lifting too low creates dual sources of truth. Cite: sharing state between components.

Effects & lifecycle (Q19–Q22)

  1. 01Q: When does useEffect cleanup run? Cleanup runs before the next effect execution when deps change, and on unmount. Use it to remove listeners, clear timers, abort fetches, and close sockets. Missing cleanup is a top production leak (duplicate websockets, stacked listeners). Write cleanups every time you subscribe. Cite: useEffect docs.
  2. 02Q: How do you cancel a fetch in useEffect? Create an AbortController, pass signal to fetch, abort in cleanup. Also ignore stale responses with a sequence number if abort is not enough. In React 19 form flows, useActionState and transitions cover some of this, but raw effects still need AbortController. Cite: useEffect data-fetching guidance and AbortController on MDN.
  3. 03Q: What is useActionState (React 19)? It manages the state of a form action: pending, returned state/error, and a wrapped action function. It replaces a common hand-rolled pattern of useTransition + error useState for server actions and progressive enhancement forms. Pair with useFormStatus in child submit buttons to read pending without prop drilling. Cite: useActionState reference.
  4. 04Q: useFormStatus vs useActionState? useFormStatus reads the status of the parent form (pending, data, method) and must be used in a component that is a descendant of the form — great for submit button pending UI. useActionState (formerly useFormState naming in some docs) binds React state to the result of an action. They solve different layers: parent form pending vs action result state. Cite: react-dom hooks docs.

RSC & Next.js (Q23–Q34)

  1. 01Q: What can Server Components do that Client Components cannot? They can await data on the server, touch DB/FS/secrets without shipping them, keep large dependencies server-only, and render markup without hydrating that subtree as interactive JS. That cuts bundle size and colocation of data with UI. They cannot use state, effects, or browser event handlers. Cite: react.dev Server Components.
  2. 02Q: When must a component be a Client Component? When it needs useState/useEffect, event handlers, browser-only APIs, or third-party libraries that depend on those. Mark the boundary with 'use client' at the top of the module and push that boundary as deep as possible so parents can stay server. Cite: nextjs.org server-and-client-components.
  3. 03Q: What does 'use client' do technically? It marks a module as a client boundary. Imports of that module from Server Components become references to a client-rendered island; the module and its dependency graph ship as client JS. It is not a runtime “switch to client” for a single function — it is a module-level contract. Putting it at the root layout is usually a boundary smell.
  4. 04Q: How do RSC pass children and compose with client components? Server Components serialize an RSC payload that can include placeholders for client components. A pattern seniors love: a Client Component that accepts children, where children are still Server Components rendered on the server and passed through — composition without forcing the whole tree client-side.
  5. 05Q: How do you share data between a Server Component and a client child? Pass serializable props (JSON-like data, not class instances or functions, except Server Actions). Fetch in the parent RSC and pass DTOs down. For ongoing interactive cache after load, hydrate a client library (React Query) with initialData from the server payload.
  6. 06Q: What is a Server Action / Server Function? An async function marked 'use server' (inline or file-level) that runs on the server when invoked from a form action or from a Client Component. Use it for mutations with revalidatePath/Tag afterward. It is not a public HTTP API — use Route Handlers for webhooks and external clients. Cite: react.dev server functions / Next updating data docs.
  7. 07Q: Why use revalidatePath after a mutation? App Router caches rendered output and fetch results. After a mutation, the cached path may still serve stale HTML/data. revalidatePath marks that path’s cache stale so the next render refetches. Pair with revalidateTag for fine-grained entity invalidation across many paths. Always consider the client cache layer separately.
  8. 08Q: What is PPR (Partial Prerendering)? A Next.js model where a static shell is prerendered and sent immediately while dynamic holes wrapped in Suspense stream in later. Users see structure fast; personalized or slow panels trail. It is the mental model for modern App Router performance design. Cite: nextjs.org partial prerendering docs.
  9. 09Q: How does ISR differ from SSG? SSG builds HTML at build time and stays static until the next build. ISR (revalidate interval or on-demand tags) serves static HTML but regenerates in the background after TTL or revalidateTag/Path calls — static speed with fresher data for catalogs and PDPs. Say the product constraint that picks one.
  10. 10Q: How do you do auth in Server Components? Read cookies/headers via next/headers in an RSC or layout, validate the session server-side, redirect() if unauthenticated, and only pass a safe user DTO to client islands. Middleware can do coarse edge redirects; authorization of data remains in the server components/services. Never trust a client-only tenant id.
  11. 11Q: How do you stream a slow component? Wrap it in Suspense with a meaningful fallback skeleton that reserves space (CLS). The outer shell streams first; the slow child streams when ready. loading.tsx is a route-level Suspense boundary. Avoid awaiting the slow call in the parent before returning any JSX if you want streaming benefits.
  12. 12Q: When does a fetch get deduped in App Router? Within a single server render pass, identical GETs can be deduped so layouts and pages do not double-hit the origin. Caching semantics (force-cache, no-store, revalidate, tags) still apply across requests — state the version/defaults carefully because Next has evolved. Opt out with cache: 'no-store' when data must be per-request.

Data fetching, hydration, rendering (Q35–Q42)

  1. 01Q: What is revalidate: 60 on a fetch? Time-based revalidation: the response may be cached and considered fresh for 60 seconds; after that, a request can trigger background regeneration while stale content may still be served until the new result is ready (ISR-style behavior). Use when data can lag by about a minute. Prefer tags when a mutation knows exactly what to invalidate.
  2. 02Q: How does revalidateTag differ? Tag-based on-demand invalidation. Tag fetches with next: { tags: ['products'] }; after a mutation, revalidateTag('products') purges entries with that tag. This is the right tool when writes know which entities changed. Forgetting to tag the original fetch is a common “revalidate did nothing” bug.
  3. 03Q: How do you prevent the 'use client' smell? Push 'use client' to the deepest leaf that needs interactivity. Keep data orchestration, layout chrome, and static markup in Server Components. If a whole page is client only because of one button, extract the button. Prove with bundle analyzer: client graphs should shrink after the refactor.
  4. 04Q: What is hydration cost and how do you shrink it? Hydration re-runs render on the client to attach listeners to server HTML. Cost scales with client component JS and tree size. Shrink it with smaller client islands, RSC for static parts, defer below-fold widgets via next/dynamic, and avoid root-level client boundaries. Measure with bundle analyzer and performance traces, not vibes.
  5. 05Q: When would you pick CSR over SSR? Heavy authenticated tools where SEO is irrelevant (admin dashboards, internal apps), or screens behind login with no indexability need and where server HTML does not buy much. Cost: blank/spinner until JS loads and weaker first-content stories. Do not CSR marketing or SEO-critical PDPs by default.
  6. 06Q: How do you debug a hydration mismatch? Read the React/Next error — it points at the text or attribute that differed. Common causes: Date.now/Math.random in render, locale/timezone formatting differences, window access during SSR, invalid HTML nesting that the browser rewrites, browser extensions mutating DOM. Fix with deterministic first paint, then enhance in useEffect; suppressHydrationWarning only for known benign cases like timestamps with extreme care.
  7. 07Q: revalidateTag ran but the client widget still shows old data — why? Server cache and client cache are different layers. revalidateTag purges server/next caches; React Query/SWR or the router client cache may still hold the previous payload until you invalidateQueries, router.refresh(), or equivalent. Senior answer names both layers and the invalidation for each.
  8. 08Q: Middleware vs Server Component auth — how do you split? Middleware at the edge is good for coarse redirects (no session cookie → login) and lightweight checks. Authorization of specific data (can this user read this row?) belongs next to the data fetch in Server Components or server services. Putting heavy DB work in middleware bloats every request and is hard to maintain. Combine: edge gate + server authorize.

Live drill: broken sandbox narrative

Practise three bugs in one sandbox: (1) stale interval closure → functional setState + cleanup; (2) unstable list keys → stable ids; (3) whole page 'use client' for one button → server page + client island. Narrate observe → hypothesis → fix → review catch.
Answer shape for every Q: (1) definition/mechanism in one sentence, (2) production failure if you get it wrong, (3) how you would prove or measure. That three-beat structure is what separates memorized flashcards from senior signal.
docsReact — Render and Commitreact.devdocsReact — You Might Not Need an Effectreact.devdocsReact — Server Componentsreact.devdocsNext.js — App Routernextjs.orgdocsNext.js — Cachingnextjs.orgdocsNext.js — Partial Prerenderingnextjs.orgarticleGreatFrontEnd — System Design PlaybookGreatFrontEnd

Checkpoint

A Server Component needs a like button with optimistic UI. Correct boundary placement?

AMark the whole page 'use client' so hooks work everywhere.BKeep the page as a Server Component for data/markup; extract LikeButton as a client child receiving serializable props.CUse useState in the Server Component — hooks work on the server in React 19.
Sign up free to answer and see why

Checkpoint

List items remount and inputs lose focus on every parent keystroke. Most likely cause?

AMissing React.memo on the parent.BUnstable keys (random/new each render) or keys that change with unrelated state.CuseEffect missing dependency array.
Sign up free to answer and see why

Checkpoint

You mutate a product via Server Action and call revalidateTag('product'), but a client widget still shows old data. Best explanation?

ArevalidateTag is broken; always hard-reload the browser.BServer cache and client cache are different layers — client React Query/SWR (or router client cache) may still hold data until invalidated/refreshed.CServer Actions cannot revalidate — only route handlers can.
Sign up free to answer and see why

Checkpoint

Pick rendering for a marketing homepage + authenticated app dashboard in one Next app.

ACSR everything for consistency.BSSG/ISR for marketing; SSR/RSC with cookies for dashboard; client islands for interactive widgets.CSSR for all routes including the static blog.
Sign up free to answer and see why

Checkpoint

Hydration error: text content mismatch on a timestamp. Best fix?

ARender Date.now() only on the server so clients get one value.BRender a stable placeholder on server and client first render; set the live time in useEffect on the client (or pass a server-serialized ISO and format consistently).CDisable hydration entirely with a CSR-only page always.
Sign up free to answer and see why

Could you run a 30-minute React/Next battery: re-renders, keys, effects, RSC boundary, revalidate layers, hydration?

New to itGetting thereConfident

Takeaways

  • Render is pure description; commit applies DOM; effects sync external systems — not derived state.
  • Keys are identity; unstable keys remount and corrupt local state/focus.
  • Justify useMemo/useCallback/Context; measure before memo culture; Compiler may retire hand memo.
  • RSC default + client islands; 'use client' high in the tree is a smell.
  • Server cache revalidation ≠ client cache; name both after mutations.
  • 42 Q&As: practise multi-sentence answers cold — mechanism, production risk, proof.

Next: component architecture and the four state buckets — where data actually lives.

Sources

Free to read · better with Enzo

Learn it with Enzo

Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.