Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Frontend engineering interviews test a uniquely broad range of skills — from low-level browser APIs and CSS layout algorithms to component architecture, state management, and performance optimization. Unlike backend interviews where you might deep-dive into one system, frontend interviews expect you to move fluidly across the entire stack from HTML semantics to deployment pipelines. The questions below are organized by topic area and progress from fundamentals to advanced concepts within each section. For each question, practice answering out loud before reading the provided answer. In real interviews, your ability to articulate concepts clearly matters as much as knowing the right answer. A senior frontend engineer who says “let me walk you through the trade-offs” will always outperform someone who recites a textbook definition.

1. HTML Fundamentals

HTML (HyperText Markup Language) is the foundational layer of the web — it defines the structure and semantics of content that browsers parse into the DOM (Document Object Model). Without HTML, there is no web page. CSS styles it, JavaScript adds behavior, but HTML is the skeleton everything hangs on.Why it actually matters beyond the obvious:
  • SEO depends on it. Google’s crawler reads your HTML structure to understand page content. A poorly structured page with <div> soup ranks worse than one using proper heading hierarchy (<h1> through <h6>), even with identical visible content.
  • Accessibility starts here. Screen readers (JAWS, NVDA, VoiceOver) navigate via HTML landmarks. If your page is all <div> and <span>, a blind user literally cannot navigate it. Proper HTML = legal compliance (ADA, WCAG 2.1 AA).
  • Performance impact. The browser’s parsing pipeline (HTML tokenization -> DOM construction -> CSSOM -> Render Tree) is directly affected by HTML structure. Deeply nested DOM trees (1500+ nodes) measurably slow rendering — Google Lighthouse flags this.
  • SSR and hydration. In frameworks like Next.js, the server sends raw HTML first. If that HTML is malformed or semantically meaningless, you get layout shifts (CLS issues) and hydration mismatches.
What interviewers are really testing: Whether you see HTML as “just tags” or understand it as the foundation of accessibility, SEO, and rendering performance.Red flag answer: “HTML is just for making web pages” with no mention of semantics, accessibility, or how it affects the broader rendering pipeline.Follow-up:
  • How does the browser parse HTML differently from XML, and why does that matter for error recovery?
  • What happens when you put a <div> inside a <p> tag — and why?
  • How does HTML structure affect Core Web Vitals scores?
Semantic HTML means using elements that describe their meaning rather than just their appearance. Instead of <div class="header">, you use <header>. Instead of <div class="nav-link">, you use <nav> with <a> tags.The key semantic elements and when to use them:
  • <header> / <footer> — page or section-level header/footer (not just “the top/bottom of the page”)
  • <nav> — primary navigation blocks (screen readers let users jump directly to <nav>)
  • <main> — the primary content area (only ONE per page)
  • <article> — self-contained content that makes sense independently (a blog post, a product card)
  • <section> — thematic grouping with a heading (use <section> when you’d give it a heading, <div> when it’s purely for styling)
  • <aside> — tangentially related content (sidebars, callouts)
  • <figure> / <figcaption> — images, charts, code snippets with captions
  • <time> — dates and times (machines can parse <time datetime="2025-03-15">)
Real-world impact:
  • Accessibility: VoiceOver on macOS lets users jump between landmarks. A site with proper <nav>, <main>, <aside> gives blind users the same quick-scan ability that sighted users get visually. Without it, they hear every single element linearly.
  • SEO: Google explicitly uses semantic structure for featured snippets. An <article> with proper <h1> -> <h2> hierarchy is more likely to appear in search features than a <div> soup page.
  • Maintainability: 6 months later, <nav> is instantly understandable. <div class="sc-bwzfXH"> (styled-components auto-generated) means nothing.
What interviewers are really testing: Do you think about the machine-readability and accessibility of your markup, or do you just make things “look right” visually?Red flag answer: Only mentioning <header> and <footer> without explaining WHY semantic HTML matters or how screen readers consume it.Follow-up:
  • When would you use <section> vs <div> — what is the actual rule?
  • How do ARIA roles relate to semantic HTML, and when do you need ARIA even with semantic elements?
  • A designer gives you a card layout — walk me through which semantic elements you would choose and why.
Meta tags are HTML elements in the <head> that provide metadata about the page to browsers, search engines, and social media crawlers. They do not render visible content but profoundly affect how the page behaves, is indexed, and is shared.Critical meta tags every frontend dev should know:
  • <meta charset="UTF-8"> — Character encoding. Must be in the first 1024 bytes of the document. Without it, international characters can break (mojibake).
  • <meta name="viewport" content="width=device-width, initial-scale=1.0"> — Controls responsive behavior on mobile. Without this, mobile browsers render at ~980px width and scale down, breaking your responsive design completely.
  • <meta name="description" content="..."> — The 150-160 character snippet Google shows in search results. Directly affects click-through rate. This is not a ranking factor, but it drives CTR which indirectly affects ranking.
  • <meta name="robots" content="noindex, nofollow"> — Controls crawler behavior. Critical for staging environments (you do NOT want Google indexing your staging site).
  • Open Graph tags (<meta property="og:title">, og:description, og:image) — Controls how the page appears when shared on social media. A missing og:image means your link looks bland on Slack, Twitter, LinkedIn.
  • <meta http-equiv="Content-Security-Policy"> — Can define CSP inline (though HTTP headers are preferred for production).
  • <link rel="canonical" href="..."> — Not technically a meta tag but often grouped with them. Tells search engines the “real” URL to avoid duplicate content penalties.
Production gotcha: In Next.js, you manage these via the <Head> component (Pages Router) or metadata export (App Router). A common bug: forgetting to set unique <title> and <meta description> per page, so every page shows the same snippet in search results.What interviewers are really testing: Whether you understand how meta tags affect SEO, social sharing, and mobile behavior — not just that they exist.Red flag answer: Only mentioning viewport and charset without discussing SEO-critical tags like description, robots, or Open Graph.Follow-up:
  • What happens if you forget the viewport meta tag on a mobile-first site?
  • How do Open Graph tags differ from Twitter Card tags, and when do you need both?
  • How would you handle dynamic meta tags for a page with user-generated content (e.g., a product page)?
This is fundamentally about the CSS display model and how elements participate in document flow.Block-level elements (<div>, <p>, <h1>-<h6>, <section>, <article>, <form>):
  • Start on a new line (create a line break before and after)
  • Take up the full available width of their parent by default
  • Respect width, height, margin, and padding on all sides
  • Stack vertically
Inline elements (<span>, <a>, <strong>, <em>, <img>, <code>):
  • Do not start on a new line — they flow within text
  • Only take up as much width as their content needs
  • Vertical margin and padding do not push other elements away (this is the gotcha most people miss). Horizontal margin/padding works fine, but vertical margin is ignored and vertical padding “bleeds” without affecting layout.
  • Cannot contain block elements (putting a <div> inside a <span> is invalid HTML)
The third category people forget: inline-block
  • Flows inline (sits within text) BUT respects width, height, and vertical margin/padding like a block element
  • Useful for buttons, badges, or any element that needs dimensions but should sit inline
Modern nuance: With CSS display: flex or display: grid, the block/inline distinction becomes less relevant because flex/grid children follow flex/grid layout rules, not normal flow. But understanding normal flow is critical for debugging layout issues outside of flex/grid contexts.What interviewers are really testing: Whether you understand the CSS box model deeply enough to predict layout behavior without trial-and-error.Red flag answer: Only saying “block takes full width, inline doesn’t” without mentioning the vertical margin/padding behavior difference or inline-block.Follow-up:
  • Why can you not set a fixed height on an inline element, and what happens if you try?
  • What is display: inline-flex and when would you use it?
  • If I set margin-top: 20px on a <span>, what happens and why?
<!DOCTYPE html> is a document type declaration that tells the browser which rendering mode to use. In HTML5, it triggers standards mode (also called “no-quirks mode”).Why this matters — the rendering modes:
  • Standards mode (triggered by <!DOCTYPE html>): The browser follows the W3C/WHATWG spec precisely. Box model works as expected. Modern CSS behaves correctly.
  • Quirks mode (triggered by a missing or malformed DOCTYPE): The browser emulates old IE5/Navigator 4 behavior. The box model changes (width includes padding and border, like box-sizing: border-box but inconsistently applied). Inline elements can have height. Various CSS calculations break in subtle ways.
  • Almost-standards mode: Triggered by some older DOCTYPEs. Mostly standards-compliant but with one key difference in how table cell vertical sizing works.
The real-world gotcha: If you have ever seen a layout that works perfectly in your development environment but breaks on production (especially with legacy HTML email renderers or older browser targets), a missing or incorrect DOCTYPE is one of the first things to check. Email HTML in particular is notorious for quirks mode rendering.Historical context: In XHTML/HTML4 days, DOCTYPEs were complex: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">. HTML5 simplified it to just <!DOCTYPE html> because browsers only need to know “use standards mode” — nothing else in the DOCTYPE is actually used.What interviewers are really testing: Whether you understand browser rendering modes and why a single missing line can cause cascading layout bugs.Red flag answer: “It just tells the browser it is HTML5” without any mention of rendering modes or what happens without it.Follow-up:
  • What visual differences would you notice if a page accidentally renders in quirks mode?
  • How does DOCTYPE affect the CSS box model behavior?
  • Why is the HTML5 DOCTYPE so much shorter than the XHTML 1.0 DOCTYPE?
All three store data in the browser, but they differ in lifetime, scope, size limits, and server accessibility.localStorage:
  • Persists indefinitely (until explicitly cleared by code or user)
  • Scoped to the origin (protocol + domain + port)
  • ~5-10 MB limit (varies by browser)
  • Never sent to the server automatically
  • Synchronous API (blocks the main thread — avoid storing large data)
  • Use case: user preferences, theme settings, cached non-sensitive data
sessionStorage:
  • Persists only for the browser tab session — gone when the tab closes
  • Scoped to the origin AND the specific tab (two tabs on the same site have separate sessionStorage)
  • ~5-10 MB limit
  • Never sent to the server automatically
  • Use case: form data preservation during a multi-step wizard, temporary UI state
Cookies (document.cookie):
  • Configurable expiration (Expires / Max-Age header) — can be session or persistent
  • Sent to the server with every HTTP request to the matching domain (this is the critical difference)
  • ~4 KB limit per cookie
  • Scoped by domain and path
  • Can be set as HttpOnly (inaccessible to JavaScript — critical for auth tokens), Secure (HTTPS only), SameSite (CSRF protection)
  • Use case: authentication tokens, session IDs, server-needed preferences
Security implications the interviewer wants to hear:
  • Never store JWTs or auth tokens in localStorage — XSS attacks can read it trivially. Use HttpOnly + Secure + SameSite=Strict cookies instead.
  • localStorage is vulnerable to XSS but NOT to CSRF. Cookies are vulnerable to CSRF but can be protected with SameSite and CSRF tokens.
  • IndexedDB exists for structured/larger data (hundreds of MB) but has an async API.
What interviewers are really testing: Your security awareness around client-side storage and whether you understand the server-accessibility distinction.Red flag answer: Describing only size and persistence differences without mentioning that cookies are sent to the server or the security implications of storing tokens in localStorage.Follow-up:
  • A teammate stores the user’s JWT in localStorage. What is your response and what do you suggest instead?
  • How does the SameSite cookie attribute prevent CSRF attacks?
  • When would you choose IndexedDB over localStorage?

2. CSS & Styling

CSS position controls how an element is placed in the document flow. Understanding this deeply means understanding stacking contexts, containing blocks, and scroll behavior.position: relative
  • Element stays in normal document flow (still occupies its original space)
  • top, right, bottom, left offset it relative to where it would normally be
  • Creates a new containing block for absolutely positioned children
  • Common use: nudging an element slightly, or creating a positioning context for a child
position: absolute
  • Element is removed from normal flow (other elements act as if it does not exist)
  • Positioned relative to the nearest positioned ancestor (any ancestor with position other than static). If none exists, it is relative to the initial containing block (effectively the viewport)
  • Common gotcha: forgetting to set position: relative on the parent, causing the absolute element to fly to the top-left of the page
  • Use case: tooltips, dropdown menus, overlays positioned relative to a trigger
position: fixed
  • Removed from normal flow
  • Positioned relative to the viewport — does not move on scroll
  • Gotcha: If any ancestor has transform, filter, or will-change set, it breaks fixed positioning (the element becomes fixed relative to that ancestor instead of the viewport). This is a well-known CSS bug that catches even senior developers.
  • Use case: sticky headers, floating action buttons, modals
position: sticky
  • A hybrid: acts like relative until the element reaches a scroll threshold, then acts like fixed
  • Requires a top, bottom, left, or right value to define when it “sticks”
  • Only sticks within its parent container — once the parent scrolls out of view, the sticky element goes with it
  • Gotcha: overflow: hidden or overflow: auto on ANY ancestor breaks sticky positioning silently
  • Use case: table headers that stick while scrolling, section headers in long lists
What interviewers are really testing: Whether you can debug layout issues involving positioned elements and understand containing blocks.Red flag answer: Describing fixed and absolute identically, or not knowing that sticky exists, or being unaware that transforms break fixed positioning.Follow-up:
  • What is a “containing block” and how does it affect absolute positioning?
  • Why would position: fixed not work as expected even when set correctly? What CSS properties on ancestors could break it?
  • How would you implement a sticky table header that works across browsers?
Flexbox is a one-dimensional layout system (either row OR column at a time) designed for distributing space and aligning items within a container. It replaced the old days of float hacks, clearfixes, and display: table-cell workarounds.The mental model: Think of a flex container as a “smart row” or “smart column” that knows how to distribute its children.Container properties (set on the parent):
  • display: flex — activates flexbox
  • flex-direction: row | column | row-reverse | column-reverse — sets the main axis
  • justify-content — alignment along the main axis (e.g., space-between, center, flex-end)
  • align-items — alignment along the cross axis (e.g., center, stretch, baseline)
  • flex-wrap: wrap — allows items to wrap to the next line (critical for responsive layouts)
  • gap — spacing between items (modern replacement for margin hacks)
Item properties (set on children):
  • flex-grow — how much extra space the item should absorb (0 = do not grow)
  • flex-shrink — how much the item should shrink if space is tight (0 = do not shrink)
  • flex-basis — the initial size before grow/shrink kicks in (like a “suggested width”)
  • The shorthand flex: 1 means flex-grow: 1; flex-shrink: 1; flex-basis: 0% — “take equal space”
  • align-self — override the container’s align-items for this specific child
Common real-world patterns:
  • Centering anything: display: flex; justify-content: center; align-items: center — this alone solved a decade of CSS centering pain
  • Sticky footer: flex container on body, flex-grow: 1 on main content
  • Navigation bar: justify-content: space-between with logo on one end and nav links on the other
  • Equal-height cards: flex items in a row naturally stretch to equal height via align-items: stretch (the default)
Gotchas:
  • flex-basis vs width: flex-basis wins over width when both are set (in the flex-direction axis). Use flex-basis for flex-direction sizing.
  • min-width: auto is the default for flex items, meaning a flex item will NOT shrink below its content size. This causes overflow bugs. Fix: min-width: 0 on the item.
  • Text overflow in flex children: long text will push a flex item beyond its allotted space unless you add overflow: hidden; text-overflow: ellipsis; min-width: 0.
What interviewers are really testing: Whether you can solve layout problems with flexbox confidently and know its edge cases, not just its basic properties.Red flag answer: Only listing properties without explaining main axis vs cross axis, or not knowing flex: 1 shorthand behavior.Follow-up:
  • What does flex: 1 actually expand to, and how does it differ from flex: auto?
  • When would you choose Flexbox over Grid, and vice versa?
  • A flex item with long text is overflowing its container. How do you fix it and why does this happen?
CSS Grid is a two-dimensional layout system that handles both rows AND columns simultaneously. While Flexbox excels at distributing items in a single line, Grid excels at defining explicit page layouts.Core concepts:
  • display: grid activates it on the container
  • grid-template-columns / grid-template-rows — define the track sizes. Example: grid-template-columns: 1fr 2fr 1fr creates three columns where the middle is twice as wide.
  • fr unit — a fraction of available space (like flex-grow but for grid tracks)
  • gap (or row-gap / column-gap) — spacing between tracks
  • grid-column / grid-row — explicitly place items spanning tracks. Example: grid-column: 1 / 3 spans columns 1 and 2.
  • grid-template-areas — name areas for readable layout definitions
Powerful patterns:
/* Named grid areas — incredibly readable */
.layout {
  display: grid;
  grid-template-areas:
    "header header header"
    "sidebar main aside"
    "footer footer footer";
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
}
  • repeat() and minmax(): grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)) creates a responsive grid that automatically adjusts column count without media queries. This single line replaces dozens of lines of media query + flexbox code.
  • auto-fill vs auto-fit: auto-fill creates empty tracks if there is extra space; auto-fit collapses empty tracks so items stretch. The difference is subtle but matters for responsive behavior.
Grid vs Flexbox decision framework:
  • Use Grid when you need to control both rows and columns (page layouts, dashboards, card grids)
  • Use Flexbox when you need to distribute items in a single direction (navbars, toolbars, centering)
  • In practice, they work together: Grid for the page layout, Flexbox for component-level alignment inside grid cells
What interviewers are really testing: Whether you reach for Grid when appropriate and understand fr, minmax(), and auto-fill/auto-fit.Red flag answer: Saying “I just use Flexbox for everything” or not knowing the difference between auto-fill and auto-fit.Follow-up:
  • How would you create a responsive card grid that adjusts from 4 columns on desktop to 1 on mobile, without media queries?
  • What is the difference between auto-fill and auto-fit in repeat()?
  • How does Grid handle implicit vs explicit tracks, and when does that matter?
Specificity is the algorithm browsers use to determine which CSS rule wins when multiple rules target the same element. It is calculated as a tuple, not a simple number.The specificity hierarchy (from highest to lowest):
  1. !important — overrides everything (specificity: infinite, effectively). Use sparingly — it is a maintenance nightmare.
  2. Inline styles (style="...") — specificity: (1,0,0,0)
  3. ID selectors (#header) — specificity: (0,1,0,0)
  4. Class selectors, attribute selectors, pseudo-classes (.btn, [type="text"], :hover) — specificity: (0,0,1,0)
  5. Element selectors, pseudo-elements (div, p, ::before) — specificity: (0,0,0,1)
  6. Universal selector (*) — specificity: (0,0,0,0) (adds nothing)
How calculation works: Each selector component adds to its respective column. #nav .item a:hover = (0,1,2,1) — one ID, one class + one pseudo-class, one element.Critical rules:
  • Specificity is compared column by column, left to right. One ID selector beats ANY number of class selectors. #header (0,1,0,0) beats .a.b.c.d.e.f.g.h.i.j (0,0,10,0).
  • When specificity is equal, the last rule in source order wins (cascade order).
  • !important creates a parallel specificity universe. Among !important rules, normal specificity comparison applies. This is why !important wars happen in legacy codebases.
Real-world strategies:
  • BEM methodology (.block__element--modifier) keeps all selectors at one class level, avoiding specificity wars entirely
  • CSS Modules / CSS-in-JS scope styles automatically, making specificity less of an issue
  • Tailwind CSS uses single utility classes, so specificity is uniformly low
  • The :where() pseudo-class has zero specificity — great for resets/defaults that should be easily overridden
  • The :is() pseudo-class takes the specificity of its most specific argument
What interviewers are really testing: Whether you can debug “why is my CSS not applying?” without just slapping !important on everything.Red flag answer: Saying “IDs are stronger than classes” without being able to explain the calculation, or suggesting !important as a go-to solution.Follow-up:
  • You write a class selector but it is not being applied. How do you debug the specificity conflict?
  • What is the specificity of :is(.foo, #bar) and why could that be surprising?
  • How do CSS Modules or BEM help manage specificity in large codebases?
CSS performance optimization impacts render speed, page load time, and runtime smoothness. Here is a layered approach from most to least impactful:1. Reduce CSS payload size:
  • Minify CSS — tools like cssnano, PostCSS, or built-in bundler minification can reduce CSS by 20-40%
  • Remove unused CSS — PurgeCSS (used by Tailwind) or the coverage tab in Chrome DevTools can identify dead CSS. Large apps often ship 70-90% unused CSS
  • Tree-shake component-level CSS — CSS Modules, styled-components, or Tailwind JIT ensure only used styles ship
2. Optimize delivery:
  • Critical CSS — extract above-the-fold CSS and inline it in <head>. Tools: Critical, Critters (used by Angular CLI). Everything else loads async via <link rel="preload">
  • Avoid render-blocking CSS — the browser will not render until all CSS in <head> is downloaded and parsed. Use media attributes to make non-critical stylesheets non-blocking: <link rel="stylesheet" href="print.css" media="print">
3. Write performant selectors:
  • Browsers match selectors right to left. .container .item span first finds ALL <span> elements, then filters up. Overly broad rightmost selectors are slow on pages with thousands of elements.
  • Avoid universal selectors as the key selector: * { } or .parent * { } matches everything
  • Keep selectors shallow (1-3 levels deep). BEM’s flat class structure (.card__title) is faster than nested selectors (.page .section .card .title)
4. Optimize runtime paint/layout:
  • Avoid triggering layout thrashing. Properties like width, height, top, left, margin trigger expensive layout recalculations. Prefer transform: translate() for animations.
  • Use will-change judiciously. It promotes elements to their own compositor layer, enabling GPU-accelerated animations. But overuse (applying it to dozens of elements) consumes memory.
  • contain: layout paint — tells the browser that an element’s internals do not affect outside layout, enabling rendering optimizations
  • Prefer transform and opacity for animations — these properties can be animated on the compositor thread without triggering layout or paint
What interviewers are really testing: Whether you think about CSS performance as more than just “minify it” — whether you understand the rendering pipeline.Red flag answer: Only mentioning minification, or suggesting “just use Tailwind” without explaining why it helps performance.Follow-up:
  • What is the difference between a layout, paint, and composite operation, and why does it matter for animations?
  • How would you identify and fix a CSS performance bottleneck using Chrome DevTools?
  • What is contain in CSS and when would you use it?
The CSS Box Model is the fundamental rendering model for every element on the page. Every element is a rectangular box composed of four layers, from inside out:1. Content — the actual text, image, or child elements 2. Padding — space between content and border (background color fills this area) 3. Border — the visible border around the padding 4. Margin — space outside the border, creating distance from other elements (transparent — background does not fill this)The critical box-sizing distinction:
  • box-sizing: content-box (default): width and height apply to the content only. Padding and border are ADDED on top. So width: 200px; padding: 20px; border: 1px solid = actual rendered width of 242px. This is the source of countless layout bugs.
  • box-sizing: border-box: width and height include padding and border. width: 200px means 200px total, padding and border included. This is what developers expect.
Best practice: Every modern CSS reset includes:
*, *::before, *::after {
  box-sizing: border-box;
}
Margin collapse — the gotcha nobody expects:
  • Vertical margins between adjacent block elements collapse — the larger margin wins, they do not add up. Two elements with margin-bottom: 20px and margin-top: 30px have 30px between them, not 50px.
  • Margin collapse does NOT happen with: flex/grid children, floated elements, absolutely positioned elements, or inline-block elements
  • Parent-child margin collapse: a child’s margin-top can “escape” and become the parent’s margin if the parent has no padding, border, or overflow set. Fix: add padding-top: 1px or overflow: hidden to the parent.
What interviewers are really testing: Whether you understand why layouts break and can explain margin collapse — one of the most common sources of CSS confusion.Red flag answer: Describing the four layers without mentioning box-sizing or margin collapse.Follow-up:
  • Why do most CSS resets set box-sizing: border-box globally, and what breaks without it?
  • Explain margin collapse. When does it happen and when does it not?
  • How does the box model differ for inline elements vs block elements?

3. JavaScript Core Concepts

Hoisting is JavaScript’s behavior of moving declarations to the top of their scope during the compilation phase, before any code executes. But the details are what separate a junior from a senior answer.What actually happens (the precise mental model):
  • JavaScript does not physically move code. During the creation phase of an execution context, the engine scans for declarations and allocates memory for them.
  • Function declarations are fully hoisted — both the name AND the function body are available before the line they are written on.
  • var declarations are hoisted but initialized to undefined. The assignment happens at runtime when that line executes. This is why console.log(x); var x = 5; logs undefined, not an error.
  • let and const declarations are hoisted but NOT initialized. They exist in a Temporal Dead Zone (TDZ) from the start of the block until the declaration line. Accessing them in the TDZ throws a ReferenceError.
  • Function expressions and arrow functions assigned to var are treated as var — hoisted as undefined. Assigned to let/const — in the TDZ.
Example that trips up most candidates:
console.log(a); // undefined (var is hoisted, initialized to undefined)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
console.log(c()); // "hello" (function declaration is fully hoisted)
console.log(d()); // TypeError: d is not a function (var hoisted as undefined)

var a = 1;
let b = 2;
function c() { return "hello"; }
var d = function() { return "world"; };
Why this matters in practice:
  • The TDZ is why let/const are safer — they fail loudly instead of silently giving undefined
  • Hoisting is why you can call function declarations before they appear in code (useful for structuring files with main logic at top, helper functions below)
  • Understanding hoisting is essential for debugging closure-related bugs and var in loops
What interviewers are really testing: Whether you understand the JavaScript engine’s two-phase execution model and can predict code behavior involving hoisting edge cases.Red flag answer: “Variables are moved to the top” without distinguishing between var, let, const, and functions, or without mentioning the Temporal Dead Zone.Follow-up:
  • What is the Temporal Dead Zone and why does it exist?
  • What is the difference between hoisting a function declaration vs a function expression?
  • Why does var inside a for loop cause unexpected closure behavior, and how do let/const fix it?
This question is deeper than it looks — it tests your understanding of scope, hoisting, the temporal dead zone, and mutability vs reassignment.
Featurevarletconst
ScopeFunction-scopedBlock-scopedBlock-scoped
HoistingHoisted, initialized to undefinedHoisted, TDZ (uninitialized)Hoisted, TDZ (uninitialized)
Re-declarationAllowed (silently overwrites)Not allowed in same scopeNot allowed in same scope
Re-assignmentAllowedAllowedNot allowed
The nuances that matter:1. Scope difference is the biggest deal:
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (var is function-scoped, all closures share the same i)

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 (let creates a new binding per iteration)
This is probably the most famous JavaScript gotcha and the main practical reason let replaced var.2. const does not mean immutable:
const user = { name: "Alice" };
user.name = "Bob"; // This WORKS — object mutation is allowed
user = { name: "Charlie" }; // TypeError — reassignment is not allowed
const prevents reassignment of the binding, not mutation of the value. For true immutability, you need Object.freeze() (shallow) or libraries like Immer.3. Global scope behavior: var at the top level creates a property on window (in browsers). let and const do not. This matters when dealing with third-party scripts that check window.someGlobal.Modern best practice: Default to const. Use let only when you need reassignment (loop counters, accumulators). Never use var in modern code.What interviewers are really testing: Whether you understand scope, closures, and the const immutability misconception.Red flag answer: “const means the value cannot change” (incorrect — it means the binding cannot be reassigned). Also: not being able to explain the var in a loop closure problem.Follow-up:
  • If const does not make objects immutable, how do you achieve immutability in JavaScript?
  • What happens if you declare var x twice in the same scope vs let x twice?
  • Why does var in a loop with setTimeout produce unexpected results, and how does let fix it?
Event delegation is a pattern where you attach a single event listener to a parent element instead of attaching listeners to each child. It works because of event bubbling — events propagate upward from the target element through its ancestors.How it works step by step:
  1. User clicks a specific child element (the event target)
  2. The event bubbles up through the DOM tree: child -> parent -> grandparent -> … -> document
  3. The listener on the parent catches the event
  4. You use event.target to determine which specific child was clicked
  5. You apply logic based on the target
Example:
// BAD: 1000 listeners for 1000 list items
document.querySelectorAll('li').forEach(li => {
  li.addEventListener('click', handleClick);
});

// GOOD: 1 listener on the parent
document.querySelector('ul').addEventListener('click', (e) => {
  if (e.target.tagName === 'LI') {
    handleClick(e);
  }
});
Why it matters in practice:
  • Performance: 1 listener vs 1,000 listeners. Each listener consumes memory. In a list with thousands of items (think: a chat app, a data table), the difference is measurable — potentially saving several MB of memory.
  • Dynamic elements: If you add new <li> elements after the initial render, delegation automatically handles them. Without delegation, you have to manually attach listeners to every new element.
  • Memory leaks: Fewer listeners means fewer opportunities for memory leaks from forgotten removeEventListener calls.
Event propagation model (the full picture):
  1. Capture phase — event travels DOWN from window to the target (rarely used, but available via addEventListener('click', handler, true))
  2. Target phase — event reaches the actual clicked element
  3. Bubble phase — event travels UP from target to window (this is where delegation works)
  • event.stopPropagation() stops bubbling (use sparingly — it can break other listeners that depend on bubbling)
  • event.stopImmediatePropagation() stops bubbling AND prevents other listeners on the same element from firing
React’s approach: React uses a form of event delegation internally. In React 17+, events are delegated to the root DOM container (not document as in earlier versions). This is why React events behave slightly differently from native DOM events.What interviewers are really testing: Whether you understand event propagation deeply and can optimize event handling in large, dynamic UIs.Red flag answer: Describing delegation without mentioning event bubbling, or not knowing the difference between event.target and event.currentTarget.Follow-up:
  • What is the difference between event.target and event.currentTarget?
  • What events do NOT bubble, and how would you handle delegation for those?
  • How does React’s event system relate to native event delegation?
A closure is when a function retains access to variables from its outer (lexical) scope, even after the outer function has finished executing and its execution context is gone from the call stack.The precise mental model: Every function in JavaScript creates a closure at creation time. The function gets a hidden [[Environment]] reference pointing to the lexical environment (scope chain) where it was defined. When the function executes, it can access variables from that preserved environment.Classic example:
function createCounter() {
  let count = 0; // This variable is "closed over"
  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// count is not accessible from outside, but the inner function remembers it
Real-world uses:
  • Data privacy / encapsulation: The module pattern uses closures to create private variables. Before ES6 modules, this was THE way to avoid global namespace pollution.
  • Factory functions: createLogger('AUTH') returns a function that always prefixes logs with “AUTH” — the prefix is closed over.
  • Event handlers and callbacks: Every callback that references outer variables uses closures. setTimeout, addEventListener, Promise .then() handlers.
  • Partial application / currying: const add5 = createAdder(5) — the 5 is closed over.
  • React hooks internally: useState, useEffect, and every hook relies on closures to associate state with the specific component instance.
The gotcha — stale closures:
function setupButtons() {
  for (var i = 0; i < 5; i++) {
    buttons[i].addEventListener('click', function() {
      console.log(i); // Always logs 5, not 0-4
    });
  }
}
All handlers close over the SAME i (because var is function-scoped). By the time any button is clicked, the loop has finished and i is 5. Fix: use let (creates a new binding per iteration) or an IIFE.Stale closures in React:
const [count, setCount] = useState(0);
useEffect(() => {
  const interval = setInterval(() => {
    console.log(count); // Always logs the initial value — stale closure!
  }, 1000);
  return () => clearInterval(interval);
}, []); // Empty deps = closure captures count from first render only
Fix: use setCount(prev => prev + 1) (functional update) or include count in the dependency array.Memory implications: Closures keep their outer scope alive in memory. If a closure references a large object, that object will not be garbage collected until the closure itself is GC’d. This is a common source of memory leaks, especially with event listeners and timers.What interviewers are really testing: Whether you understand scope chains, can identify stale closure bugs, and know the practical applications beyond textbook definitions.Red flag answer: “A closure is a function inside a function” — this is imprecise (closures exist for ALL functions) and misses the key concept of retained scope.Follow-up:
  • How can closures cause memory leaks, and how do you prevent them?
  • Explain stale closures in React hooks and how to fix them.
  • How would you use closures to implement a private variable pattern?
Synchronous code executes line by line, blocking until each operation completes. Asynchronous code allows operations to run in the background, continuing execution without waiting.Why async exists: JavaScript is single-threaded (one call stack). If a network request took 2 seconds synchronously, the entire UI would freeze for 2 seconds — no scrolling, no clicking, no animations. Async solves this by offloading waiting to browser APIs (Web APIs) while the main thread stays free.The Event Loop — how it actually works:
  1. Synchronous code runs on the call stack
  2. Async operations (fetch, setTimeout, DOM events) are handled by Web APIs (outside the JS engine)
  3. When an async operation completes, its callback goes to the task queue (macrotask) or microtask queue
  4. The event loop checks: “Is the call stack empty? If yes, move the next task from the queue to the stack.”
  5. Microtasks (Promises, queueMicrotask, MutationObserver) have priority over macrotasks (setTimeout, setInterval, I/O)
The evolution of async patterns:
  • Callbacks — the original way. Problem: callback hell (deeply nested callbacks become unreadable)
  • Promises — chainable, cleaner error handling via .catch(). Problem: still can get messy with many .then() chains
  • async/await — syntactic sugar over Promises. Reads like synchronous code but is async. The current standard.
Critical nuance — microtasks vs macrotasks:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2
// Sync first (1, 4), then microtask (3), then macrotask (2)
This ordering question appears in interviews constantly. Understanding it means understanding the event loop.Error handling nuance:
  • Unhandled promise rejections used to silently fail. Modern Node.js (v15+) crashes on unhandled rejections. Always use .catch() or try/catch with await.
  • Promise.all fails fast — one rejection rejects all. Promise.allSettled waits for all to complete regardless of rejection. Choose based on your use case.
What interviewers are really testing: Whether you understand the event loop, can predict execution order, and know the practical differences between async patterns.Red flag answer: “Async means things run at the same time” — JavaScript is single-threaded, nothing truly runs in parallel (Web Workers aside). Async is about non-blocking, not parallelism.Follow-up:
  • Explain the event loop. What are microtasks vs macrotasks and why does the order matter?
  • What is the output of this code? [setTimeout 0, Promise.resolve, console.log]
  • When would you use Promise.all vs Promise.allSettled vs Promise.race?
== performs type coercion (converts operands to the same type before comparing). === performs strict comparison (no type conversion — both value AND type must match).Why == is dangerous — the coercion rules are wild:
0 == ""          // true (both coerce to 0)
0 == "0"         // true (string "0" coerces to number 0)
"" == "0"        // false (string comparison, different strings)
false == "0"     // true (false -> 0, "0" -> 0)
null == undefined // true (special rule in the spec)
null == 0        // false (null only coerces to undefined, not 0)
NaN == NaN       // false (NaN is not equal to anything, including itself)
These rules are defined in the ECMAScript spec’s Abstract Equality Comparison algorithm, and almost nobody memorizes them all. This is exactly why === is preferred.When == is actually acceptable:
  • value == null — this checks for both null AND undefined in one expression. Some style guides (including some internal ones at large companies) allow this as the one exception.
  • ESLint’s eqeqeq rule with "allow-null" option exists specifically for this pattern.
The Object.is() alternative:
  • Behaves like === but handles two edge cases differently: Object.is(NaN, NaN) returns true, and Object.is(+0, -0) returns false. React uses Object.is internally for state comparison in hooks.
What interviewers are really testing: Whether you understand JavaScript’s type coercion system and have the discipline to use strict equality.Red flag answer: ”== checks value, === checks value and type” — this is the surface answer. Not mentioning coercion rules or why == is problematic shows lack of depth.Follow-up:
  • Can you explain the coercion steps that make [] == false evaluate to true?
  • When is == actually appropriate to use?
  • What is Object.is() and how does it differ from ===?

4. React Fundamentals

React is a declarative, component-based JavaScript library for building user interfaces. Created by Facebook (now Meta) in 2013 and maintained as open source.The key ideas that define React:1. Declarative over Imperative: Instead of telling the browser step by step how to update the DOM (jQuery style: “find this element, change its text, add this class”), you describe WHAT the UI should look like for a given state, and React figures out HOW to update the DOM efficiently.
// Imperative (jQuery)
$('#counter').text(count);
if (count > 10) $('#counter').addClass('warning');

// Declarative (React)
<span className={count > 10 ? 'warning' : ''}>{count}</span>
2. Component-based Architecture: UIs are composed of reusable, self-contained components. Each component manages its own state and rendering logic. This enables team scaling — different developers own different components.3. Virtual DOM and Reconciliation: React maintains an in-memory representation of the DOM. On state change, it creates a new virtual DOM tree, diffs it against the previous one (reconciliation), and applies only the minimum necessary DOM mutations. This avoids the performance cost of full DOM re-renders.4. Unidirectional Data Flow: Data flows down through props (parent to child). Events flow up through callbacks. This makes data flow predictable and debuggable compared to two-way binding (Angular v1’s digest cycle was famously hard to debug).5. JSX: A syntax extension that looks like HTML but compiles to React.createElement() calls. It is not a template language — it is full JavaScript, which means you get type checking, full IDE support, and the ability to use any JS expression.React’s ecosystem evolution:
  • Class components -> Function components + Hooks (React 16.8+)
  • Client-side only -> Server Components (React 18+)
  • react-router for routing, Redux/Zustand/Jotai for state management
  • Next.js, Remix, Gatsby as full-stack frameworks built on React
What interviewers are really testing: Whether you understand React’s core philosophy (declarative, component-based, virtual DOM) and can articulate WHY these design choices matter, not just recite features.Red flag answer: “React is a framework for building web apps” — it is a library, not a framework. Also, not mentioning the virtual DOM, declarative nature, or unidirectional data flow.Follow-up:
  • What is the difference between a library and a framework, and why is React a library?
  • How does React’s unidirectional data flow compare to two-way data binding in Angular?
  • Why did React move from class components to hooks?
The Virtual DOM is a lightweight JavaScript object tree that mirrors the real DOM structure. It is React’s core performance optimization mechanism.How it works — the three-step process:Step 1: Render — When state or props change, React calls your component’s render function (or function body in function components). This produces a new virtual DOM tree — plain JavaScript objects describing the UI.Step 2: Diffing (Reconciliation) — React compares the new virtual tree with the previous one using its diffing algorithm. The algorithm uses two key heuristics:
  • Different element types produce different trees — React will tear down the old tree and build a new one (e.g., changing <div> to <span> does not try to patch)
  • key props tell React which children in a list are the same across renders, enabling efficient reordering instead of re-creating
Step 3: Commit — React applies only the calculated minimum set of DOM mutations to the real DOM. Changing one text node in a list of 1000 items results in exactly one DOM operation.Why this matters:
  • Direct DOM manipulation is expensive — the DOM is a complex C++ object model. Reading element.offsetHeight can trigger a full layout recalculation (reflow).
  • The virtual DOM lets React batch multiple state updates into a single DOM update pass (automatic batching since React 18).
  • The diffing algorithm is O(n) — not the theoretical O(n^3) for tree diff — because of the two heuristics above.
Common misconceptions to clear up:
  • “Virtual DOM is faster than the real DOM” — No. The virtual DOM adds overhead (creating JS objects, diffing). It is faster than NAIVE DOM manipulation (re-rendering the entire page). A hand-optimized imperative approach (like what Svelte compiles to) can be faster because it skips the diffing step entirely.
  • React 18’s concurrent features (Suspense, transitions) build on top of the virtual DOM — they allow React to pause, resume, and prioritize rendering work.
Fiber Architecture (React 16+): The virtual DOM is implemented using the Fiber data structure — a linked list of “fiber nodes” representing each component/element. This enables:
  • Incremental rendering: React can split rendering work across multiple frames, preventing long renders from blocking the main thread
  • Priority-based updates: User interactions (clicks) get higher priority than background updates (data fetching)
  • Concurrent Mode: Multiple versions of the virtual DOM can exist simultaneously
What interviewers are really testing: Whether you understand WHY the virtual DOM exists (not just WHAT it is), its limitations, and how Fiber changed the game.Red flag answer: “Virtual DOM is faster than the real DOM” — this oversimplification shows lack of depth. Also: not being able to explain the diffing algorithm’s heuristics.Follow-up:
  • Why is the virtual DOM not always faster than direct DOM manipulation? When would a framework like Svelte be faster?
  • What is React Fiber and how does it improve on the original reconciliation algorithm?
  • How do key props affect reconciliation performance, and what happens when you use array index as keys?
Props and state are the two mechanisms React uses to manage data, and understanding their difference is fundamental to React’s unidirectional data flow model.Props (properties):
  • Data passed from parent to child components
  • Read-only in the receiving component — a child should NEVER modify its own props
  • Changing props triggers a re-render of the child component
  • Think of props as function arguments — the parent calls <Child name="Alice" /> like calling Child({ name: "Alice" })
  • Can pass any JavaScript value: strings, numbers, objects, arrays, functions, even other components (render props pattern)
State:
  • Data managed within a component via useState (function components) or this.setState (class components)
  • Mutable — the component itself can update its state
  • State updates trigger a re-render of that component AND all its children (unless optimized with React.memo, useMemo, etc.)
  • State is asynchronoussetState does not update immediately. React batches state updates for performance.
The mental model:
Props = inputs from outside (read-only)
State = internal memory (read-write)

Parent owns the data -> passes down via props -> child renders based on props
Child needs internal data -> uses state -> updates trigger re-render
Child wants to communicate up -> calls a callback function received as a prop
When to lift state up: If two sibling components need the same data, the state should live in their common parent and be passed down as props. This is “lifting state up” — one of React’s most important patterns.Common pitfalls:
  • Derived state anti-pattern: Copying props into state (const [name, setName] = useState(props.name)) creates a stale copy that does not update when props change. Instead, derive values directly from props or use useMemo.
  • Too much state: Not everything needs to be state. If you can calculate it from existing state/props, do not store it as separate state.
  • Prop drilling: Passing props through many levels of components. Solutions: Context API, state management libraries (Zustand, Redux), or component composition.
What interviewers are really testing: Whether you understand React’s data flow model and know when to use state vs props vs derived values.Red flag answer: “Props are for passing data, state is for storing data” — correct but shallow. Not mentioning immutability of props, async nature of state updates, or derived state anti-patterns.Follow-up:
  • What is the derived state anti-pattern and how do you avoid it?
  • How do you decide whether data should be state, a prop, or derived from existing data?
  • What is prop drilling, and what are the trade-offs between Context API and state management libraries for solving it?
This is about who owns the form input’s source of truth — React state or the DOM.Controlled components:
  • React state is the single source of truth for the input value
  • Every keystroke triggers onChange -> updates state -> React re-renders the input with the new value
const [email, setEmail] = useState('');
<input value={email} onChange={(e) => setEmail(e.target.value)} />
  • Advantages: Full control over the value at all times. Can validate, transform, or reject input on every keystroke (e.g., formatting a phone number, preventing non-numeric characters). Can disable submit buttons based on validation state. Easier to test because the value is always in React state.
  • Trade-off: More code. Every input needs state and an onChange handler. For large forms with 20+ fields, this is boilerplate-heavy without a form library.
Uncontrolled components:
  • The DOM itself is the source of truth. You access the value when you need it (typically on submit) via a ref.
const emailRef = useRef();
<input ref={emailRef} defaultValue="" />
// On submit: emailRef.current.value
  • Advantages: Less code, simpler for basic forms. Better performance for large forms (no re-render on every keystroke). Easier integration with non-React code.
  • Trade-off: Cannot easily validate on every keystroke, cannot conditionally enable/disable UI based on current input values, harder to test.
The defaultValue vs value distinction:
  • value makes it controlled (React owns it)
  • defaultValue makes it uncontrolled (sets initial DOM value, React does not track changes)
  • Setting value without an onChange handler creates a read-only input that React warns about
In practice — form libraries:
  • React Hook Form uses uncontrolled components internally (refs + register) for performance, but provides a controlled API. This is why it is faster than Formik for large forms — fewer re-renders.
  • Formik uses controlled components by default, which can cause performance issues in forms with many fields (every keystroke re-renders the entire form unless optimized with <FastField>).
File inputs are ALWAYS uncontrolled<input type="file"> cannot be controlled because the value is read-only for security reasons.What interviewers are really testing: Whether you understand the trade-offs and can choose the right approach for a given situation, not just define both terms.Red flag answer: Not knowing which approach React Hook Form vs Formik uses internally, or saying “always use controlled” without acknowledging the performance trade-off.Follow-up:
  • When would you choose an uncontrolled component over a controlled one?
  • How does React Hook Form achieve better performance than Formik, architecturally?
  • What happens if you set value on an input without an onChange handler?
Keys are unique identifiers that help React’s reconciliation algorithm efficiently update lists. Without proper keys, React cannot tell which items were added, removed, or reordered — it has to re-render everything.How keys work in reconciliation:
  • When React diffs a list of children, it compares elements by their key. Same key = same element (possibly updated). Missing key = removed. New key = added.
  • With keys, React can reorder DOM elements instead of destroying and recreating them. This preserves component state, focus, scroll position, and animation state.
Why array index as key is problematic:
// BAD: Using index as key
items.map((item, index) => <Item key={index} {...item} />)
If you add an item to the beginning of the list:
  • Without proper keys (using index): React thinks every item changed (item at index 0 is different, index 1 is different…). It re-renders ALL items.
  • With unique keys: React knows the old items just shifted position. It inserts one new DOM node and moves the rest.
Worse: index keys corrupt state. If <Item> has internal state (e.g., a checked checkbox, text in an input), index keys can cause that state to “stick” to the wrong item after reordering. This is a real bug that ships to production.When index keys ARE acceptable:
  • The list is static (never reordered, filtered, or items added/removed)
  • Items have no internal state (no inputs, checkboxes, animations)
  • The list is used purely for display
What to use as keys:
  • Database IDs (ideal: key={user.id})
  • Unique business identifiers (email, SKU)
  • crypto.randomUUID() generated at data creation time (not at render time — generating in render creates new keys every render, defeating the purpose)
  • Never generate keys during render: key={Math.random()} re-creates every element on every render
What interviewers are really testing: Whether you understand WHY keys matter (reconciliation performance + state correctness), not just “you need to add a key to avoid the warning.”Red flag answer: “I just add index as the key to get rid of the React warning.” This shows no understanding of the actual purpose.Follow-up:
  • Show me a scenario where using array index as a key causes a bug with component state.
  • What happens to component state when a key changes?
  • Why should you never use Math.random() or Date.now() as a key in render?

5. React Hooks & Lifecycle

useEffect is React’s hook for synchronizing a component with external systems — things outside of React’s rendering flow like API calls, subscriptions, timers, or manual DOM manipulation.Mental model: “After React renders this component, also do this side effect.”The three dependency array patterns:
// 1. Runs after EVERY render (no dependency array)
useEffect(() => { /* runs every time */ });

// 2. Runs only ONCE after mount (empty array)
useEffect(() => { /* runs on mount */ }, []);

// 3. Runs when specific dependencies change
useEffect(() => { /* runs when userId changes */ }, [userId]);
Cleanup functions — preventing memory leaks:
useEffect(() => {
  const subscription = dataSource.subscribe(handleChange);
  return () => {
    subscription.unsubscribe(); // Cleanup runs before next effect or unmount
  };
}, [dataSource]);
The cleanup function runs:
  • Before the effect re-runs (when dependencies change)
  • When the component unmounts Common use: clearing intervals, unsubscribing from WebSockets, canceling fetch requests via AbortController.
Common pitfalls:1. Stale closures in effects: If you read state inside an effect but do not include it in dependencies, you get stale values. ESLint’s exhaustive-deps rule catches this — do NOT ignore it without understanding why.2. Infinite loops:
// BUG: runs on every render because effect updates state, which triggers re-render
useEffect(() => {
  setData(transformData(rawData));
}); // Missing dependency array!

// Also a bug: object/array in deps
useEffect(() => { ... }, [{ id: 1 }]); // New object ref every render = infinite loop
3. Using useEffect for derived state (anti-pattern):
// BAD — unnecessary effect
useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// GOOD — calculate during render
const fullName = firstName + ' ' + lastName;
If you can calculate something from existing state/props, do it during render, not in an effect.4. Race conditions in data fetching:
useEffect(() => {
  let cancelled = false;
  fetch(`/api/user/${id}`).then(res => res.json()).then(data => {
    if (!cancelled) setUser(data); // Only update if not stale
  });
  return () => { cancelled = true; };
}, [id]);
Without the cancellation flag, rapidly changing id can cause the wrong data to display. Better: use AbortController or a data-fetching library like TanStack Query.What interviewers are really testing: Whether you know when to use useEffect (and when NOT to), handle cleanup properly, and avoid the common antipatterns.Red flag answer: Not mentioning cleanup functions, or using useEffect for derived state calculations.Follow-up:
  • When should you NOT use useEffect? Give examples of things developers put in effects that should be elsewhere.
  • How do you handle race conditions in useEffect when fetching data?
  • What is the difference between useEffect and useLayoutEffect?
Both are memoization hooks — they cache values or functions between renders to avoid unnecessary recomputation or re-creation. But they solve different problems.useMemo — memoize a computed value:
const expensiveResult = useMemo(() => {
  return data.filter(item => item.active).sort((a, b) => a.name.localeCompare(b.name));
}, [data]);
  • Returns the result of the function
  • Only recomputes when dependencies change
  • Use case: expensive calculations (sorting/filtering large arrays), creating objects/arrays passed to child components (prevents unnecessary re-renders via reference equality)
useCallback — memoize a function reference:
const handleClick = useCallback((id) => {
  setItems(items => items.filter(item => item.id !== id));
}, []);
  • Returns the function itself (not its return value)
  • The function reference stays the same between renders unless dependencies change
  • useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
When they actually matter:
  • Passing callbacks to child components wrapped in React.memo — without useCallback, the new function reference breaks memoization
  • Passing objects/arrays as props — useMemo prevents creating new references that cause re-renders
  • Dependencies of other hooks — if a value is in useEffect’s dependency array, memoize it to avoid unnecessary effect runs
  • Genuinely expensive computations (not just simple calculations)
When they are overkill (the premature optimization trap):
  • Simple calculations (adding two numbers, string concatenation)
  • Functions not passed to memoized children
  • Inline event handlers on DOM elements (not custom components)
  • The overhead of useMemo/useCallback itself (comparing dependencies) can be MORE than just recomputing a simple value
The cost of memoization:
// This is WORSE than not memoizing:
const name = useMemo(() => firstName + ' ' + lastName, [firstName, lastName]);
// The dependency comparison is more expensive than the string concatenation
Profiling-first approach: Do not memoize by default. Use React DevTools Profiler to identify actual re-render bottlenecks, THEN apply memoization strategically.React Compiler (React 19+): The experimental React Compiler automatically memoizes values and functions, potentially making manual useMemo/useCallback unnecessary in the future.What interviewers are really testing: Whether you understand the trade-offs of memoization and can avoid premature optimization while knowing when it is genuinely needed.Red flag answer: “I wrap everything in useMemo/useCallback for performance” — this shows a lack of understanding of the overhead these hooks introduce.Follow-up:
  • When does memoization actually hurt performance instead of helping?
  • How does React.memo work with useCallback to prevent child re-renders?
  • What is the React Compiler and how might it change the way we use these hooks?
Forms in React range from simple (few inputs, basic validation) to complex (multi-step wizards, dynamic fields, async validation). Your approach should scale with complexity.Level 1 — Simple forms with controlled state:
const [form, setForm] = useState({ email: '', password: '' });
const handleChange = (e) => {
  setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
Good for: 2-5 fields, simple validation. Re-renders on every keystroke but irrelevant at this scale.Level 2 — React Hook Form (the modern standard):
const { register, handleSubmit, formState: { errors } } = useForm();
<input {...register("email", { required: true, pattern: /^\S+@\S+$/i })} />
  • Uses uncontrolled components internally (refs, not state)
  • Minimal re-renders — only the changed field re-renders, not the entire form
  • Built-in validation with declarative rules
  • Integrates with Zod/Yup for schema validation
  • ~8KB gzipped
  • Best for: Most production forms. 10+ fields, complex validation, dynamic fields.
Level 3 — Formik (older but still widely used):
  • Uses controlled components by default
  • More re-renders than React Hook Form (every keystroke re-renders the form)
  • <FastField> component for optimizing specific fields
  • Larger bundle, more boilerplate
  • When you see it: Legacy projects, projects started before React Hook Form matured
Validation strategies:
  • Client-side: Zod + React Hook Form resolver for type-safe validation. Define the schema once, get TypeScript types AND validation rules.
  • On-blur vs on-change vs on-submit: On-change is aggressive (errors while typing feel annoying). On-blur is standard (validate when user leaves the field). On-submit is simplest but least helpful.
  • Server-side validation is mandatory — client-side validation is for UX, server-side is for security. Never trust the client.
Accessibility in forms:
  • Every input needs a <label> with matching htmlFor/id
  • Error messages should be linked via aria-describedby
  • Use aria-invalid="true" on invalid fields
  • Focus management: move focus to the first error on submit failure
What interviewers are really testing: Whether you choose the right tool for the job and understand the performance implications of controlled vs uncontrolled approaches.Red flag answer: “I just use useState for each field” for a form with 20+ fields — this shows lack of awareness of form libraries and performance considerations.Follow-up:
  • Why does React Hook Form perform better than Formik for large forms?
  • How would you implement a multi-step wizard form with validation per step?
  • How do you handle async validation (e.g., checking if a username is taken)?
The Context API provides a way to share data across the component tree without passing props through every level (avoiding “prop drilling”).How it works:
// 1. Create context with a default value
const ThemeContext = createContext('light');

// 2. Provide the value at a higher level
<ThemeContext.Provider value={theme}>
  <App />
</ThemeContext.Provider>

// 3. Consume it anywhere below
const theme = useContext(ThemeContext);
What Context is good for:
  • Truly global, infrequently-changing data: theme, locale/language, authenticated user, feature flags
  • Dependency injection: providing services/configurations to deeply nested components
  • Compound components: <Select> providing shared state to <Option> children
The critical limitation — performance: When a Context value changes, every component that consumes that context re-renders, regardless of whether it uses the specific part of the value that changed.
// BAD: Every consumer re-renders when ANY property changes
<AppContext.Provider value={{ user, theme, locale, notifications }}>

// BETTER: Split into separate contexts
<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
Additional performance mitigation:
  • Split contexts by update frequency (user data changes rarely, notifications change often)
  • Memoize the context value: const value = useMemo(() => ({ user, logout }), [user])
  • Use React.memo on intermediate components to prevent cascading re-renders
Context is NOT a state management replacement: Context is a delivery mechanism (how data gets to components), not a state management solution (how data is updated and organized). For complex state with frequent updates (shopping cart, real-time data), use:
  • Zustand — minimal, performant, no boilerplate. Selectors prevent unnecessary re-renders.
  • Redux Toolkit — best for large apps with complex state logic, time-travel debugging, middleware
  • Jotai/Recoil — atomic state management, fine-grained reactivity
What interviewers are really testing: Whether you understand Context’s limitations and know when to reach for a dedicated state management solution.Red flag answer: “Context replaces Redux” — this is a common misconception. Context is a transport layer, Redux is a state management pattern. They solve different problems.Follow-up:
  • How does a Context value change cause re-renders, and how do you optimize this?
  • When would you use Context vs Zustand vs Redux?
  • How would you structure contexts in a large application to minimize unnecessary re-renders?
Reconciliation is React’s process of determining the minimum set of DOM operations needed to update the UI after a state or prop change. It is the core algorithm that makes React performant.The reconciliation algorithm in detail:Step 1: Re-render triggers A component re-renders when: state changes (setState/useState), props change, parent re-renders, or context value changes.Step 2: Virtual DOM diffing React compares the new virtual DOM tree with the previous one, starting from the root of the changed subtree.Step 3: The diffing heuristics:Heuristic 1 — Different element types produce different trees: If the root element changes type (e.g., <div> to <span>, or <ComponentA> to <ComponentB>), React destroys the entire old subtree (unmounts all children, destroys DOM nodes, triggers cleanup effects) and builds a new one from scratch. This is why you should not change component types dynamically at the same position.Heuristic 2 — Same type, different attributes: If the element type is the same, React keeps the same DOM node and only updates the changed attributes/props. For components, it calls render with the new props.Heuristic 3 — Lists and keys: For child lists, React uses keys to match old and new children. Without keys, React compares children by position (index), which forces re-creation when items are reordered.Why these heuristics work: The theoretical optimal tree diff is O(n^3) — impossibly slow for UI rendering. React’s heuristics reduce this to O(n) by making two assumptions that are true 99% of the time in real UIs: elements rarely change type, and developers provide keys for lists.Batching (React 18+): React 18 introduced automatic batching — multiple state updates in the same event handler, timeout, or promise are batched into a single re-render. Previously, only updates inside React event handlers were batched.
// React 18: ONE re-render, not three
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  setName('Alice');
}
Concurrent features: React 18’s concurrent rendering allows React to pause rendering work and resume it later, or abandon it if a higher-priority update arrives. This is powered by the Fiber architecture — each component is a “fiber” that can be processed incrementally.What interviewers are really testing: Whether you understand the algorithm’s trade-offs, the O(n) heuristic assumptions, and how keys/batching/concurrent rendering affect it.Red flag answer: “React checks what changed and updates the DOM” — too vague. Not mentioning the heuristics, key-based list diffing, or batching.Follow-up:
  • Why is React’s diffing O(n) instead of O(n^3), and what assumptions make that possible?
  • How does automatic batching in React 18 change rendering behavior compared to React 17?
  • What happens during reconciliation when you conditionally render different component types at the same position?

6. Next.js & Advanced Frontend

Next.js is a full-stack React framework by Vercel that provides server-side rendering, static site generation, API routes, file-based routing, and a suite of performance optimizations out of the box.Why Next.js exists (what it solves that React alone does not):
  • SEO: Client-rendered React apps (Create React App) send an empty HTML shell to the browser — search engine crawlers see nothing. Next.js can server-render or statically generate full HTML.
  • Performance: First Contentful Paint (FCP) is faster with SSR/SSG because the browser receives rendered HTML immediately, rather than waiting for JS to download, parse, and execute.
  • Routing: React has no built-in router. Next.js provides file-system based routing with zero configuration.
  • Full-stack capability: API routes let you build backend endpoints in the same project, eliminating the need for a separate Express/Fastify server for simple APIs.
App Router vs Pages Router (critical to understand):
  • Pages Router (legacy but stable): pages/ directory, getServerSideProps, getStaticProps, getStaticPaths. Each file = a route.
  • App Router (Next.js 13+, current standard): app/ directory, React Server Components by default, layout.tsx, loading.tsx, error.tsx conventions, streaming, server actions. Fundamentally different mental model.
Key features beyond routing:
  • next/image — automatic image optimization (WebP conversion, lazy loading, responsive sizing, CDN-based resizing)
  • Middleware — runs before a request is processed. Use for auth checks, redirects, A/B testing, geolocation
  • ISR (Incremental Static Regeneration) — static pages that revalidate in the background at specified intervals, combining SSG performance with data freshness
  • Server Actions — mutate data from the server directly in components, no API routes needed
  • Edge Runtime — deploy functions to edge locations (Cloudflare Workers-style) for low-latency responses
What interviewers are really testing: Whether you understand Next.js as more than “React with SSR” — it is a full-stack framework with a distinct architecture.Red flag answer: Only mentioning SSR, or confusing the App Router with the Pages Router, or not knowing about ISR.Follow-up:
  • What are the key differences between the App Router and Pages Router?
  • When would you choose ISR over SSR, and what are the trade-offs?
  • How does Next.js middleware work, and what are practical use cases?
These are the four rendering strategies in modern web development. Each has different trade-offs for performance, SEO, data freshness, and server cost.CSR (Client-Side Rendering):
  • Browser downloads a minimal HTML shell + JavaScript bundle
  • JavaScript renders the UI on the client
  • Pros: Simple deployment (static files), rich interactivity, no server cost
  • Cons: Poor SEO (empty HTML), slow First Contentful Paint (FCP), loading spinners
  • Use when: Dashboards, admin panels, authenticated apps where SEO does not matter
  • Example: Create React App, Vite + React
SSR (Server-Side Rendering):
  • Server renders full HTML on every request, sends it to the browser
  • Browser displays HTML immediately (fast FCP), then “hydrates” with JavaScript for interactivity
  • Pros: Great SEO, fast FCP, always fresh data
  • Cons: Server cost (every request = render work), slower Time to First Byte (TTFB) for expensive pages, server scaling complexity
  • Use when: Personalized content (user profiles), frequently changing data, SEO-critical pages with dynamic content
  • Next.js (App Router): Server Components are SSR by default. In Pages Router: getServerSideProps
SSG (Static Site Generation):
  • Pages rendered at build time, served as static HTML files from a CDN
  • Pros: Fastest possible performance (CDN-served, no server computation), highly cacheable, nearly free to host
  • Cons: Data is stale until next build, build times grow with page count (100K pages = long builds), no personalization
  • Use when: Blog posts, documentation, marketing pages, product listings that change infrequently
  • Next.js: generateStaticParams (App Router) or getStaticPaths + getStaticProps (Pages Router)
ISR (Incremental Static Regeneration) — Next.js specific:
  • Combines SSG and SSR: pages are statically generated but revalidate in the background after a configured time interval
  • User 1 gets the cached static page. After the revalidation period, the next request triggers a background regeneration. User 2 gets the fresh page.
  • Pros: SSG-level performance with near-real-time data freshness, no full rebuild needed
  • Cons: Data can be stale within the revalidation window, more complex mental model, cache invalidation can be tricky
  • Use when: E-commerce product pages (prices change daily, not per-second), news articles, any content that is “mostly static” with periodic updates
  • Next.js: revalidate: 60 in fetch options or page config
Decision framework:
Does it need SEO? No -> CSR
Yes -> Does data change per request? Yes -> SSR
No -> Does data change at all? No -> SSG
Yes -> ISR (with appropriate revalidation interval)
What interviewers are really testing: Whether you can choose the right rendering strategy for a given use case, articulating the trade-offs clearly.Red flag answer: Not knowing what ISR is, or saying “SSR is always better for SEO” without discussing the TTFB trade-off.Follow-up:
  • A client wants an e-commerce site with 50,000 product pages. Which rendering strategy do you choose and why?
  • What is the difference between on-demand revalidation and time-based revalidation in ISR?
  • How does streaming SSR (React 18) change the traditional SSR trade-offs?
Dynamic routing allows a single file to handle multiple URL paths using parameterized segments.Pages Router syntax:
  • pages/users/[id].js matches /users/1, /users/abc, etc.
  • Access the parameter: const { id } = useRouter().query (client) or from getServerSideProps({ params })
  • Catch-all routes: pages/docs/[...slug].js matches /docs/a, /docs/a/b/c (any depth)
  • Optional catch-all: pages/docs/[[...slug]].js also matches /docs (the base path)
App Router syntax (Next.js 13+):
  • app/users/[id]/page.tsx matches /users/1
  • Access parameter: function Page({ params }: { params: { id: string } })
  • Catch-all: app/docs/[...slug]/page.tsx
  • Route groups: app/(marketing)/about/page.tsx — the (marketing) folder creates a layout group without affecting the URL
  • Parallel routes: app/@dashboard/page.tsx — render multiple pages in the same layout simultaneously
  • Intercepting routes: app/(.)photo/[id]/page.tsx — intercept a route for modal behavior (like Instagram’s photo modal)
For SSG with dynamic routes:
// App Router
export async function generateStaticParams() {
  const users = await getUsers();
  return users.map(user => ({ id: user.id.toString() }));
}

// Pages Router
export async function getStaticPaths() {
  const users = await getUsers();
  return {
    paths: users.map(u => ({ params: { id: u.id.toString() } })),
    fallback: 'blocking', // or true, or false
  };
}
The fallback option (Pages Router) matters:
  • false: Only pre-generated paths work, 404 for everything else
  • true: Non-generated paths show a loading state, then render on first visit and cache
  • 'blocking': Non-generated paths SSR on first visit (no loading state), then cache. Best for SEO.
What interviewers are really testing: Whether you know both router patterns and understand fallback behavior for statically generated dynamic routes.Red flag answer: Only knowing Pages Router syntax, or not understanding the fallback options for SSG dynamic routes.Follow-up:
  • What is the difference between fallback: true and fallback: 'blocking' and when would you use each?
  • How do parallel routes and intercepting routes work in the App Router?
  • How would you implement a nested dynamic route like /blog/[category]/[slug]?
Next.js performance optimization spans build time, server time, network delivery, and client-side runtime.1. Image optimization:
  • Use next/image exclusively — it handles WebP/AVIF conversion, responsive srcset, lazy loading, and blur placeholder
  • Set explicit width and height (or use fill) to prevent Cumulative Layout Shift (CLS)
  • Configure remotePatterns in next.config.js for external image domains
  • Use priority prop on above-the-fold images (hero images) to disable lazy loading
2. Rendering strategy selection:
  • Default to SSG/ISR for content pages — served from CDN edge, fastest possible
  • Use SSR only when per-request personalization is needed
  • Stream long-running server components with <Suspense> boundaries
  • Use loading.tsx for route-level loading states
3. Code splitting and bundle optimization:
  • Next.js automatically code-splits by route
  • Use dynamic() (equivalent to React.lazy) for heavy components: const Chart = dynamic(() => import('./Chart'), { ssr: false })
  • ssr: false is key for client-only libraries (D3, maps, rich text editors) — avoids server-side import errors
  • Analyze bundle with @next/bundle-analyzer — identify unexpected large dependencies. A common find: importing all of lodash instead of lodash/debounce
4. Data fetching optimization:
  • Use React Server Components to fetch data on the server (zero client-side JavaScript for data fetching)
  • Colocate data fetching with components — avoid waterfall requests by fetching in parallel
  • Use fetch with next: { revalidate: N } for ISR caching
  • Implement next: { tags: ['products'] } + revalidateTag('products') for on-demand cache invalidation
5. Font optimization:
  • Use next/font to self-host Google Fonts (eliminates external network request)
  • display: 'swap' prevents invisible text during font loading
  • Reduces CLS from font loading
6. Third-party script management:
  • Use next/script with strategy="lazyOnload" for analytics, chat widgets, etc.
  • strategy="afterInteractive" for scripts needed soon but not blocking render
  • Avoid loading heavy third-party scripts synchronously
Measurable targets (what to aim for):
  • Largest Contentful Paint (LCP) < 2.5s
  • First Input Delay (FID) < 100ms
  • Cumulative Layout Shift (CLS) < 0.1
  • Time to First Byte (TTFB) < 800ms
What interviewers are really testing: Whether you take a systematic, measurable approach to performance or just list buzzwords.Red flag answer: Only mentioning next/image without discussing rendering strategies, bundle analysis, or Core Web Vitals targets.Follow-up:
  • How would you diagnose a slow LCP on a Next.js page?
  • What is the difference between dynamic() with ssr: false and a regular React.lazy?
  • How does React Server Components change the performance optimization landscape compared to client-side React?
Environment variables in Next.js follow a specific security model that separates server-only from client-exposed values.The NEXT_PUBLIC_ prefix rule:
  • Variables without the prefix (e.g., DATABASE_URL) are only available on the server (Server Components, API routes, getServerSideProps). They are never included in the client bundle.
  • Variables with NEXT_PUBLIC_ prefix (e.g., NEXT_PUBLIC_API_URL) are inlined into the client JavaScript bundle at build time. They are visible to anyone who inspects your JavaScript.
  • Critical security implication: Never put secrets (API keys, database credentials, JWT secrets) in NEXT_PUBLIC_ variables. They will be exposed in client-side code.
File hierarchy and loading order:
  1. .env — default for all environments
  2. .env.local — local overrides (gitignored by default)
  3. .env.development / .env.production / .env.test — environment-specific
  4. .env.development.local / .env.production.local — environment-specific local overrides
  • Priority: .env.*.local > .env.local > .env.* > .env
  • process.env.NODE_ENV is automatically set (development, production, test)
Common mistakes:
  • Putting DATABASE_URL as NEXT_PUBLIC_DATABASE_URL — now your database credentials are in the browser bundle
  • Not restarting the dev server after adding new env vars — Next.js reads them at startup
  • Using process.env.SOME_VAR in client components without NEXT_PUBLIC_ — it will be undefined on the client (no error, just silently missing)
Runtime vs build-time env vars:
  • NEXT_PUBLIC_ variables are replaced at build time (string replacement in the bundle). If you deploy the same build to staging and production, the values from the build-time environment are baked in.
  • Server-side variables are read at runtime via process.env
  • For runtime client-side config, use Next.js runtime configuration or fetch from an API endpoint
Production best practices:
  • Store secrets in your deployment platform (Vercel Environment Variables, AWS Secrets Manager, Doppler)
  • Use .env.local for local development only
  • Validate env vars at startup using a schema (Zod + t3-env library is the gold standard)
  • Never commit .env.local to git (it should be in .gitignore)
What interviewers are really testing: Whether you understand the security boundary between server and client, and whether you handle secrets properly.Red flag answer: Not knowing the NEXT_PUBLIC_ prefix distinction, or suggesting it is fine to put API secrets in client-exposed env vars.Follow-up:
  • What happens if you access a non-NEXT_PUBLIC variable in a client component?
  • How would you validate that all required environment variables are present at build/startup time?
  • What is the difference between build-time and runtime environment variables, and when does this distinction matter?

7. TypeScript in Frontend

TypeScript is a statically-typed superset of JavaScript that compiles to plain JavaScript. It was created by Microsoft and has become the industry standard for serious frontend development.Why TypeScript matters — the real reasons, not the marketing:1. Catches bugs at compile time, not runtime:
  • JavaScript: user.naem silently returns undefined. You discover this from a bug report in production.
  • TypeScript: user.naem shows a red squiggly immediately. The typo never reaches git.
  • Studies (Airbnb, Bloomberg) show TypeScript prevents 15-20% of bugs that would otherwise reach production.
2. Self-documenting code:
// Without TS: What does this function accept? What does it return? Read the implementation.
function processOrder(order, options) { ... }

// With TS: The signature IS the documentation
function processOrder(order: Order, options: ProcessOptions): Promise<Receipt> { ... }
3. Refactoring confidence: Rename a property in a type, and TypeScript shows every file that needs updating. Without types, renaming user.name to user.displayName across a 500-file codebase is a manual search-and-pray operation.4. Better IDE experience: Autocomplete, jump-to-definition, inline documentation, parameter hints — all powered by the type system. This alone saves hours per week.5. Team scaling: Types serve as contracts between components/modules/teams. Team A defines an API type, Team B consumes it — if A changes the type, B’s code fails at compile time, not in QA.The trade-offs (be honest about these):
  • Learning curve — generics, utility types, and type narrowing take time to master
  • Build step required — cannot just run .ts files in the browser (though tools like Bun and Deno can execute TS directly)
  • Occasional type gymnastics — complex types (conditional types, mapped types) can be harder to read than the code they protect
  • Third-party type quality varies — @types/ packages can be outdated or incorrect
What interviewers are really testing: Whether you see TypeScript as a productivity tool (not just “Java for JavaScript”) and can articulate specific benefits with examples.Red flag answer: “TypeScript adds types to JavaScript” — too surface-level. Also: “TypeScript makes code slower” (it does not — it compiles to JavaScript, there is zero runtime overhead).Follow-up:
  • Does TypeScript have any runtime overhead? Why or why not?
  • What is the strict mode in tsconfig and what does it enable?
  • When might TypeScript not be the right choice for a project?
Both interface and type define the shape of data in TypeScript, but they have different capabilities and use cases.Interface:
interface User {
  id: number;
  name: string;
  email: string;
}

// Declaration merging — interfaces with the same name merge
interface User {
  role: string; // Added to the original User interface
}

// Extends (inheritance)
interface Admin extends User {
  permissions: string[];
}
Type alias:
type User = {
  id: number;
  name: string;
  email: string;
};

// Union types (interface CANNOT do this)
type Status = 'active' | 'inactive' | 'pending';
type Result = Success | Error;

// Intersection (similar to extends)
type Admin = User & { permissions: string[] };

// Mapped types, conditional types, template literals
type Readonly<T> = { readonly [P in keyof T]: T[P] };
Key differences:
Featureinterfacetype
Object shapesYesYes
Declaration mergingYesNo (duplicate identifier error)
extends keywordYesNo (use & intersection)
Union typesNoYes
Primitive aliasesNoYes (type ID = string)
Mapped typesNoYes
Conditional typesNoYes
Tuple typesNoYes (type Pair = [string, number])
When to use which:
  • interface for object shapes and public APIs: Declaration merging is useful for library authors (consumers can extend your interfaces). Extends keyword reads more naturally for OOP-style hierarchies.
  • type for everything else: Unions, intersections, utility types, complex type transformations, primitives, tuples.
  • Team convention matters most. Many teams standardize on one. The React community tends toward type for props; library authors tend toward interface for extensibility.
Performance note: The TypeScript compiler can cache interface types more efficiently than complex type aliases. For extremely large codebases (500K+ lines), preferring interfaces for simple object shapes can improve compile times slightly.What interviewers are really testing: Whether you understand the technical differences (especially declaration merging and union types) and can justify your choice.Red flag answer: “They are basically the same” — while similar for simple cases, their capabilities diverge significantly for advanced use cases.Follow-up:
  • What is declaration merging and when is it useful?
  • Can you create a union type with interfaces? Why or why not?
  • When would you prefer type over interface for React component props?
Generics let you write type-safe code that works with any type without losing type information. They are TypeScript’s version of parametric polymorphism.The problem generics solve:
// Without generics: loses type information
function first(arr: any[]): any {
  return arr[0];
}
const result = first([1, 2, 3]); // result is 'any' — no autocomplete, no safety

// With generics: preserves type information
function first<T>(arr: T[]): T {
  return arr[0];
}
const result = first([1, 2, 3]); // result is 'number' — full type safety
Common generic patterns:1. Generic functions:
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}
2. Generic constraints (extends):
// T must have a 'length' property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // Works: strings have length
longest(10, 20);        // Error: numbers have no length
3. Generic React components:
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return <>{items.map(renderItem)}</>;
}

// Usage: T is inferred as User
<List items={users} renderItem={(user) => <span>{user.name}</span>} />
4. Generic with default types:
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}
5. keyof with generics (type-safe property access):
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
getProperty(user, 'name'); // Returns string
getProperty(user, 'foo');  // Compile error: 'foo' is not a key of User
Real-world usage in React/frontend:
  • API hooks: useQuery<User>('/api/user') returns typed data
  • Form libraries: useForm<FormValues>() gives type-safe field access
  • Table components: <DataTable<Product> columns={...} data={products} />
  • State management: Zustand stores use generics for typed state
What interviewers are really testing: Whether you can write reusable, type-safe abstractions — not just consume them.Red flag answer: Only showing the basic identity<T> example without demonstrating constraints, keyof, or real-world React usage.Follow-up:
  • How do generic constraints work, and when would you use extends in a generic?
  • Write a generic type that makes all properties of an object optional except for id.
  • How would you type a generic API hook that fetches data and returns typed results?
Utility types are built-in TypeScript type transformations that modify existing types without redefining them. They are the building blocks of advanced type manipulation.Essential utility types:Partial<T> — Makes all properties optional
interface User { id: number; name: string; email: string; }
type UpdateUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
// Use case: PATCH endpoints where you only send changed fields
Required<T> — Makes all properties required (opposite of Partial)Pick<T, K> — Select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }
// Use case: List views that only need a subset of fields
Omit<T, K> — Remove specific properties
type CreateUser = Omit<User, 'id'>;
// { name: string; email: string; }
// Use case: Create forms where id is auto-generated
Readonly<T> — Makes all properties readonly
type ImmutableUser = Readonly<User>;
// Attempting to reassign any property = compile error
// Use case: Redux state, config objects
Record<K, V> — Creates an object type with keys K and values V
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// Use case: Lookup maps, dictionaries
Exclude<T, U> / Extract<T, U> — Filter union types
type Status = 'active' | 'inactive' | 'pending' | 'deleted';
type ActiveStatus = Exclude<Status, 'deleted'>;
// 'active' | 'inactive' | 'pending'
ReturnType<T> — Extract the return type of a function
function getUser() { return { id: 1, name: 'Alice' }; }
type User = ReturnType<typeof getUser>;
// { id: number; name: string; }
// Use case: Inferring types from existing functions without manual duplication
NonNullable<T> — Removes null and undefined from a typeComposing utility types (where it gets powerful):
// Create form type: all fields optional except id (which is omitted entirely)
type UserForm = Partial<Omit<User, 'id'>>;

// Make only certain fields required
type UserWithRequiredEmail = Omit<Partial<User>, 'email'> & Pick<User, 'email'>;
What interviewers are really testing: Whether you can leverage the type system to avoid manual type duplication and keep types DRY.Red flag answer: Only knowing Partial and Readonly, or not being able to compose utility types together.Follow-up:
  • How would you create a type where all properties are optional EXCEPT id and email?
  • What is the difference between Omit and Exclude?
  • How does ReturnType work under the hood (hint: conditional types and infer)?
TypeScript transforms React development from “hope it works” to “know it compiles, know it works.” Here is the concrete value at each layer:1. Type-safe props:
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
  children: React.ReactNode;
  disabled?: boolean;
}

function Button({ variant, size = 'md', onClick, children, disabled }: ButtonProps) {
  return <button className={`btn-${variant} btn-${size}`} onClick={onClick} disabled={disabled}>{children}</button>;
}

// Compile error: 'warning' is not assignable to type 'primary' | 'secondary' | 'danger'
<Button variant="warning" onClick={() => {}}>Click</Button>
2. Type-safe hooks:
// useState infers type, or you can be explicit
const [user, setUser] = useState<User | null>(null);

// useRef with proper typing
const inputRef = useRef<HTMLInputElement>(null);

// useReducer with discriminated union actions
type Action =
  | { type: 'INCREMENT'; payload: number }
  | { type: 'RESET' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT': return { count: state.count + action.payload };
    case 'RESET': return { count: 0 };
  }
}
3. Type-safe API responses:
interface ApiResponse<T> {
  data: T;
  error: string | null;
}

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}
// Autocomplete works on the returned data: response.data.name
4. Type-safe event handlers:
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
};
5. Generic components for reusability:
interface SelectProps<T> {
  options: T[];
  getLabel: (item: T) => string;
  getValue: (item: T) => string;
  onChange: (item: T) => void;
}

function Select<T>({ options, getLabel, getValue, onChange }: SelectProps<T>) { ... }
Common TypeScript + React patterns:
  • React.FC vs function declaration — React.FC implicitly includes children (pre-React 18) and does not support generics well. Most teams now prefer explicit function declarations with typed props.
  • ComponentPropsWithoutRef<'button'> — extend native HTML element props
  • as const for literal types — const sizes = ['sm', 'md', 'lg'] as const creates a readonly tuple type
What interviewers are really testing: Whether you use TypeScript beyond basic prop types — hooks, events, generics, API responses.Red flag answer: Only showing interface Props { name: string } without demonstrating hooks typing, event typing, or generic components.Follow-up:
  • How would you type a component that accepts all native button props plus custom ones?
  • What is the difference between React.FC<Props> and a regular function with typed props?
  • How do you handle typing for a component that can render as different HTML elements (polymorphic components)?

8. Frontend Performance & Optimization

Frontend performance is measured by Core Web Vitals and affects user experience, conversion rates, and SEO rankings. Google’s data shows that as page load time goes from 1s to 3s, bounce rate increases by 32%.Systematic optimization framework (in priority order):1. Reduce what you ship (biggest impact):
  • Bundle analysis: Use webpack-bundle-analyzer or @next/bundle-analyzer to visualize your bundle. Common finds: shipping all of moment.js (330KB) when you need date-fns (tree-shakeable), or importing entire lodash (70KB) instead of lodash/debounce (1KB).
  • Tree shaking: Use ES modules (import/export) not CommonJS (require). Modern bundlers (Webpack 5, Vite, Turbopack) eliminate unused exports.
  • Code splitting: Split by route (automatic in Next.js), split heavy components with React.lazy() / dynamic().
  • Dependency audit: npm ls --all | wc -l — if you have 1500+ dependencies, some are likely unused.
2. Optimize rendering:
  • Minimize re-renders: React.memo for pure components, useMemo/useCallback where profiling shows impact, avoid passing new object/array literals as props.
  • Virtualize long lists: react-window or @tanstack/virtual for lists with 100+ items. Rendering 10,000 DOM nodes kills scrolling performance.
  • Avoid layout thrashing: Batch DOM reads and writes. Do not interleave element.offsetHeight reads with style changes.
  • Use CSS containment: contain: layout paint on independent sections.
3. Optimize assets:
  • Images: WebP/AVIF format, responsive srcset, lazy loading, proper sizing (do not serve a 4000px image in a 400px container). next/image handles this automatically.
  • Fonts: Self-host (next/font), use font-display: swap, subset to only needed characters.
  • Videos: Lazy load, use poster images, prefer streaming formats.
4. Optimize network:
  • Caching: Set proper Cache-Control headers. Immutable assets (hashed filenames) get max-age=31536000. HTML gets no-cache or short TTL.
  • CDN: Serve static assets from edge locations (Vercel, Cloudflare, CloudFront).
  • Preconnect/Prefetch: <link rel="preconnect" href="https://api.example.com"> for known third-party origins.
  • HTTP/2 or HTTP/3: Multiplexing eliminates the need for domain sharding or sprite sheets.
5. Measure, do not guess:
  • Chrome DevTools Performance tab for runtime profiling
  • Lighthouse for overall audit
  • Web Vitals library for real-user monitoring (RUM)
  • React DevTools Profiler for component-level render analysis
What interviewers are really testing: Whether you take a data-driven approach to performance and can prioritize high-impact optimizations.Red flag answer: Listing random optimizations without a framework or mentioning measurement.Follow-up:
  • Walk me through how you would diagnose and fix a slow page from scratch.
  • What is the difference between lab data (Lighthouse) and field data (CrUX), and why does it matter?
  • How would you set up performance budgets in a CI/CD pipeline?
Lazy loading defers the loading of components or resources until they are actually needed, reducing the initial bundle size and improving Time to Interactive (TTI).React.lazy() for component-level code splitting:
const HeavyChart = React.lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <Suspense fallback={<div>Loading chart...</div>}>
      <HeavyChart />
    </Suspense>
  );
}
  • import() creates a separate webpack chunk that loads on demand
  • Suspense provides a fallback UI while the chunk loads
  • The component’s JavaScript only downloads when <HeavyChart> first renders
Next.js dynamic() — enhanced lazy loading:
const Map = dynamic(() => import('./Map'), {
  loading: () => <MapSkeleton />,
  ssr: false, // Critical for client-only libraries like Mapbox, D3
});
  • ssr: false prevents the component from rendering on the server (avoids window is not defined errors)
  • Built-in loading component option
Route-level lazy loading:
  • Next.js automatically code-splits per route — each page is its own chunk
  • React Router: lazy(() => import('./routes/Dashboard')) in route definitions
  • Users only download JavaScript for the routes they visit
Intersection Observer for scroll-based lazy loading:
function LazySection({ children }) {
  const [isVisible, ref] = useInView({ triggerOnce: true });
  return <div ref={ref}>{isVisible ? children : <Placeholder />}</div>;
}
Use libraries like react-intersection-observer for components that should only load when scrolled into view (below-the-fold content, infinite scroll lists).Image lazy loading:
  • Native: <img loading="lazy" /> (supported in all modern browsers)
  • next/image does this by default (except when priority is set)
Trade-offs and gotchas:
  • Waterfall loading: If lazy-loaded component A loads and then triggers loading of component B, you get a sequential waterfall. Preload critical lazy chunks: import(/* webpackPreload: true */ './HeavyChart')
  • Loading state flicker: If the chunk loads in <200ms, showing a spinner creates an unpleasant flash. Use a minimum delay or skeleton screens.
  • Error boundaries: What happens if the chunk fails to load (network error)? Wrap lazy components in error boundaries to show a retry UI.
What interviewers are really testing: Whether you understand the trade-offs of lazy loading and handle edge cases like error states and loading waterfalls.Red flag answer: Only mentioning React.lazy without discussing Suspense, error handling, or when NOT to lazy load (small components where the overhead is not worth it).Follow-up:
  • When would lazy loading actually hurt performance instead of helping?
  • How do you handle errors when a lazy-loaded chunk fails to download?
  • What is the difference between React.lazy and Next.js dynamic?
Both are rate-limiting techniques for controlling how often a function executes in response to rapid events. They solve different problems.Debouncing — “wait until the user stops”: The function only executes after the user stops triggering the event for a specified delay period. Every new trigger resets the timer.
function debounce(fn, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Use case: search-as-you-type
const handleSearch = debounce((query) => {
  fetch(`/api/search?q=${query}`); // Only fires after 300ms of no typing
}, 300);
When to use: Search inputs (wait until user stops typing), window resize handlers, auto-save (save after user stops editing).Throttling — “execute at most once per interval”: The function executes at most once within a specified time window, regardless of how many times the event fires.
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// Use case: scroll position tracking
const handleScroll = throttle(() => {
  updateScrollIndicator(window.scrollY);
}, 100); // Updates at most every 100ms
When to use: Scroll handlers, mouse move tracking, resize handling where you need continuous updates (not just the final state), rate-limiting API calls.The key difference:
  • Debounce groups multiple calls into ONE call at the END of the burst
  • Throttle allows ONE call per time interval DURING the burst
Analogy: Debouncing is like an elevator that waits for people to stop pressing buttons before moving. Throttling is like a bus that departs every 10 minutes regardless of how many people are waiting.Real-world performance numbers: A scroll event fires ~60 times per second (every 16.67ms on a 60Hz display). Without throttling, an expensive handler runs 60 times per second. Throttled to 100ms, it runs 10 times per second — an 83% reduction in work with minimal visual difference.In React with hooks:
// Using lodash/debounce with useCallback to maintain reference stability
const debouncedSearch = useCallback(
  debounce((query) => fetchResults(query), 300),
  []
);
// Or use the useDebouncedCallback hook from 'use-debounce' library
What interviewers are really testing: Whether you can choose the right technique for a given problem and implement it correctly.Red flag answer: Confusing debounce with throttle, or not being able to explain when to use each.Follow-up:
  • You need to implement auto-save that saves 2 seconds after the user stops typing, but also saves every 30 seconds while they are continuously typing. How would you combine debounce and throttle?
  • How would you implement debouncing in a React component without causing stale closure issues?
  • What is requestAnimationFrame throttling and when is it better than time-based throttling?
Bundle splitting (code splitting) divides your application’s JavaScript into multiple smaller chunks loaded on demand, instead of one massive bundle.Why it matters (with real numbers): A typical React SPA might produce a 500KB+ JavaScript bundle. On a 3G connection (1.6 Mbps), that is ~2.5 seconds just to download, plus parse and execute time. Splitting into a 100KB initial chunk + on-demand chunks means the user sees content 5x faster.Types of splitting:1. Route-based splitting (highest impact, automatic in Next.js): Each route gets its own chunk. Visiting /dashboard does not download code for /settings.2. Component-based splitting: Heavy components (charts, rich text editors, maps) loaded via React.lazy() or dynamic().3. Vendor splitting: Separate node_modules into their own chunk. Browser caches this independently — your app code changes frequently but dependencies change rarely.
// Webpack config
optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: { test: /node_modules/, name: 'vendors', chunks: 'all' }
    }
  }
}
4. Dynamic imports for conditional features:
// Only load PDF library when user clicks "Export PDF"
async function exportPDF() {
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();
  // ...
}
Caching strategy impact: Splitting enables granular caching. With content-hashed filenames (app.a1b2c3.js), unchanged chunks stay cached. A one-line code change only invalidates the changed chunk, not the entire bundle. This can reduce repeat-visit download sizes by 80-90%.Tools for analysis:
  • webpack-bundle-analyzer — visual treemap of your bundle
  • source-map-explorer — analyze bundle composition from source maps
  • @next/bundle-analyzer — Next.js-specific wrapper
  • Chrome DevTools Coverage tab — shows what percentage of downloaded JS is actually executed on the current page
What interviewers are really testing: Whether you understand the impact on loading performance and caching, not just the mechanism.Red flag answer: Only describing the concept without mentioning caching benefits, real performance impact, or analysis tools.Follow-up:
  • How does bundle splitting interact with HTTP caching strategies?
  • What is the overhead of having too many small chunks (hint: HTTP/1.1 vs HTTP/2)?
  • How would you analyze your bundle to find the best splitting opportunities?
Images are typically the largest payload on a web page — often 50-70% of total page weight. Optimizing them has the highest performance ROI.1. Format selection (biggest impact):
  • WebP: 25-35% smaller than JPEG at equivalent quality. Supported by 97%+ of browsers.
  • AVIF: 50% smaller than JPEG, better than WebP. Supported by 90%+ of browsers. Slower to encode.
  • Use <picture> for progressive enhancement:
<picture>
  <source srcset="image.avif" type="image/avif" />
  <source srcset="image.webp" type="image/webp" />
  <img src="image.jpg" alt="Description" />
</picture>
  • SVG for icons, logos, illustrations — infinitely scalable, tiny file size for simple graphics
2. Responsive images (srcset and sizes):
<img
  srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
  src="image-800.jpg"
  alt="Product photo"
/>
The browser selects the appropriate image based on viewport width and device pixel ratio. Without this, a mobile user downloads a 2MB desktop image for a 300px-wide slot.3. Lazy loading:
  • Native: loading="lazy" attribute (supported in all modern browsers)
  • Only loads images when they are near the viewport
  • Never lazy load above-the-fold images — these should load immediately (use loading="eager" or Next.js priority prop)
4. Dimension specification:
  • ALWAYS set width and height attributes (or use CSS aspect-ratio)
  • Without dimensions, the browser does not know the image size until it loads, causing Cumulative Layout Shift (CLS) — content jumps around as images pop in
5. Next.js Image component:
import Image from 'next/image';
<Image src="/hero.jpg" width={1200} height={630} alt="Hero" priority />
  • Automatic format conversion (WebP/AVIF)
  • Automatic responsive sizing
  • Built-in lazy loading
  • Blur placeholder option (placeholder="blur")
  • On-demand image optimization API (resizes at the edge, not at build time)
6. CDN and compression:
  • Serve images from a CDN (Cloudflare Images, Cloudinary, Imgix, Vercel Image Optimization)
  • These services resize, format-convert, and cache images at edge locations
  • Cloudinary example: https://res.cloudinary.com/demo/image/upload/w_400,f_auto,q_auto/sample.jpg — auto-format, auto-quality, 400px wide
What interviewers are really testing: Whether you understand the full optimization pipeline from format to delivery, and specifically how it affects Core Web Vitals.Red flag answer: Only mentioning “use lazy loading” without discussing formats, responsive images, or CLS prevention.Follow-up:
  • What is Cumulative Layout Shift and how do images cause it?
  • How would you handle image optimization for user-uploaded content at scale?
  • When would you choose a CDN-based image service over Next.js built-in image optimization?

9. Accessibility & Testing

Web accessibility means building applications that are usable by everyone, including people with visual, auditory, motor, or cognitive disabilities. This affects approximately 15-20% of the global population — and temporarily disabled users (broken arm, bright sunlight on screen) expand that even further.Why it is non-negotiable (not just “nice to have”):
  • Legal requirement: ADA (US), EAA (EU), AODA (Canada). Web accessibility lawsuits increased 300%+ between 2018-2023. Target, Dominos, and Winn-Dixie all faced major lawsuits.
  • Business impact: Accessible sites reach more users. The disability community controls ~$8 trillion in annual disposable income globally.
  • SEO benefit: Accessible sites rank better — semantic HTML, alt text, and proper heading hierarchy are both accessibility AND SEO best practices.
WCAG 2.1 levels:
  • Level A: Minimum accessibility (alt text, keyboard navigation, no seizure-inducing content)
  • Level AA: Standard target for most organizations (color contrast 4.5:1, resize to 200%, focus visible)
  • Level AAA: Enhanced (contrast 7:1, sign language for video). Rarely required as a blanket target.
The four WCAG principles (POUR):
  • Perceivable: Information must be presentable in ways all users can perceive (alt text, captions, sufficient contrast)
  • Operable: UI must be operable by all (keyboard navigation, no time limits, no seizure triggers)
  • Understandable: Content and UI must be understandable (clear language, predictable navigation, error identification)
  • Robust: Content must be robust enough for assistive technologies to interpret (valid HTML, ARIA attributes)
Practical implementation checklist:
  • Semantic HTML (<nav>, <main>, <button> not <div onClick>)
  • All images have descriptive alt text (or alt="" for decorative images)
  • Color contrast ratio minimum 4.5:1 for normal text, 3:1 for large text
  • Full keyboard navigation (Tab, Shift+Tab, Enter, Escape, arrow keys)
  • Visible focus indicators (never outline: none without a replacement)
  • ARIA labels for interactive elements without visible text (aria-label, aria-labelledby)
  • Skip navigation link for keyboard users
  • Form inputs with associated <label> elements
  • Error messages that are programmatically associated with inputs (aria-describedby)
What interviewers are really testing: Whether accessibility is part of your development workflow or an afterthought.Red flag answer: “We add ARIA labels at the end” or treating accessibility as a checkbox exercise rather than a design principle.Follow-up:
  • Walk me through how a screen reader user would navigate a page you have built.
  • What is the difference between aria-label, aria-labelledby, and aria-describedby?
  • How do you test for accessibility during development, not just at the end?
React testing follows a testing pyramid — lots of unit tests, fewer integration tests, fewest E2E tests. The philosophy has shifted from testing implementation details to testing user behavior.Testing Library philosophy (the modern standard):
“The more your tests resemble the way your software is used, the more confidence they can give you.” — Kent C. Dodds
The testing stack:
  • Jest or Vitest — test runner and assertion library
  • React Testing Library (RTL) — renders components and provides user-centric queries
  • MSW (Mock Service Worker) — mocks API calls at the network level
  • Playwright or Cypress — E2E testing in real browsers
Unit testing with RTL:
import { render, screen, fireEvent } from '@testing-library/react';

test('increments counter on click', () => {
  render(<Counter />);
  const button = screen.getByRole('button', { name: /increment/i });
  fireEvent.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Key RTL query priority (use in this order):
  1. getByRole — queries by ARIA role (most accessible, most robust)
  2. getByLabelText — for form inputs
  3. getByPlaceholderText — fallback for inputs
  4. getByText — for non-interactive content
  5. getByTestId — last resort (test-specific, not user-facing)
What to test vs what not to test:
  • Test: User interactions, rendered output, conditional rendering, error states, form validation, accessibility (role queries prove accessibility)
  • Do not test: Implementation details (state variable names, hook call counts, internal methods), CSS styling, third-party library internals
Integration testing with MSW:
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json([{ id: 1, name: 'Alice' }]));
  })
);

test('displays users from API', async () => {
  render(<UserList />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});
MSW intercepts at the network level, so your component code (fetch, axios, React Query) runs unchanged. This gives higher confidence than mocking fetch directly.E2E testing with Playwright:
test('user can log in and see dashboard', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[name=email]', 'user@test.com');
  await page.fill('[name=password]', 'password');
  await page.click('button[type=submit]');
  await expect(page.locator('h1')).toContainText('Dashboard');
});
What interviewers are really testing: Whether you write tests that give real confidence vs tests that just increase coverage numbers.Red flag answer: Testing implementation details (expect(setState).toHaveBeenCalledWith(...)) or relying heavily on snapshot tests without understanding what they actually verify.Follow-up:
  • Why does React Testing Library discourage testing implementation details?
  • How does MSW differ from mocking fetch or axios directly?
  • What is your strategy for deciding what to unit test vs integration test vs E2E test?
Snapshot testing captures a component’s rendered output as a serialized string and compares it against a stored reference snapshot on subsequent test runs.How it works:
test('renders correctly', () => {
  const { container } = render(<Button variant="primary">Click me</Button>);
  expect(container).toMatchSnapshot();
});
  • First run: creates a .snap file with the serialized DOM output
  • Subsequent runs: compares current output against the stored snapshot
  • If they differ: test fails. You review the diff and either update the snapshot (intentional change) or fix the bug (unintentional change).
The problems with snapshot testing (why many teams move away from it):1. Brittle: Any change to the component (even unrelated to the test’s purpose) breaks the snapshot. Adding a CSS class, changing a wrapper div, updating a library — all trigger failures that developers rubber-stamp with --updateSnapshot without reviewing.2. Large, unreadable diffs: A snapshot of a complex component can be hundreds of lines. When it fails, the diff is too large to review meaningfully, defeating the purpose.3. False confidence: Passing snapshot tests do not prove the component WORKS — they only prove it renders the SAME thing. A bug in the initial render gets immortalized in the snapshot.4. No intent communication: A snapshot does not tell you WHAT about the rendering matters. A targeted assertion like expect(screen.getByRole('alert')).toHaveTextContent('Error!') communicates intent clearly.When snapshots ARE useful:
  • Inline snapshots (toMatchInlineSnapshot) for small, focused outputs — the snapshot lives in the test file, easier to review
  • API response shapes — verify the structure of serialized data
  • Configuration objects — catch unintended changes to complex configs
  • As a change detection tool during refactoring — temporarily add snapshots, refactor, verify nothing changed, then remove them
Better alternatives:
  • Targeted assertions: expect(screen.getByRole('heading')).toHaveTextContent('Welcome')
  • Visual regression testing: Percy, Chromatic, or Playwright visual comparisons (pixel-level comparison of screenshots)
What interviewers are really testing: Whether you understand snapshot testing’s limitations and when it adds vs subtracts value.Red flag answer: “Snapshot tests are great because they test everything automatically” — this shows a lack of understanding of their limitations and maintenance cost.Follow-up:
  • What happens when a team routinely runs --updateSnapshot without reviewing changes?
  • When would you use visual regression testing instead of snapshot testing?
  • How do inline snapshots improve on file-based snapshots?
Forms are one of the most common accessibility failure points because they involve complex interactions: input, validation, error recovery, and submission.1. Label every input (non-negotiable):
<!-- Explicit association (preferred) -->
<label htmlFor="email">Email Address</label>
<input id="email" type="email" name="email" />

<!-- Implicit association (wrapping) -->
<label>
  Email Address
  <input type="email" name="email" />
</label>
  • Every <input>, <select>, and <textarea> MUST have an associated label
  • Placeholder text is NOT a substitute for labels — it disappears on input and has low contrast
  • aria-label for visually hidden labels (search inputs with icon-only UI)
2. Error handling that is accessible:
<label htmlFor="email">Email Address</label>
<input
  id="email"
  type="email"
  aria-invalid="true"
  aria-describedby="email-error"
/>
<span id="email-error" role="alert">Please enter a valid email address</span>
  • aria-invalid="true" signals to screen readers that the field has an error
  • aria-describedby links the error message to the input — screen readers announce it when the input is focused
  • role="alert" makes the error announced immediately when it appears (live region)
  • On submit failure: Move focus to the first invalid field so keyboard/screen reader users know where to fix
3. Keyboard navigation:
  • Tab through all fields in logical order (use tabindex only when necessary, and avoid tabindex values > 0)
  • Enter submits the form (native <button type="submit"> handles this)
  • Escape closes modals/dropdowns
  • Custom dropdowns must support arrow key navigation
  • Do not trap focus (user must be able to Tab out of the form) — except in modals, where focus trapping IS correct
4. Group related fields:
<fieldset>
  <legend>Shipping Address</legend>
  <label htmlFor="street">Street</label>
  <input id="street" />
  <!-- more fields -->
</fieldset>
  • <fieldset> + <legend> groups related inputs and provides context to screen readers
  • Critical for radio button groups
5. Auto-complete and input types:
  • Use correct type attributes: email, tel, url, number, date — these trigger appropriate mobile keyboards and enable browser autofill
  • Use autocomplete attributes: autocomplete="email", autocomplete="given-name" — reduces user effort and errors
6. Clear submission feedback:
  • Show a visible success/error message after submission
  • Use aria-live="polite" regions for dynamic status updates
  • Do not rely solely on color to indicate errors (colorblind users)
What interviewers are really testing: Whether you build accessible forms by default or retrofit accessibility as an afterthought.Red flag answer: “I add labels to inputs” without mentioning error handling, keyboard navigation, or ARIA attributes.Follow-up:
  • How would you make a custom select/dropdown component accessible?
  • What is the difference between aria-live="polite" and aria-live="assertive"?
  • How do you test form accessibility during development?
Accessibility testing should be integrated throughout development, not just run at the end. Here is a layered tooling approach:1. During development (instant feedback):
  • ESLint eslint-plugin-jsx-a11y — catches 30+ accessibility issues at code time (missing alt text, click handlers on non-interactive elements, missing labels). Runs in IDE as you type.
  • TypeScript — proper typing of ARIA attributes catches invalid attribute values at compile time
  • Browser DevTools Accessibility panel — inspect the accessibility tree, computed roles, labels, and name computation for any element
  • axe DevTools browser extension — one-click accessibility audit of the current page, highlights issues with WCAG rule references
2. During testing (automated checks):
  • jest-axe — integrate axe-core into your test suite:
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('form is accessible', async () => {
  const { container } = render(<LoginForm />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
  • Playwright axe integration — run accessibility checks in E2E tests
  • Storybook accessibility addon — shows a11y status for each component story
3. During CI/CD (gate merges on accessibility):
  • Lighthouse CI — set accessibility score thresholds. Fail the build if score drops below 90.
  • Pa11y CI — automated WCAG compliance checking in pipelines
  • axe-core/cli — command-line accessibility testing
4. Manual testing (irreplaceable):
  • Screen reader testing: VoiceOver (macOS), NVDA (Windows, free), JAWS (Windows, enterprise). Automated tools catch ~30% of accessibility issues. Screen reader testing catches interaction, flow, and comprehension issues that no automated tool can.
  • Keyboard-only navigation: Unplug your mouse and navigate your entire app with just Tab, Shift+Tab, Enter, Space, Escape, and arrow keys.
  • Zoom testing: Zoom to 200% and verify nothing breaks, overlaps, or becomes unusable.
  • Color contrast testing: Use tools like Colour Contrast Analyser or browser DevTools contrast checker.
5. Real-user testing:
  • Disability community testing services like Fable, accessiBe UserWay. Nothing replaces feedback from actual users with disabilities.
What interviewers are really testing: Whether you have a systematic approach to accessibility testing, not just “I run Lighthouse.”Red flag answer: Only mentioning Lighthouse without discussing automated testing integration, screen reader testing, or keyboard testing.Follow-up:
  • What percentage of accessibility issues can automated tools catch, and what requires manual testing?
  • How would you integrate accessibility testing into a CI/CD pipeline?
  • Have you done screen reader testing? What surprised you about the experience?

10. Deployment & Best Practices

Deployment strategy depends on your rendering mode, infrastructure requirements, and team capabilities.Next.js deployment options:1. Vercel (recommended for Next.js):
  • Zero-config deployment (built by the same team that builds Next.js)
  • Automatic preview deployments for every PR
  • Edge Functions, serverless functions, ISR support out of the box
  • CDN edge network for static assets
  • Analytics and Web Vitals monitoring built in
  • Trade-off: Vendor lock-in, can get expensive at scale ($20/seat/month pro, usage-based pricing)
2. Self-hosted / Docker:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
  • Full control over infrastructure
  • Required for compliance (data sovereignty, on-premise requirements)
  • Deploy to AWS ECS/EKS, Google Cloud Run, or any container platform
  • You manage scaling, CDN, SSL, health checks
3. Static export + CDN (for SSG-only):
  • next export (Pages Router) or output: 'export' in config (App Router)
  • Deploy to Cloudflare Pages, Netlify, AWS S3 + CloudFront, GitHub Pages
  • Cheapest option, fastest delivery, but no SSR/ISR/API routes
4. AWS Amplify / Google Cloud:
  • Managed serverless deployment with SSR support
  • More control than Vercel, less than self-hosted
React SPA (Create React App / Vite) deployment:
  • npm run build produces a dist or build folder of static files
  • Host anywhere that serves static files: Netlify, Cloudflare Pages, S3 + CloudFront, Vercel, GitHub Pages
  • Configure SPA routing: all paths should return index.html (common gotcha: 404 on refresh because the server does not know about client-side routes)
Production checklist:
  • Build optimization: NODE_ENV=production, minification, tree shaking
  • Error monitoring: Sentry, Bugsnag, or Datadog RUM
  • Performance monitoring: Vercel Analytics, SpeedCurve, or Web Vitals
  • Health checks and uptime monitoring
  • Rollback strategy (keep previous deployment available)
  • Environment variables configured in the deployment platform (not committed to git)
What interviewers are really testing: Whether you can make infrastructure decisions based on requirements and understand the trade-offs between managed and self-hosted.Red flag answer: “Just deploy to Vercel” without discussing alternatives, trade-offs, or production concerns.Follow-up:
  • When would you choose self-hosted Docker over Vercel for a Next.js app?
  • How do you handle the SPA routing problem when deploying a React app to a static host?
  • What is your rollback strategy when a deployment introduces a bug?
Frontend security is about protecting users from attacks and protecting your application from abuse. The frontend cannot be trusted, but it is the first line of defense for user experience.1. Cross-Site Scripting (XSS) — the #1 frontend vulnerability:
  • What: Attacker injects malicious JavaScript that executes in other users’ browsers
  • Prevention:
    • React auto-escapes JSX output (safe by default)
    • NEVER use dangerouslySetInnerHTML with user input without sanitizing with DOMPurify first
    • Set Content-Security-Policy headers to restrict script sources
    • Sanitize all user-generated HTML on the server side
    • Use HttpOnly cookies for auth tokens (JavaScript cannot read them, so XSS cannot steal them)
2. Cross-Site Request Forgery (CSRF):
  • What: Attacker tricks an authenticated user’s browser into making requests to your API
  • Prevention:
    • SameSite=Strict or SameSite=Lax cookie attribute (prevents cookies from being sent on cross-origin requests)
    • CSRF tokens for state-changing requests
    • Check Origin and Referer headers on the server
3. Authentication token storage:
  • Never store tokens in localStorage — XSS can steal them trivially
  • Preferred: HttpOnly + Secure + SameSite=Strict cookies. JavaScript cannot access these, and they are automatically sent with requests.
  • If you must use tokens in JavaScript (SPA calling separate API domain): store in memory (variable), not localStorage. Accept that refresh loses the token (redirect to login or use a refresh token in an HttpOnly cookie).
4. Content Security Policy (CSP):
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-abc123';
  style-src 'self' 'unsafe-inline';
  img-src 'self' https://cdn.example.com;
  connect-src 'self' https://api.example.com;
  • Prevents inline script injection, restricts resource origins
  • Nonce-based CSP allows specific inline scripts while blocking injected ones
  • Report-URI for monitoring CSP violations without blocking (deploy in report-only mode first)
5. Input validation and sanitization:
  • Validate on the client for UX, validate on the server for security
  • Never trust client-side validation alone — it can be bypassed entirely
  • Sanitize user-generated content before rendering (DOMPurify for HTML)
6. Dependency security:
  • npm audit — check for known vulnerabilities in dependencies
  • Dependabot / Renovate — automated dependency update PRs
  • Lock files (package-lock.json) — prevent supply chain attacks from modified packages
  • Consider npm ci in CI (uses lockfile exactly, no modifications)
7. HTTPS everywhere:
  • Force HTTPS via Strict-Transport-Security header (HSTS)
  • All API calls over HTTPS
  • Mixed content (HTTP resources on HTTPS page) is blocked by modern browsers
What interviewers are really testing: Whether you understand the threat model and can implement defense-in-depth, not just list buzzwords.Red flag answer: “Use HTTPS and sanitize inputs” without discussing XSS specifics, CSP, or token storage security.Follow-up:
  • Why is localStorage unsafe for auth tokens, and what is the recommended alternative?
  • How would you implement Content Security Policy for a Next.js app?
  • A pentester found an XSS vulnerability in your app. Walk me through your response.
CI/CD (Continuous Integration / Continuous Deployment) automates the path from code commit to production, catching bugs early and enabling rapid, reliable releases.Continuous Integration (CI) — what runs on every PR:
  1. Lint: ESLint + Prettier check code quality and formatting
  2. Type check: tsc --noEmit catches TypeScript errors
  3. Unit + integration tests: Jest/Vitest + React Testing Library
  4. Build: npm run build — catches import errors, missing env vars, build-time failures
  5. Bundle size check: Fail if bundle exceeds size budget (tools: bundlesize, size-limit)
  6. Accessibility audit: axe-core or Lighthouse CI
  7. Visual regression: Chromatic or Percy for component screenshot comparison
  8. E2E tests: Playwright against a preview deployment
Continuous Deployment (CD) — what happens after merge:
  • Preview deployments: Every PR gets a unique URL (Vercel, Netlify do this automatically). QA, designers, and PMs review before merge.
  • Staging environment: Merged code deploys to staging automatically for final validation.
  • Production deployment: After staging validation (automated or manual gate), deploy to production.
  • Canary/gradual rollout: Route 5% of traffic to new version, monitor error rates, then roll out to 100%.
GitHub Actions example:
name: CI
on: [push, pull_request]
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run test -- --coverage
      - run: npm run build
Production monitoring (post-deploy):
  • Error tracking: Sentry with source maps for readable stack traces
  • Performance: Web Vitals monitoring (real user data)
  • Rollback trigger: automated rollback if error rate spikes above threshold
  • Feature flags: LaunchDarkly, Statsig, or custom — deploy code without exposing features, enable for specific users/percentages
What interviewers are really testing: Whether you think about the full pipeline from commit to production monitoring, not just “we use GitHub Actions.”Red flag answer: Describing CI/CD as just “running tests before deployment” without mentioning preview deployments, monitoring, rollback strategies, or progressive rollout.Follow-up:
  • How would you set up bundle size budgets in your CI pipeline?
  • What is a canary deployment and when would you use it for a frontend app?
  • How do you handle database migrations or API changes that need to deploy in sync with frontend changes?
Environment variable security is about ensuring secrets never leak to unauthorized parties — not in source code, not in client bundles, not in logs.The fundamental rules:1. Never commit secrets to git:
  • .env.local, .env.*.local in .gitignore
  • .env can be committed with non-secret defaults (useful for documentation)
  • Use git-secrets or truffleHog in CI to scan for accidentally committed secrets
  • If a secret is ever committed: Rotate it immediately. Git history preserves it even after deletion.
2. Separate client vs server secrets:
  • Next.js: NEXT_PUBLIC_ prefix = exposed in client bundle. Everything else is server-only.
  • Vite: VITE_ prefix = exposed in client bundle
  • Mental model: If it starts with the public prefix, assume the entire world can read it
3. Use deployment platform’s secret management:
  • Vercel: Environment Variables UI, supports per-environment (production, preview, development)
  • AWS: Systems Manager Parameter Store or Secrets Manager
  • GitHub Actions: Repository secrets and environment secrets
  • Doppler / Vault: Centralized secrets management for complex setups with rotation, access control, and audit logs
4. Validate environment variables at startup:
// Using t3-env (Zod-based validation)
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
});
  • Fail fast at build/startup if a required variable is missing
  • Type-safe access throughout the codebase
  • Documents which variables exist and their expected format
5. Secret rotation and access control:
  • Rotate secrets regularly (API keys, database credentials)
  • Least-privilege: each service gets only the secrets it needs
  • Audit trail: who accessed which secrets and when
Common mistakes:
  • Logging process.env in error handlers (secrets appear in log aggregators)
  • Hardcoding API keys in frontend code as “constants”
  • Sharing .env files via Slack/email (use a secrets manager)
  • Using the same API key for development and production
What interviewers are really testing: Whether you treat secrets management as a first-class concern with proper tooling, not just “keep .env out of git.”Red flag answer: “Just add .env to .gitignore” without discussing validation, rotation, or the client/server boundary.Follow-up:
  • A secret was accidentally committed to git. What steps do you take?
  • How do you share environment variables with a new team member onboarding?
  • What is the difference between Vercel environment variables and a dedicated secrets manager like HashiCorp Vault?
Maintainability is about making code easy to understand, change, and extend — by you in 6 months, or by a new hire who has never seen the codebase. It is the highest-leverage investment a team can make.1. Project structure:
  • Feature-based organization over type-based. Instead of /components, /hooks, /utils with 200 files each, organize by feature: /features/auth/, /features/dashboard/, /features/checkout/. Each feature folder contains its components, hooks, types, tests, and utilities.
  • Colocation principle: Keep related files together. A component’s test, styles, types, and stories should be in the same directory, not scattered across parallel folder trees.
  • Barrel exports (index.ts) for clean public APIs per feature. Internal implementation stays private.
2. Component design:
  • Single Responsibility: Each component does one thing well. If a component file exceeds 200-300 lines, consider splitting.
  • Composition over configuration: Prefer composable primitives (<Card>, <Card.Header>, <Card.Body>) over heavily configured components (<Card headerTitle="..." bodyContent="..." footerButtons={[...]}>).
  • Custom hooks for logic: Extract business logic into custom hooks. Components should primarily be about rendering — the “view” layer.
  • Consistent patterns: If the team uses controlled forms, use controlled forms everywhere. If custom hooks are named useXxx, always name them that way.
3. Code quality automation:
  • ESLint with team-agreed rules (Airbnb or Next.js config as a base)
  • Prettier for formatting (end all formatting debates)
  • Husky + lint-staged for pre-commit hooks (catch issues before they enter git)
  • TypeScript strict mode — enables all strict checks. Non-negotiable for new projects.
4. Naming conventions:
  • Components: PascalCase (UserProfile.tsx)
  • Hooks: camelCase with use prefix (useAuth.ts)
  • Constants: UPPER_SNAKE_CASE (MAX_RETRY_COUNT)
  • Types/Interfaces: PascalCase (UserProfile, ApiResponse)
  • Boolean variables: prefix with is, has, should (isLoading, hasError)
  • Event handlers: prefix with handle in component, on in props (handleClick, onClick)
5. Documentation as code:
  • TypeScript types ARE documentation (self-documenting interfaces)
  • JSDoc for complex functions: why, not what
  • Storybook for component documentation and visual testing
  • ADR (Architecture Decision Records) for significant technical decisions
6. Testing strategy:
  • Write tests for behavior, not implementation
  • Coverage target: 70-80% as a guideline, not a dogma
  • Prioritize testing critical paths: auth, checkout, data mutations
What interviewers are really testing: Whether you think about long-term maintainability and team productivity, not just “clean code.”Red flag answer: Listing generic advice like “write clean code” and “use comments” without specific, opinionated practices.Follow-up:
  • How do you decide between a feature-based vs type-based folder structure?
  • What is your approach to managing technical debt without stopping feature development?
  • How would you onboard a new developer to an existing codebase as quickly as possible?

Conclusion & Interview Tips

This guide covers key frontend interview topics — from HTML to React, Next.js, and TypeScript. Each answer is designed to reflect the depth and nuance that interviewers at top-tier companies expect.

Preparation Tips

  • Master fundamentals (HTML, CSS, JS) before frameworks — interviewers can tell when you learned React before understanding the DOM
  • Build real projects (e.g., dashboards, e-commerce apps, real-time chat) and be ready to discuss the technical decisions you made
  • Focus on trade-offs — every answer should include “it depends” with specific conditions
  • Practice explaining concepts out loud — the interview is a conversation, not a written exam
  • Read source code of libraries you use (React Query, Zustand, React Hook Form) — this gives you depth that impresses interviewers

During the Interview

  • Structure your answers: Start with a one-liner summary, then go deeper. “The way I think about X is…” followed by 2-3 structured points.
  • Think out loud: Interviewers want to see your thought process, not just the final answer. “I am considering A and B. A is better because…” shows judgment.
  • Acknowledge trade-offs: Never say “always use X.” Say “I would use X in this context because Y, but if Z changes, I would reconsider.”
  • Be honest about gaps: “I have not worked with that in production, but my understanding is…” is far better than bluffing.
  • Ask clarifying questions: “What scale are we talking about?” or “Is SEO a priority?” shows you think about context.

Common Mistakes to Avoid

  • Buzzword dumping: Listing technologies without explaining why or when to use them
  • Not going deep enough: Surface-level answers signal surface-level understanding
  • Ignoring the “why”: Every technical choice has a reason. If you cannot explain why, you do not truly understand it.
  • Skipping edge cases: Production systems encounter edge cases. Mention them before the interviewer asks.
Frontend interviews test not only your technical skills but also your design thinking, accessibility awareness, and judgment about trade-offs. The best candidates do not just know WHAT to build — they know WHY they would build it that way and WHAT could go wrong.
Good luck with your Frontend Developer interviews!