Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

20 chapters
⌕
Beginner15›
00Scouting reportR1Screening callR2Machine codingR3JavaScript & TSR3·TSTypeScriptR4React & Next.jsR5Node & NestJSR6Databases & RedisR7DSA roundR8System designR9AWS, Docker, CI/CDR10Resume grillingR11BehaviouralR12HR & the number✓The week before
Advanced5›
50LWhat changes at ₹50L50LHard DSA50LDistributed systems50LRuntime internals50LStaff behavioural
/ search[ ] chaptert top

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
R4

React & Next.js

Length45–60 min
WhoFrontend lead
DecidesYour strongest round — win it decisively
Fail modeHook rules memorised, rendering model not understood
serviceproductsaasagency

This is where your six years should be loudest. It is also where 2023 answers now get you marked down: the React Compiler is stable, fetch has not cached by default since Next 15, and Next 16 moved to explicit opt-in caching. Answers below are checked against what actually ships in September 2026.

4.1
What actually happens when state changes? Explain reconciliation.
What they are really testingWhether you have a model of rendering or just a set of rules.

A state update schedules a re-render. React calls your component function again, producing a new element tree, and diffs it against the previous one. The diff is heuristic, not optimal — a general tree diff is O(n³), so React makes two assumptions to get O(n):

  1. Two elements of different types produce different trees, so it unmounts the old subtree and mounts a new one rather than trying to match across types.
  2. Siblings are matched by key.

Then it commits the minimal set of DOM mutations. The crucial sentence: a re-render is not a DOM update. Components re-render constantly; the DOM only changes where the diff found a difference. Candidates who conflate the two end up memoising things that were never touching the DOM anyway.

If they push into Fiber: rendering is split into a render phase (interruptible, can be thrown away, must be pure — this is why Strict Mode double-invokes in development) and a commit phase (synchronous, applies the mutations, runs layout effects). That split is what makes concurrent features possible.

They will push with
  • Why must the render phase be pure?
  • What does Strict Mode double-invoking actually catch?
  • Why does changing an element type remount the whole subtree?
4.2
Why must keys be stable, and what exactly breaks with index keys?
What they are really testingThe single best question for finding out whether someone understands reconciliation or has memorised a lint warning.

Keys tell React which element in the new list corresponds to which in the old. With array indices as keys, deleting the first item shifts every subsequent key down by one — so React concludes that every item's content changed, rather than that one item was removed.

What actually breaks is anything not in props: uncontrolled input values, focus, scroll position inside the row, CSS transitions, and component-local state. They follow the position instead of the data.

// delete row 0 and the typed values shift up by one
{rows.map((r, i) => <input key={i} defaultValue={r.name} />)}

// stable identity — state follows the row it belongs to
{rows.map(r => <input key={r.id} defaultValue={r.name} />)}

Index keys are safe for a list that is never reordered, filtered, sorted or spliced — an append-only log, for instance. Say that qualification; a blanket "never use index keys" sounds memorised.

The inverse trick worth knowing: you can deliberately change a key to force a remount and reset state — <Form key={userId} /> is the idiomatic way to clear a form when the selected user changes.

They will push with
  • How would you reset a form when the selected item changes?
  • What if your data genuinely has no id?
  • Is Math.random() as a key ever acceptable?
4.3
useMemo, useCallback, React.memo — when do they help and when are they noise?
What they are really testingWhether you cargo-cult optimisation. In 2026 there is a second layer: whether you know the compiler changed the answer.

Classically, all three help in exactly three situations: an expensive computation on every render; a value or callback passed to a memoised child, where a fresh identity would defeat the memo; and a value in a dependency array, where a new identity would re-fire an effect. Everywhere else they cost more than they save — each adds a comparison and holds a reference alive.

Say it like this

I do not memoise by default. On the projects I own we turn the React Compiler on and let it handle it; before that, I memoised after seeing a problem in the profiler, not before. The three cases I would still reach for it by hand are an expensive pure computation, a value going into a context provider, and something in a dependency array whose identity is re-firing an effect.

The answer that loses the room

"I wrap everything in useCallback for performance." This is the answer they hear most and it is wrong in both directions — it adds cost, and it does nothing at all unless the child is memoised.

2026 note

The React Compiler reached 1.0 and is stable in Next.js 16. It memoises automatically at build time by analysing your components, which removes most hand-written useMemo and useCallback. The correct 2026 answer is: "on a new codebase I turn the compiler on and stop writing manual memoisation; it does a better and more consistent job than I do. Manual memo remains for the cases the compiler cannot see — values crossing a context boundary, or an expensive computation it cannot prove is pure. And the compiler only works if the code follows the Rules of React, so it is also a forcing function for cleaning up mutation during render." Saying this shows you are current; saying "I memoise everything" now reads as three years out of date.

They will push with
  • What does React.memo compare, and how do you customise it?
  • Why does useCallback do nothing if the child is not memoised?
  • What are the Rules of React the compiler depends on?
4.4
Explain useEffect. What is the most common bug?
What they are really testingEffects are where most React bugs live. They want the stale closure and the missing cleanup.

It runs after the render is committed to the DOM. The dependency array controls re-running; the returned function runs before the next run and on unmount.

Bug one: the stale closure. The effect captures state from the render it was created in and then reads that stale value later — classically inside a setInterval.

stale — always logs 0
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000)
  return () => clearInterval(id)
}, [])                          // count is frozen at its first-render value
two correct fixes
// functional updater — no dependency on the current value at all
setCount(c => c + 1)

// or a ref holding the latest value, when you need to READ it
const latest = useRef(count)
useEffect(() => { latest.current = count })
the fetch race, fixed
useEffect(() => {
  const ac = new AbortController()
  fetch(`/api/users/${id}`, { signal: ac.signal })
    .then(r => r.json()).then(setUser)
    .catch(e => { if (e.name !== 'AbortError') setError(e) })
  return () => ac.abort()        // stale request cancelled on id change
}, [id])
2026 note

React 18+ Strict Mode mounts, unmounts and remounts every effect in development precisely to surface these missing cleanups. That is a feature, not a bug — and saying so is a small credibility marker.

The modern framing to add: "most effects I see should not be effects at all. Deriving state from props, transforming data for render, or reacting to a user event are all things that belong in render or in the handler. I use effects for genuine synchronisation with something outside React."

Bug two: no cleanup. A subscription or an in-flight request that outlives the component — a resolved response calls setState after unmount, or a slower earlier request overwrites a faster later one (the race that shows the wrong user's data).

They will push with
  • When should you NOT use an effect?
  • How do you fetch data in React today? (A query library or the framework, not raw useEffect.)
  • What is useLayoutEffect for?
4.5
You migrated Redux to Zustand. Defend that to someone who disagrees.
What they are really testingThis is on your resume, so it will be asked. They are testing whether you can name what you gave up.

That answer wins because it names the losses. A candidate who claims a migration had no downside gets marked down every time — the interviewer's job is to find out whether you evaluate or evangelise.

Be ready for the technical follow-up on why Zustand re-renders less than Context: it uses an external store with selector-based subscriptions (useSyncExternalStore underneath), so a component re-renders only when the slice it selected changes. Context has no selector — every consumer re-renders when the value changes.

Say it like this

The trigger was a measured cost, not a preference. With Redux Toolkit every piece of state touched a slice file, an action, a selector and often a thunk — four places to change for one feature, which is where the roughly half of state code that was ceremony came from. Zustand collapsed that into one store hook with a selector, so a feature that took four files took one.

Two things I gave up, and I want to be straight about them. We lost Redux DevTools time-travel debugging, which we had genuinely used to track down a payments bug — I mitigated that with Zustand's devtools middleware, but it is not the same. And we lost the enforced discipline that stops a team putting logic in the wrong layer; I replaced that with a single store directory and a review rule, which is weaker.

If I were starting a system with a large team and heavy derived state, I would still pick Redux Toolkit. For four applications and a small team, the ceremony was not paying for itself.

They will push with
  • Why not just use Context?
  • How does Zustand avoid re-rendering every consumer?
  • What would make you go back to Redux?
4.6
What is Context for, and why is it not a state manager?
What they are really testingA specific, common architectural mistake.

Context solves prop drilling — it is dependency injection for the tree. It does not solve rendering: every consumer re-renders when the provider's value changes, with no way to subscribe to a slice of it. Put a frequently-changing object near the root and you re-render half the application.

Three mitigations, in order of preference:

  1. Split into several contexts by change frequency — a rarely-changing ThemeContext and a frequently-changing CartContext should not be one object.
  2. Memoise the provider value, otherwise a new object literal every render invalidates every consumer regardless.
  3. For genuinely shared mutable state, use an external store with selectors — which is the actual argument for Zustand in your migration story.
// every consumer re-renders on every parent render — new object each time
<Ctx.Provider value={{ user, setUser }}>

// stable identity
const value = useMemo(() => ({ user, setUser }), [user])
<Ctx.Provider value={value}>
They will push with
  • How would you subscribe to only part of a context?
  • Does the compiler fix the provider-value problem? (It helps, but splitting contexts is still the real fix.)
4.7
Controlled vs uncontrolled components. Which do you reach for?
What they are really testingForm performance — and whether you know why React Hook Form is fast.

Controlled: React state is the source of truth, every keystroke is a render. Uncontrolled: the DOM holds the value and you read it via a ref on submit.

Controlled for live validation, dependent fields, and input formatting. Uncontrolled for large forms where per-keystroke renders cost you — which is exactly why React Hook Form is uncontrolled underneath and outperforms Formik on big forms. Naming that connection is the answer.

// controlled — re-renders the form on every keypress
<input value={v} onChange={e => setV(e.target.value)} />

// uncontrolled — zero renders while typing
<input defaultValue={v} ref={ref} />
They will push with
  • How do you validate an uncontrolled form?
  • What warning do you get when a component flips between the two?
4.8
Explain the four rendering strategies in Next.js and how you choose.
What they are really testingDirectly relevant to your Shopyvilla work. They will connect it to your 40% claim.
StrategyRenderedRight for
Static (SSG)Build timeMarketing, docs — identical for everyone. Fastest and cheapest.
ISRBuild time, revalidated on a schedule or on demandCatalogue and product pages that change hourly, not per request. Your commerce work.
Server (SSR)Per requestPersonalised or auth-gated pages; search results that depend on query params.
Client (CSR)In the browserDashboards behind a login where SEO is irrelevant and data is user-specific.
Say it like this

On Shopyvilla, fifteen category and product pages were client-rendered, so crawlers got an empty shell and users got a spinner. Moving them to server rendering with incremental regeneration cut initial load by about forty percent and made the catalogue actually indexable — the organic visibility was the point and the speed was the side effect.

They will push with
  • How do you choose the revalidate window?
  • What happens on the very first request after a deploy with ISR?
  • How would you handle a page that is 90% static and 10% personalised?
4.9
How does caching work in the App Router?
What they are really testingThe highest-value Next.js question in 2026, because the answer changed twice and most candidates are still on the old one.

There are four layers, and knowing there are four is most of the answer:

  1. Request memoisation — dedupes identical fetch calls within a single render pass, so three components asking for the same user cause one request. For non-fetch data access, React's cache() does the same.
  2. Data Cache — persistent across requests and deploys, controlled per fetch.
  3. Full Route Cache — the rendered HTML and RSC payload for static routes.
  4. Router Cache — client-side, holds visited segments for back/forward navigation.
current model — opt in explicitly
// not cached (the default since 15)
await fetch(url)

// cached until revalidated
await fetch(url, { cache: 'force-cache' })

// time-based
await fetch(url, { next: { revalidate: 3600, tags: ['products'] } })

// non-fetch data (an ORM query)
const getUser = cache(async (id) => db.user.findUnique({ where: { id } }))
Next 16 — use cache
'use cache'
import { cacheLife, cacheTag } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')
  return db.product.findMany()
}

// invalidate after a write, from a Server Action or route handler
revalidateTag('products')
2026 note

This is the part that changed. In Next 14, fetch was cached indefinitely by default and you opted out. Since Next 15 the default flipped: fetch is not cached, and you opt in with { cache: 'force-cache' } or { next: { revalidate: n } }. The client Router Cache for dynamic routes also went to 0 by default. Next 16 goes further with Cache Components and the 'use cache' directive, where caching is explicit and annotated at the page, component or function level, with cacheLife() profiles and cacheTag() for invalidation. Saying "Next caches fetch by default" in an interview today marks you as two major versions behind.

They will push with
  • How do you invalidate after a mutation?
  • What is the difference between revalidatePath and revalidateTag?
  • Why did the default change?
4.10
Server Components — what can they not do, and what is the mental model?
What they are really testingWhether you have shipped the App Router or only read about it.

They run only on the server, never ship their code to the browser, and can await data directly. That means: no useState, no useEffect, no event handlers, no browser APIs, no class components.

The model: server components are the default; push 'use client' as far down the tree as you can, so interactivity is a set of islands rather than the whole page. A 'use client' at the top of a layout drags everything below it into the client bundle, which is the most common way teams accidentally lose the entire benefit.

Two gotchas worth naming unprompted:

  • Props crossing the server→client boundary must be serialisable — no functions, no class instances, no Dates in older versions. This is the error people hit first.
  • A client component can render a server component, as long as it arrives as children rather than being imported. That is the escape hatch for wrapping server content in a client provider.
// this does NOT pull Feed into the client bundle
<ClientProvider>
  <ServerFeed />      // passed as children from a server parent
</ClientProvider>

// this DOES — an import inside a client file is a client import
'use client'
import ServerFeed from './server-feed'   // no longer a server component
They will push with
  • How do you share state between two client islands?
  • Where does a context provider go in an App Router app?
  • What is the RSC payload?
4.11
What are Server Actions, and how do they change form handling?
What they are really testingModern React data mutation. Increasingly the standard question at product companies.

A Server Action is a function marked 'use server' that runs on the server but can be called directly from client code or wired to a <form action>. Next creates the endpoint for you; you never write the fetch, the route or the serialisation.

The genuinely useful property is progressive enhancement: a form with a server action submits and works before JavaScript has loaded, because it is still an HTML form post.

// app/actions.ts
'use server'
export async function createBooking(prev, formData) {
  const parsed = BookingSchema.safeParse(Object.fromEntries(formData))
  if (!parsed.success) return { error: 'Check the dates' }
  await db.booking.create({ data: parsed.data })
  revalidateTag('bookings')
  return { ok: true }
}

// the form — React 19 hooks
'use client'
const [state, action, pending] = useActionState(createBooking, null)
return (
  <form action={action}>
    <input name="date" />
    <button disabled={pending}>{pending ? 'Booking…' : 'Book'}</button>
    {state?.error && <p role="alert">{state.error}</p>}
  </form>
)
2026 note

The security sentence that scores: "a Server Action is a public HTTP endpoint. Marking it 'use server' does not authenticate it — I still validate the input with a schema and check the session inside the action, exactly as I would in a controller." A lot of candidates assume the boundary protects them. It does not.

They will push with
  • How do you show optimistic UI with one? (useOptimistic.)
  • Can you call a Server Action outside a form?
  • How do you rate-limit one?
4.12
What did React 19 actually add that you use?
What they are really testingWhether you track the ecosystem or ship on whatever was current when you learned it.
  • Actions and useActionState — pending state, errors and optimistic updates for async mutations, without hand-rolled isLoading booleans.
  • useOptimistic — show the result immediately, reconcile when the server answers, roll back on failure.
  • useFormStatus — a nested button can read its parent form's pending state without prop drilling.
  • use() — read a promise or a context conditionally, unlike a hook. Lets you unwrap a promise passed down from a server component.
  • ref as a prop — forwardRef is no longer needed for function components. Large amounts of boilerplate deleted.
  • <Context> as a provider — <Ctx> instead of <Ctx.Provider>.
  • Document metadata and resource hints — <title>, <meta>, preload hoisted automatically from anywhere in the tree.
  • Better error messages for hydration mismatches — a genuine day-to-day improvement.
useOptimistic — the one they will ask you to demo
const [optimistic, addOptimistic] = useOptimistic(
  messages,
  (state, newMsg) => [...state, { ...newMsg, sending: true }]
)

async function send(formData) {
  const text = formData.get('text')
  addOptimistic({ text })        // paints instantly
  await sendMessage(text)        // reverts automatically if this throws
}
They will push with
  • How does useOptimistic roll back?
  • What is the difference between use() and useContext()?
4.13
What is hydration, and what causes a mismatch?
What they are really testingSSR debugging. Everyone has hit this; not everyone can explain it.

The server sends HTML; React then attaches event listeners and builds its internal tree over that existing markup rather than recreating it. A mismatch is when the tree React builds on the client differs from the HTML the server sent.

The causes, in the order you will actually meet them:

  • Date.now(), new Date() formatting, Math.random() — different value on each side.
  • Reading window, localStorage, navigator or a media query during render.
  • Locale or timezone differences between server and browser.
  • Invalid HTML nesting — a <div> inside a <p>, which the browser silently restructures so the DOM no longer matches what React sent.
  • Browser extensions injecting attributes.
the correct pattern for genuinely client-only values
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return <Skeleton />      // server and first client render agree
return <LocalTime value={ts} />

// or, for a single unavoidable node:
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
The answer that loses the room

Reaching for suppressHydrationWarning across a whole subtree. It silences the warning without fixing the divergence, and the interactive result is then genuinely wrong. It is a scalpel for one text node, not a bandage.

They will push with
  • How would you render a theme from localStorage without a flash?
  • Why does invalid nesting cause this?
4.14
You cut page load 35%. Take me through exactly what you measured.
What they are really testingThe highest-risk question in your entire loop. Every number on your resume is a claim until you can source it.

Answer in four beats — measured, diagnosed, changed, verified:

  • Measured. Name the metric and the tool. Lighthouse LCP on the three heaviest routes, plus server-side p95 on the endpoints those routes call. Give the before number if you have it.
  • Diagnosed. The heavy routes made the same expensive read on every request — listing and category data that changed a few times a day. The database was repeating identical work.
  • Changed. Redis cache-aside in front of those endpoints, plus the Redux→Zustand move which cut the JavaScript the client had to parse before hydration.
  • Verified. Same Lighthouse runs, same routes, plus cache hit ratio from Redis INFO.
Say it like this

The honest caveat is that these were lab measurements on specific routes rather than field data from real users — we did not have real-user monitoring in place. If I did it again I would put RUM in first, because lab numbers and field numbers diverge, and I would rather quote a p75 LCP from actual traffic.

Volunteering that limitation makes every other number on your resume more credible, not less. Interviewers are calibrating how much of what you say they can trust; a candidate who marks their own uncertainty gets trusted on everything else.

They will push with
  • Which specific change contributed most?
  • What is LCP and what usually causes a bad one?
  • What would you optimise next?
4.15
Name the Core Web Vitals and what causes a bad score on each.
What they are really testingWhether the performance work on your resume came with understanding.
MetricGoodUsual cause when bad
LCP largest contentful paint< 2.5sA huge unoptimised hero image, a slow server response, or render-blocking CSS/fonts.
INP interaction to next paint< 200msLong tasks on the main thread — heavy re-renders, big JSON parses, unmemoised expensive work in a handler. Replaced FID in 2024.
CLS cumulative layout shift< 0.1Images without dimensions, ads or banners injected above content, and web fonts swapping at a different size.

Fixes worth naming: next/image with explicit sizing (it reserves the box, which is most of CLS), font-display: swap with a metric-matched fallback, preloading the LCP image, and code-splitting to shorten long tasks.

2026 note

Mentioning INP rather than FID is a small currency check — FID was retired in 2024 and candidates still naming it are quoting old material.

They will push with
  • How would you measure these on real users rather than in Lighthouse?
  • What is a long task?
  • How does next/image prevent layout shift?
4.16
Rapid-fire React & Next.js
What they are really testingBreadth. Expect fifteen of these in a services round.
  • Rules of hooks and why. Hooks are matched by call order across renders; a conditional hook desynchronises the list and state lands on the wrong hook.
  • useRef vs useState. Both persist across renders; only state triggers one. Refs for DOM nodes, timer ids, and "latest value" reads.
  • useLayoutEffect. Runs synchronously after DOM mutation, before paint. For measuring an element and adjusting it without a visible flicker. It blocks paint, so use it sparingly — and it does not run on the server.
  • Custom hooks. A function starting with use that calls other hooks. A good one has a single responsibility and returns a stable API.
  • React.lazy + Suspense. Split at route boundaries first, then at heavy below-the-fold components (a chart library, a rich text editor).
  • Error boundaries. Catch errors during rendering, in lifecycles and in constructors. They cannot catch errors in event handlers, async code, or on the server during SSR — those need try/catch.
  • Virtualising 50,000 rows. Render only the visible window plus an overscan buffer; react-window or TanStack Virtual. The hard part is variable row heights.
  • useTransition. Marks an update as non-urgent so typing stays responsive while an expensive list re-filters. useDeferredValue is the value-shaped version.
  • Middleware in Next. Runs before a request completes, at the edge. Auth redirects, locale routing, A/B bucketing. Keep it thin — it is on every request.
  • Route handlers. app/api/x/route.ts exporting GET/POST. They take and return standard Web Request/Response, unlike the old req/res API routes.
  • Auth in the App Router. Session check in a server component or middleware; never trust a client-side guard, because the data fetch is what actually needs protecting.
  • loading.tsx and error.tsx. File conventions that wrap a segment in Suspense and an error boundary automatically.
  • Parallel and intercepting routes. Slot-based layouts and the "open a photo in a modal but deep-link to the full page" pattern.
  • How do you test a component? React Testing Library, asserting on what a user sees and does — roles and text, not class names. Do not test implementation details or internal state.
  • WCAG 2.1 AA — name four things you actually did. Semantic landmarks and heading order; focus management on route change; contrast ratios of 4.5:1 for text; labels on every control with ARIA only where semantics ran out; no keyboard traps in modals.
←previousTypeScript↑ CovernextNode & NestJS→