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.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.
1. HTML Fundamentals
What is HTML and why is it important?
What is HTML and why is it important?
- 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.
- 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?
Explain semantic HTML
Explain semantic HTML
<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">)
- 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.
<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.
What are meta tags?
What are meta tags?
Difference between block and inline elements?
Difference between block and inline 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, andpaddingon all sides - Stack vertically
<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
marginandpaddingdo 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)
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
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-flexand when would you use it? - If I set
margin-top: 20pxon a<span>, what happens and why?
Explain the role of the DOCTYPE declaration
Explain the role of the DOCTYPE declaration
<!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-boxbut 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.
<!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?
What is the difference between localStorage, sessionStorage, and cookies?
What is the difference between localStorage, sessionStorage, and cookies?
2. CSS & Styling
What is the difference between relative, absolute, fixed, and sticky positioning?
What is the difference between relative, absolute, fixed, and sticky positioning?
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,leftoffset 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
positionother thanstatic). If none exists, it is relative to the initial containing block (effectively the viewport) - Common gotcha: forgetting to set
position: relativeon 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, orwill-changeset, it breaksfixedpositioning (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
relativeuntil the element reaches a scroll threshold, then acts likefixed - Requires a
top,bottom,left, orrightvalue 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: hiddenoroverflow: autoon ANY ancestor breaks sticky positioning silently - Use case: table headers that stick while scrolling, section headers in long lists
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: fixednot 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?
Explain Flexbox
Explain Flexbox
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 flexboxflex-direction: row | column | row-reverse | column-reverse— sets the main axisjustify-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)
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: 1meansflex-grow: 1; flex-shrink: 1; flex-basis: 0%— “take equal space” align-self— override the container’salign-itemsfor this specific child
- 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: 1on main content - Navigation bar:
justify-content: space-betweenwith 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)
flex-basisvswidth: flex-basis wins over width when both are set (in the flex-direction axis). Useflex-basisfor flex-direction sizing.min-width: autois the default for flex items, meaning a flex item will NOT shrink below its content size. This causes overflow bugs. Fix:min-width: 0on 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.
flex: 1 shorthand behavior.Follow-up:- What does
flex: 1actually expand to, and how does it differ fromflex: 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?
Explain CSS Grid
Explain CSS Grid
display: gridactivates it on the containergrid-template-columns/grid-template-rows— define the track sizes. Example:grid-template-columns: 1fr 2fr 1frcreates three columns where the middle is twice as wide.frunit — a fraction of available space (like flex-grow but for grid tracks)gap(orrow-gap/column-gap) — spacing between tracksgrid-column/grid-row— explicitly place items spanning tracks. Example:grid-column: 1 / 3spans columns 1 and 2.grid-template-areas— name areas for readable layout definitions
repeat()andminmax():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-fillvsauto-fit:auto-fillcreates empty tracks if there is extra space;auto-fitcollapses empty tracks so items stretch. The difference is subtle but matters for responsive behavior.
- 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
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-fillandauto-fitinrepeat()? - How does Grid handle implicit vs explicit tracks, and when does that matter?
What is specificity in CSS?
What is specificity in CSS?
!important— overrides everything (specificity: infinite, effectively). Use sparingly — it is a maintenance nightmare.- Inline styles (
style="...") — specificity:(1,0,0,0) - ID selectors (
#header) — specificity:(0,1,0,0) - Class selectors, attribute selectors, pseudo-classes (
.btn,[type="text"],:hover) — specificity:(0,0,1,0) - Element selectors, pseudo-elements (
div,p,::before) — specificity:(0,0,0,1) - Universal selector (
*) — specificity:(0,0,0,0)(adds nothing)
#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).
!importantcreates a parallel specificity universe. Among!importantrules, normal specificity comparison applies. This is why!importantwars happen in legacy codebases.
- 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
!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?
How do you optimize CSS performance?
How do you optimize CSS performance?
- 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
coveragetab 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
- 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. Usemediaattributes to make non-critical stylesheets non-blocking:<link rel="stylesheet" href="print.css" media="print">
- Browsers match selectors right to left.
.container .item spanfirst 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)
- Avoid triggering layout thrashing. Properties like
width,height,top,left,margintrigger expensive layout recalculations. Prefertransform: translate()for animations. - Use
will-changejudiciously. 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
transformandopacityfor animations — these properties can be animated on the compositor thread without triggering layout or paint
- 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
containin CSS and when would you use it?
Explain the CSS Box Model
Explain the CSS Box Model
box-sizing distinction:box-sizing: content-box(default):widthandheightapply to the content only. Padding and border are ADDED on top. Sowidth: 200px; padding: 20px; border: 1px solid= actual rendered width of 242px. This is the source of countless layout bugs.box-sizing: border-box:widthandheightinclude padding and border.width: 200pxmeans 200px total, padding and border included. This is what developers expect.
- Vertical margins between adjacent block elements collapse — the larger margin wins, they do not add up. Two elements with
margin-bottom: 20pxandmargin-top: 30pxhave 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-topcan “escape” and become the parent’s margin if the parent has no padding, border, or overflow set. Fix: addpadding-top: 1pxoroverflow: hiddento the parent.
box-sizing or margin collapse.Follow-up:- Why do most CSS resets set
box-sizing: border-boxglobally, 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
Explain hoisting in JavaScript
Explain hoisting in JavaScript
- 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.
vardeclarations are hoisted but initialized toundefined. The assignment happens at runtime when that line executes. This is whyconsole.log(x); var x = 5;logsundefined, not an error.letandconstdeclarations 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 aReferenceError.- Function expressions and arrow functions assigned to
varare treated asvar— hoisted asundefined. Assigned tolet/const— in the TDZ.
- The TDZ is why
let/constare safer — they fail loudly instead of silently givingundefined - 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
varin loops
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
varinside aforloop cause unexpected closure behavior, and how dolet/constfix it?
Difference between var, let, and const
Difference between var, let, and const
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function-scoped | Block-scoped | Block-scoped |
| Hoisting | Hoisted, initialized to undefined | Hoisted, TDZ (uninitialized) | Hoisted, TDZ (uninitialized) |
| Re-declaration | Allowed (silently overwrites) | Not allowed in same scope | Not allowed in same scope |
| Re-assignment | Allowed | Allowed | Not allowed |
let replaced var.2. const does not mean immutable: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
constdoes not make objects immutable, how do you achieve immutability in JavaScript? - What happens if you declare
var xtwice in the same scope vslet xtwice? - Why does
varin a loop withsetTimeoutproduce unexpected results, and how doesletfix it?
What is event delegation?
What is event delegation?
- User clicks a specific child element (the event target)
- The event bubbles up through the DOM tree: child -> parent -> grandparent -> … -> document
- The listener on the parent catches the event
- You use
event.targetto determine which specific child was clicked - You apply logic based on the target
- 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
removeEventListenercalls.
- Capture phase — event travels DOWN from
windowto the target (rarely used, but available viaaddEventListener('click', handler, true)) - Target phase — event reaches the actual clicked element
- 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
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.targetandevent.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?
Explain closures
Explain closures
[[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:- 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)— the5is closed over. - React hooks internally:
useState,useEffect, and every hook relies on closures to associate state with the specific component instance.
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: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?
Difference between synchronous and asynchronous code
Difference between synchronous and asynchronous code
- Synchronous code runs on the call stack
- Async operations (fetch, setTimeout, DOM events) are handled by Web APIs (outside the JS engine)
- When an async operation completes, its callback goes to the task queue (macrotask) or microtask queue
- The event loop checks: “Is the call stack empty? If yes, move the next task from the queue to the stack.”
- Microtasks (Promises,
queueMicrotask,MutationObserver) have priority over macrotasks (setTimeout,setInterval, I/O)
- 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.
- Unhandled promise rejections used to silently fail. Modern Node.js (v15+) crashes on unhandled rejections. Always use
.catch()ortry/catchwithawait. Promise.allfails fast — one rejection rejects all.Promise.allSettledwaits for all to complete regardless of rejection. Choose based on your use case.
- 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.allvsPromise.allSettledvsPromise.race?
What is the difference between == and ===?
What is the difference between == and ===?
== 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:=== is preferred.When == is actually acceptable:value == null— this checks for bothnullANDundefinedin one expression. Some style guides (including some internal ones at large companies) allow this as the one exception.- ESLint’s
eqeqeqrule with"allow-null"option exists specifically for this pattern.
Object.is() alternative:- Behaves like
===but handles two edge cases differently:Object.is(NaN, NaN)returnstrue, andObject.is(+0, -0)returnsfalse. React usesObject.isinternally for state comparison in hooks.
== is problematic shows lack of depth.Follow-up:- Can you explain the coercion steps that make
[] == falseevaluate totrue? - When is
==actually appropriate to use? - What is
Object.is()and how does it differ from===?
4. React Fundamentals
What is React?
What is React?
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-routerfor routing, Redux/Zustand/Jotai for state management- Next.js, Remix, Gatsby as full-stack frameworks built on React
- 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?
Explain virtual DOM
Explain virtual DOM
- 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) keyprops tell React which children in a list are the same across renders, enabling efficient reordering instead of re-creating
- Direct DOM manipulation is expensive — the DOM is a complex C++ object model. Reading
element.offsetHeightcan 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.
- “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.
- 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
- 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
keyprops affect reconciliation performance, and what happens when you use array index as keys?
What are props and state?
What are props and state?
- 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 callingChild({ name: "Alice" }) - Can pass any JavaScript value: strings, numbers, objects, arrays, functions, even other components (render props pattern)
- Data managed within a component via
useState(function components) orthis.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 asynchronous —
setStatedoes not update immediately. React batches state updates for performance.
- 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 useuseMemo. - 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 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?
Difference between controlled and uncontrolled components
Difference between controlled and uncontrolled 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
- 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.
- The DOM itself is the source of truth. You access the value when you need it (typically on submit) via a
ref.
- 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.
defaultValue vs value distinction:valuemakes it controlled (React owns it)defaultValuemakes it uncontrolled (sets initial DOM value, React does not track changes)- Setting
valuewithout anonChangehandler creates a read-only input that React warns about
- 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>).
<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
valueon an input without anonChangehandler?
What is the purpose of keys in lists?
What is the purpose of keys in lists?
- 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.
- 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.
<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
- 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
- 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()orDate.now()as a key in render?
5. React Hooks & Lifecycle
Explain useEffect
Explain useEffect
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:- Before the effect re-runs (when dependencies change)
- When the component unmounts
Common use: clearing intervals, unsubscribing from WebSockets, canceling fetch requests via
AbortController.
exhaustive-deps rule catches this — do NOT ignore it without understanding why.2. Infinite loops: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?
What is useMemo and useCallback?
What is useMemo and useCallback?
useMemo — memoize a computed value:- 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:- Returns the function itself (not its return value)
- The function reference stays the same between renders unless dependencies change
useCallback(fn, deps)is equivalent touseMemo(() => fn, deps)
- Passing callbacks to child components wrapped in
React.memo— withoutuseCallback, the new function reference breaks memoization - Passing objects/arrays as props —
useMemoprevents 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)
- 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/useCallbackitself (comparing dependencies) can be MORE than just recomputing a simple value
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?
How to handle forms in React?
How to handle forms in React?
- 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.
- 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
- 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.
- Every input needs a
<label>with matchinghtmlFor/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
- 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)?
Explain Context API
Explain Context API
- 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
- Split contexts by update frequency (user data changes rarely, notifications change often)
- Memoize the context value:
const value = useMemo(() => ({ user, logout }), [user]) - Use
React.memoon intermediate components to prevent cascading re-renders
- 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
- 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?
How does React reconciliation work?
How does React reconciliation work?
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.- 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
What is Next.js?
What is Next.js?
- 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.
- 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.tsxconventions, streaming, server actions. Fundamentally different mental model.
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 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?
Explain SSR, SSG, ISR, and CSR
Explain SSR, SSG, ISR, and CSR
- 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
- 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
- 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) orgetStaticPaths+getStaticProps(Pages Router)
- 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: 60in fetch options or page config
- 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?
What is dynamic routing in Next.js?
What is dynamic routing in Next.js?
pages/users/[id].jsmatches/users/1,/users/abc, etc.- Access the parameter:
const { id } = useRouter().query(client) or fromgetServerSideProps({ params }) - Catch-all routes:
pages/docs/[...slug].jsmatches/docs/a,/docs/a/b/c(any depth) - Optional catch-all:
pages/docs/[[...slug]].jsalso matches/docs(the base path)
app/users/[id]/page.tsxmatches/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)
fallback option (Pages Router) matters:false: Only pre-generated paths work, 404 for everything elsetrue: 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 is the difference between
fallback: trueandfallback: '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]?
How to optimize performance in Next.js?
How to optimize performance in Next.js?
- Use
next/imageexclusively — it handles WebP/AVIF conversion, responsivesrcset, lazy loading, and blur placeholder - Set explicit
widthandheight(or usefill) to prevent Cumulative Layout Shift (CLS) - Configure
remotePatternsinnext.config.jsfor external image domains - Use
priorityprop on above-the-fold images (hero images) to disable lazy loading
- 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.tsxfor route-level loading states
- Next.js automatically code-splits by route
- Use
dynamic()(equivalent toReact.lazy) for heavy components:const Chart = dynamic(() => import('./Chart'), { ssr: false }) ssr: falseis 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 oflodashinstead oflodash/debounce
- 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
fetchwithnext: { revalidate: N }for ISR caching - Implement
next: { tags: ['products'] }+revalidateTag('products')for on-demand cache invalidation
- Use
next/fontto self-host Google Fonts (eliminates external network request) display: 'swap'prevents invisible text during font loading- Reduces CLS from font loading
- Use
next/scriptwithstrategy="lazyOnload"for analytics, chat widgets, etc. strategy="afterInteractive"for scripts needed soon but not blocking render- Avoid loading heavy third-party scripts synchronously
- Largest Contentful Paint (LCP) < 2.5s
- First Input Delay (FID) < 100ms
- Cumulative Layout Shift (CLS) < 0.1
- Time to First Byte (TTFB) < 800ms
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()withssr: falseand a regularReact.lazy? - How does React Server Components change the performance optimization landscape compared to client-side React?
How to handle environment variables in Next.js?
How to handle environment variables in Next.js?
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.
.env— default for all environments.env.local— local overrides (gitignored by default).env.development/.env.production/.env.test— environment-specific.env.development.local/.env.production.local— environment-specific local overrides
- Priority:
.env.*.local>.env.local>.env.*>.env process.env.NODE_ENVis automatically set (development,production,test)
- Putting
DATABASE_URLasNEXT_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_VARin client components withoutNEXT_PUBLIC_— it will beundefinedon the client (no error, just silently missing)
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
- Store secrets in your deployment platform (Vercel Environment Variables, AWS Secrets Manager, Doppler)
- Use
.env.localfor local development only - Validate env vars at startup using a schema (Zod +
t3-envlibrary is the gold standard) - Never commit
.env.localto git (it should be in.gitignore)
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
What is TypeScript and why use it?
What is TypeScript and why use it?
- JavaScript:
user.naemsilently returnsundefined. You discover this from a bug report in production. - TypeScript:
user.naemshows a red squiggly immediately. The typo never reaches git. - Studies (Airbnb, Bloomberg) show TypeScript prevents 15-20% of bugs that would otherwise reach production.
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
.tsfiles 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
- Does TypeScript have any runtime overhead? Why or why not?
- What is the
strictmode in tsconfig and what does it enable? - When might TypeScript not be the right choice for a project?
Explain interfaces vs types
Explain interfaces vs types
interface and type define the shape of data in TypeScript, but they have different capabilities and use cases.Interface:| Feature | interface | type |
|---|---|---|
| Object shapes | Yes | Yes |
| Declaration merging | Yes | No (duplicate identifier error) |
extends keyword | Yes | No (use & intersection) |
| Union types | No | Yes |
| Primitive aliases | No | Yes (type ID = string) |
| Mapped types | No | Yes |
| Conditional types | No | Yes |
| Tuple types | No | Yes (type Pair = [string, number]) |
interfacefor 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.typefor 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
typefor props; library authors tend towardinterfacefor extensibility.
- 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
typeoverinterfacefor React component props?
What are generics?
What are generics?
extends):keyof with generics (type-safe property access):- 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
identity<T> example without demonstrating constraints, keyof, or real-world React usage.Follow-up:- How do generic constraints work, and when would you use
extendsin 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?
What are utility types?
What are utility types?
Partial<T> — Makes all properties optionalRequired<T> — Makes all properties required (opposite of Partial)Pick<T, K> — Select specific propertiesOmit<T, K> — Remove specific propertiesReadonly<T> — Makes all properties readonlyRecord<K, V> — Creates an object type with keys K and values VExclude<T, U> / Extract<T, U> — Filter union typesReturnType<T> — Extract the return type of a functionNonNullable<T> — Removes null and undefined from a typeComposing utility types (where it gets powerful):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
idandemail? - What is the difference between
OmitandExclude? - How does
ReturnTypework under the hood (hint: conditional types andinfer)?
How does TypeScript improve React development?
How does TypeScript improve React development?
React.FCvs function declaration —React.FCimplicitly includeschildren(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 propsas constfor literal types —const sizes = ['sm', 'md', 'lg'] as constcreates a readonly tuple type
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
How to improve frontend performance?
How to improve frontend performance?
- Bundle analysis: Use
webpack-bundle-analyzeror@next/bundle-analyzerto visualize your bundle. Common finds: shipping all ofmoment.js(330KB) when you needdate-fns(tree-shakeable), or importing entirelodash(70KB) instead oflodash/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.
- Minimize re-renders:
React.memofor pure components,useMemo/useCallbackwhere 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.offsetHeightreads with style changes. - Use CSS containment:
contain: layout painton independent sections.
- Images: WebP/AVIF format, responsive
srcset, lazy loading, proper sizing (do not serve a 4000px image in a 400px container).next/imagehandles this automatically. - Fonts: Self-host (
next/font), usefont-display: swap, subset to only needed characters. - Videos: Lazy load, use poster images, prefer streaming formats.
- Caching: Set proper
Cache-Controlheaders. Immutable assets (hashed filenames) getmax-age=31536000. HTML getsno-cacheor 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.
- 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
- 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?
Explain lazy loading in React
Explain lazy loading in React
React.lazy() for component-level code splitting:import()creates a separate webpack chunk that loads on demandSuspenseprovides a fallback UI while the chunk loads- The component’s JavaScript only downloads when
<HeavyChart>first renders
dynamic() — enhanced lazy loading:ssr: falseprevents the component from rendering on the server (avoidswindow is not definederrors)- Built-in loading component option
- 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
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/imagedoes this by default (except whenpriorityis set)
- 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.
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.lazyand Next.jsdynamic?
What is debouncing and throttling?
What is debouncing and throttling?
- Debounce groups multiple calls into ONE call at the END of the burst
- Throttle allows ONE call per time interval DURING the burst
- 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
requestAnimationFramethrottling and when is it better than time-based throttling?
What is bundle splitting?
What is bundle splitting?
/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.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 bundlesource-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
- 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?
Explain image optimization techniques
Explain image optimization techniques
- 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:
- SVG for icons, logos, illustrations — infinitely scalable, tiny file size for simple graphics
srcset and sizes):- 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.jspriorityprop)
- ALWAYS set
widthandheightattributes (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
Image component:- 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)
- 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 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
What is web accessibility (a11y)?
What is web accessibility (a11y)?
- 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.
- 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.
- 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)
- Semantic HTML (
<nav>,<main>,<button>not<div onClick>) - All images have descriptive
alttext (oralt=""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: nonewithout 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)
- Walk me through how a screen reader user would navigate a page you have built.
- What is the difference between
aria-label,aria-labelledby, andaria-describedby? - How do you test for accessibility during development, not just at the end?
How do you test React components?
How do you test React components?
“The more your tests resemble the way your software is used, the more confidence they can give you.” — Kent C. DoddsThe 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
getByRole— queries by ARIA role (most accessible, most robust)getByLabelText— for form inputsgetByPlaceholderText— fallback for inputsgetByText— for non-interactive contentgetByTestId— last resort (test-specific, not user-facing)
- 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
fetch directly.E2E testing with Playwright: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
fetchoraxiosdirectly? - What is your strategy for deciding what to unit test vs integration test vs E2E test?
Explain snapshot testing
Explain snapshot testing
- First run: creates a
.snapfile 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).
--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
- Targeted assertions:
expect(screen.getByRole('heading')).toHaveTextContent('Welcome') - Visual regression testing: Percy, Chromatic, or Playwright visual comparisons (pixel-level comparison of screenshots)
- What happens when a team routinely runs
--updateSnapshotwithout reviewing changes? - When would you use visual regression testing instead of snapshot testing?
- How do inline snapshots improve on file-based snapshots?
How to ensure accessibility in forms?
How to ensure accessibility in forms?
- 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-labelfor visually hidden labels (search inputs with icon-only UI)
aria-invalid="true"signals to screen readers that the field has an erroraria-describedbylinks the error message to the input — screen readers announce it when the input is focusedrole="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
- Tab through all fields in logical order (use
tabindexonly when necessary, and avoidtabindexvalues > 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
<fieldset>+<legend>groups related inputs and provides context to screen readers- Critical for radio button groups
- Use correct
typeattributes:email,tel,url,number,date— these trigger appropriate mobile keyboards and enable browser autofill - Use
autocompleteattributes:autocomplete="email",autocomplete="given-name"— reduces user effort and errors
- 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)
- How would you make a custom select/dropdown component accessible?
- What is the difference between
aria-live="polite"andaria-live="assertive"? - How do you test form accessibility during development?
What tools can you use for accessibility testing?
What tools can you use for accessibility testing?
- 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
- jest-axe — integrate axe-core into your test suite:
- Playwright axe integration — run accessibility checks in E2E tests
- Storybook accessibility addon — shows a11y status for each component story
- 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
- 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.
- Disability community testing services like Fable, accessiBe UserWay. Nothing replaces feedback from actual users with disabilities.
- 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
How to deploy React or Next.js apps?
How to deploy React or Next.js apps?
- 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)
- 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
next export(Pages Router) oroutput: '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
- Managed serverless deployment with SSR support
- More control than Vercel, less than self-hosted
npm run buildproduces adistorbuildfolder 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)
- 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)
- 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?
How to secure frontend apps?
How to secure frontend apps?
- What: Attacker injects malicious JavaScript that executes in other users’ browsers
- Prevention:
- React auto-escapes JSX output (safe by default)
- NEVER use
dangerouslySetInnerHTMLwith user input without sanitizing with DOMPurify first - Set
Content-Security-Policyheaders to restrict script sources - Sanitize all user-generated HTML on the server side
- Use
HttpOnlycookies for auth tokens (JavaScript cannot read them, so XSS cannot steal them)
- What: Attacker tricks an authenticated user’s browser into making requests to your API
- Prevention:
SameSite=StrictorSameSite=Laxcookie attribute (prevents cookies from being sent on cross-origin requests)- CSRF tokens for state-changing requests
- Check
OriginandRefererheaders on the server
- Never store tokens in
localStorage— XSS can steal them trivially - Preferred:
HttpOnly+Secure+SameSite=Strictcookies. 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).
- 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)
- 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)
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 ciin CI (uses lockfile exactly, no modifications)
- Force HTTPS via
Strict-Transport-Securityheader (HSTS) - All API calls over HTTPS
- Mixed content (HTTP resources on HTTPS page) is blocked by modern browsers
- 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.
Explain CI/CD for frontend apps
Explain CI/CD for frontend apps
- Lint: ESLint + Prettier check code quality and formatting
- Type check:
tsc --noEmitcatches TypeScript errors - Unit + integration tests: Jest/Vitest + React Testing Library
- Build:
npm run build— catches import errors, missing env vars, build-time failures - Bundle size check: Fail if bundle exceeds size budget (tools:
bundlesize,size-limit) - Accessibility audit: axe-core or Lighthouse CI
- Visual regression: Chromatic or Percy for component screenshot comparison
- E2E tests: Playwright against a preview deployment
- 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%.
- 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
- 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?
How to manage environment variables securely?
How to manage environment variables securely?
.env.local,.env.*.localin.gitignore.envcan be committed with non-secret defaults (useful for documentation)- Use
git-secretsortruffleHogin CI to scan for accidentally committed secrets - If a secret is ever committed: Rotate it immediately. Git history preserves it even after deletion.
- 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
- 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
- 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
- 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
- Logging
process.envin error handlers (secrets appear in log aggregators) - Hardcoding API keys in frontend code as “constants”
- Sharing
.envfiles via Slack/email (use a secrets manager) - Using the same API key for development and production
- 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?
Best practices for maintainable frontend code?
Best practices for maintainable frontend code?
- Feature-based organization over type-based. Instead of
/components,/hooks,/utilswith 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.
- 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.
- 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.
- Components: PascalCase (
UserProfile.tsx) - Hooks: camelCase with
useprefix (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
handlein component,onin props (handleClick,onClick)
- 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
- Write tests for behavior, not implementation
- Coverage target: 70-80% as a guideline, not a dogma
- Prioritize testing critical paths: auth, checkout, data mutations
- 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.