Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up

Front-end interviews cover far more than framework syntax. Expect questions on the box model and specificity, when to use Flexbox versus Grid, positioning and container queries, the critical rendering path and Core Web Vitals, and the difference between server-side rendering, static generation and client-side rendering. Accessibility and CORS come up regularly, and strong candidates are expected to reason about performance with measurements rather than assumptions. The questions below cover the whole stack a front-end engineer owns.

Behavioural Questions

1. Tell me about a front-end project you are proud of. What made it challenging?

Note: Choose the project with the most interesting constraint, not the one with the most features. Constraints are what produce good interview stories.

Give it in four parts:

  • The problem and who it was for. Keep it to one or two sentences and include scale — users, page views, or how much of the business ran through it.
  • The constraint that made it hard. Strong options: a strict performance budget on slow mobile networks, an accessibility requirement, supporting an old browser the customer base still used, or a design system you had to build alongside the feature.
  • A decision you made and the alternative you rejected. This is where interviewers judge seniority — say what you traded away.
  • The measured outcome. Load time, conversion, bounce rate, support tickets, or Lighthouse score before and after.

Be honest about what you would change. Naming a weakness in your own work reads as confidence, not doubt.

2. How do you work with designers, especially when a design is hard or expensive to build?

The answer interviewers want is that you push back with information rather than either refusing or silently absorbing the cost.

  • Get involved before the handoff. Reviewing designs while they are still cheap to change prevents most conflicts. Ask about empty states, long text, error states, and loading behaviour — the things designs usually omit and developers end up inventing.
  • Quantify the cost. "This animation adds 40KB and drops us below our performance budget on 3G" is a conversation. "That's not possible" is a fight.
  • Offer an alternative that keeps the intent. Designers care about the effect, not the implementation. Usually there is a cheaper way to get the same feeling.
  • Use a shared vocabulary. A design system with agreed tokens for spacing, colour, and type removes most of the pixel-level negotiation permanently.

Note: Mentioning that you raise accessibility issues at design time — contrast ratios, focus order, touch target size — is a strong differentiator, because it is much cheaper to fix there than in code.

3. Describe a time you had to improve the performance of a slow page. What did you do?

Tell it as measurement first, guessing never.

  • What the complaint was, and what the data said. These often differ. Real user monitoring beats a lab test on your own laptop, because your laptop is not the user's phone.
  • Which metric was actually bad. Naming the specific Core Web Vital shows you know they have different causes: LCP is usually images or a slow server response, CLS is missing dimensions or late-injected content, INP is long JavaScript tasks blocking the main thread.
  • How you profiled. The Performance panel for main-thread work, the Network waterfall for request chains, the Coverage tab for unused CSS and JavaScript.
  • What you changed. Typical high-value fixes: code splitting a route, lazy-loading below-the-fold images, adding width and height attributes, replacing a heavy dependency, preloading a font, or moving work off the main thread.
  • The before and after numbers, and any business metric that moved with them.

Note: Mention the budget or CI check you added afterwards so the improvement did not silently erode.

4. How do you handle a situation where requirements change midway through building a feature?

Show that you treat it as normal rather than as a failure of process — because on most teams it is normal.

  • Establish what actually changed. Often the underlying need has not changed at all, only the proposed solution. Asking what problem the new requirement solves sometimes reveals that what you have already built covers it.
  • State the cost honestly and early. What is now wasted, what can be reused, and what the new date looks like. Absorbing scope quietly and missing the date is much worse than a difficult conversation on day three.
  • Protect what is expensive to change. If the change touches a data model or an API contract other teams consume, that needs more scrutiny than a change to layout.

How to reduce the pain structurally: build in thin vertical slices behind a feature flag, so there is always something demonstrable and always a safe way to turn it off. Getting a rough version in front of the stakeholder early is the single most effective way to surface a change before it is expensive.

5. How do you make sure the interfaces you build are accessible, and have you ever had to argue for it?

Split it into what you do by default and what you check.

By default: semantic HTML before ARIA — a real <button> is keyboard-operable, focusable, and announced correctly with no extra work, while a <div onclick> needs four attributes and a key handler to catch up. Labels tied to inputs, headings in order, alt text that says what the image is for, and visible focus styles you never remove.

What you check:

  • Tab through the whole feature without touching the mouse.
  • Run axe or Lighthouse, understanding that automated tools catch perhaps a third of real problems.
  • Test one flow with a screen reader — VoiceOver or NVDA.
  • Check colour contrast at design time.

On arguing for it: the effective arguments are legal exposure, the size of the affected audience, and the fact that retrofitting costs several times more than building it in. The ineffective argument is that it is the right thing to do — true, but it rarely moves a roadmap on its own.

6. Tell me about a bug that only appeared in one browser or on one device. How did you track it down?

The interviewer wants to see a systematic debugging process, not luck. Use STAR, and spend most of your time on the Action.

  • Situation: Name the symptom precisely — for example, a checkout button that did nothing on iOS Safari 16 while every desktop browser worked, reported by customer support with a drop in mobile conversions.
  • Task: You owned the fix and needed it before a sale weekend.
  • Action: Walk through how you narrowed it down. Reproduce on a real device or BrowserStack, connect remote debugging with Safari Web Inspector, check the console, then bisect — disable scripts, strip CSS, compare feature support on caniuse. Name the root cause, such as an unsupported regular expression lookbehind that threw a syntax error and killed the whole bundle, or an element with a transparent overlay capturing taps.
  • Result: Quantify it — conversions recovered, the fix shipped in hours.

Then show what you changed so it could not happen again: adding the browser to your browserslist and transpile targets, a real-device smoke test in the release checklist, or client-side error monitoring such as Sentry grouped by user agent.

Note: Avoid blaming the browser. Strong candidates say what they learned about feature detection, progressive enhancement and testing on the devices their actual users have — which in India is often a mid-range Android phone on a slow network.

7. Describe a code review where you disagreed with a teammate about a front-end approach. How was it resolved?

This question tests collaboration and technical judgement together. The worst answers are stories where you were simply right and the other person gave in.

Structure your answer:

  • The disagreement: Pick something substantive — adding a heavy date library versus using the built-in Intl API, putting server data in Redux versus a query cache, or a clever abstraction that hurt readability.
  • How you raised it: Comment on the code, not the person. Ask questions first: “What made you choose this over X?” Often the author had context you lacked.
  • How you moved it forward: Take long threads off the pull request into a short call. Bring evidence — bundle size from a build analyser, a quick benchmark, an accessibility check, or the team's existing conventions.
  • The outcome: Say honestly who changed their mind. It is fine if it was you. If it was a genuine trade-off, explain how you agreed to decide — the tech lead, an architecture decision record, or trying it behind a flag.

Close with what changed for the team afterwards, such as a lint rule or a documented convention so the same debate did not repeat.

Note: Separate blocking issues (bugs, security, accessibility failures) from preferences. Saying you mark nitpicks as non-blocking shows maturity and respect for the author's time.

8. Tell me about a time you had to ship a front-end feature under a very tight deadline. What trade-offs did you make?

The interviewer is checking whether you can cut scope without cutting quality, and whether you communicate trade-offs openly rather than quietly shipping something fragile.

A strong answer covers:

  • The constraint: A fixed launch date, a marketing campaign or a client demo, with a realistic estimate that did not fit.
  • How you prioritised: You split the feature into must-have and nice-to-have with the product manager. For example, the new filter panel shipped with the three most-used filters, while saved filters and animations moved to the next sprint.
  • What you refused to compromise: Keyboard access and labels, error and loading states, and no console errors in production. Mention that accessibility and security are expensive to retrofit.
  • How you managed the debt: You logged tickets for the shortcuts, used a feature flag so the release could be switched off, and added a few tests around the riskiest path.
  • The result: It shipped on time, and you paid down the debt in the following sprint.

Note: Mention early communication. Telling stakeholders on day two that full scope will not fit is far better than announcing it the night before launch.

9. How would you lead the migration of a legacy jQuery or server-rendered front end to a modern framework such as React?

The interviewer wants to hear that you would avoid a big-bang rewrite and deliver value incrementally while the business keeps shipping.

Points to cover:

  • Make the case first: Tie the migration to business pain — slow feature delivery, frequent regressions, difficulty hiring — not to the framework being fashionable.
  • Incremental strategy: Use the strangler-fig pattern. Mount React components inside existing pages one widget at a time, or route new pages to the new app behind a reverse proxy while old pages keep working.
  • Shared foundations: Set up the build pipeline, design tokens, linting and a component library first so every migrated screen looks consistent.
  • Safety net: Add end-to-end tests around critical journeys such as login and payment before touching them, so behaviour is locked in.
  • Bring the team along: Pair programming, a short internal guide, and a reference implementation of one page that others copy.
  • Measure progress: Track the percentage of traffic or pages migrated, bundle size and Core Web Vitals, and set a date to delete the old code.

Note: If you have done this for real, give numbers: how many screens, how long it took, and what you would sequence differently. Admitting a mistake, such as migrating a low-value page first, makes the story credible.

10. Tell me about a production incident caused by front-end code. How did you respond and what did you change afterwards?

This is about ownership under pressure. Interviewers look for calm triage, clear communication and a blameless follow-up.

Structure the story in four parts:

  • Detection: How you found out — an error-rate alert in Sentry, a spike in support tickets, or a drop in checkout completions on the analytics dashboard.
  • Mitigation first: Your priority was stopping the damage, not finding the perfect fix. Roll back the deployment, switch off the feature flag, or purge a bad CDN cache. Say how long recovery took.
  • Root cause: Be specific. Examples: an old cached HTML page requesting JavaScript chunks that no longer existed after a deploy, an API field that became null and crashed a component with no error boundary, or a third-party script blocking the main thread.
  • Prevention: What you changed so it could not recur — keeping previous asset versions on the CDN, React error boundaries around risky widgets, schema validation of API responses, canary releases, or synthetic monitoring of key journeys.

Mention communication: you kept stakeholders updated in the incident channel and wrote a short post-mortem focused on the process, not the person.

Note: Do not pick an incident you caused carelessly and never fixed. Choose one where the follow-up improved the team's practices, and own your part plainly.

Technical Questions

11. Explain the CSS box model and the difference between content-box and border-box sizing.

Every element is drawn as four nested rectangles: the content box, then padding around it, then the border, then margin outside that. Margin is transparent space between elements and is not part of the element's own background.

box-sizing decides what width refers to:

  • content-box is the default. width: 300px sets the content area alone, so 20px of padding and a 2px border on each side make the element actually occupy 344px. This is why layouts overflow unexpectedly.
  • border-box makes width include padding and border. Set 300px and the element occupies 300px, with the content area shrinking to accommodate the padding.

Almost every codebase applies this reset:

*, *::before, *::after { box-sizing: border-box; }

Note: Be ready for margin collapsing as a follow-up. Adjacent vertical margins between block elements collapse to the larger of the two rather than adding up — and this does not happen in flex or grid containers, or across a border or padding.

12. What is the difference between Flexbox and CSS Grid, and how do you choose between them?

The clean distinction is dimensionality. Flexbox is one-dimensional — it lays items out along a single axis and the other axis follows the content. Grid is two-dimensional — you define rows and columns together and place items into that structure.

Reach for Flexbox when:

  • The content should determine the sizing — a navigation bar, a toolbar, a row of buttons, a card footer.
  • You want items to wrap naturally without caring which row they land on.
  • You need alignment along one axis, including the classic centring case.

Reach for Grid when:

  • The layout should determine the structure — a page skeleton, a dashboard, an image gallery with aligned rows and columns.
  • You need items to line up in both directions, which flex-wrap cannot guarantee.
  • You want to name areas and rearrange them at breakpoints with grid-template-areas, which is far more readable than reordering markup.

Note: They are not rivals. The normal pattern is Grid for the page skeleton with Flexbox inside individual components. And auto-fit with minmax gives you a responsive gallery with no media queries at all: grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))

Free workshop by Jobaaj Learnings

13. How does CSS specificity work, and how is a conflict between two rules resolved?

When several rules target the same element, the browser resolves it in this order:

  • Origin and importance. An !important declaration beats a normal one; author styles beat user-agent defaults.
  • Specificity, counted as three numbers — inline styles, then IDs, then classes, attribute selectors and pseudo-classes, then elements and pseudo-elements. So #nav .item (1,1,0) beats .header .nav .item (0,3,0), because any number of classes cannot outweigh a single ID.
  • Source order. If specificity ties, the rule that appears later wins.

Things that catch people out:

  • The universal selector * and combinators such as > and + add nothing.
  • :not() itself adds nothing, but its argument counts.
  • :where() always has zero specificity, which makes it excellent for defaults that should be easy to override.

Note: The right conclusion is that you should keep specificity flat rather than win specificity battles. Escalating to !important is a symptom, and it makes the next override worse.

14. What is the difference between position relative, absolute, fixed, sticky and static?

position controls what an element is positioned against and whether it stays in the normal document flow.

  • static — the default. In normal flow; top, left, right and bottom are ignored entirely.
  • relative — stays in the flow and still occupies its original space, but is visually shifted by the offsets. Its main use is to create a positioning context for absolutely positioned children.
  • absolute — removed from the flow, so it no longer takes up space. Positioned against the nearest ancestor that is not static; if there is none, against the initial containing block.
  • fixed — removed from the flow and positioned against the viewport, so it does not move when the page scrolls. Used for headers, modals, and cookie banners.
  • sticky — a hybrid. It behaves as relative until it reaches a specified offset while scrolling, then behaves as fixed within its parent's bounds.

Note: Two gotchas worth knowing. Sticky silently does nothing unless you give it a threshold such as top: 0, and it is constrained by its parent — an ancestor with overflow hidden or auto breaks it. And any ancestor with a transform, filter, or will-change becomes the containing block for fixed children, which is a genuinely confusing bug to meet in the wild.

15. What is the critical rendering path, and how do you optimise a page's first paint?

The critical rendering path is everything the browser must do between receiving HTML and painting pixels:

  • Parse HTML into the DOM.
  • Parse CSS into the CSSOM.
  • Combine them into the render tree.
  • Layout — compute the geometry of every box.
  • Paint and composite the layers to the screen.

The two blockers: CSS is render-blocking, because the browser will not paint until it knows the styles. A synchronous <script> in the head is parser-blocking, because it can rewrite the document.

How to make first paint faster:

  • Inline the small amount of CSS needed for above-the-fold content, and load the rest asynchronously.
  • Add defer to scripts so they download in parallel and execute after parsing, or async for independent third-party scripts.
  • Preload the LCP image and the primary font, and use font-display: swap so text is never invisible.
  • Serve modern image formats at the right size, with width and height set so nothing shifts.
  • Reduce JavaScript. It is almost always the largest and most expensive resource on the page.

16. Explain the difference between server-side rendering, static generation and client-side rendering.

All three produce the same page; they differ in when the HTML is built.

  • Client-side rendering (CSR) — the server sends a near-empty HTML shell and JavaScript builds the page in the browser. Cheap to host and excellent for highly interactive applications behind a login, but the first paint waits for the bundle to download and run, and crawlers see little without executing JavaScript.
  • Server-side rendering (SSR) — HTML is built on the server for each request, then hydrated in the browser. Fast first contentful paint and good for SEO, at the cost of server work on every request and a gap where the page looks ready but is not yet interactive.
  • Static site generation (SSG) — HTML is built once at deploy time and served from a CDN. The fastest and cheapest option by a wide margin, but the content is fixed until the next build. Incremental regeneration softens this by rebuilding individual pages on a schedule or on demand.

How to choose: content that is the same for everyone and changes rarely — marketing pages, documentation, blogs — should be static. Content that is personalised or changes constantly needs SSR. A dashboard behind a login can be CSR, because SEO is irrelevant and the shell can be cached.

17. What is the Virtual DOM, and does it make applications faster?

The Virtual DOM is a lightweight JavaScript description of what the UI should look like. When state changes, the library builds a new tree, diffs it against the previous one, and applies only the differences to the real DOM — a process called reconciliation.

The honest answer to whether it is faster is: not compared to hand-written, perfectly targeted DOM updates. Touching the DOM directly with full knowledge of what changed will always beat diffing. What the Virtual DOM actually buys you is a programming model: you describe the UI as a function of state and never write imperative update code, and the library guarantees the result is fast enough by batching updates and avoiding the naive alternative of rebuilding everything.

Worth knowing: several modern frameworks have dropped it. Svelte compiles updates at build time, and Solid uses fine-grained reactivity to update exactly the nodes that depend on a changed signal. Both are faster than diffing, which is good evidence that the Virtual DOM is a means rather than an end.

Note: The keys prop follows directly from this. Keys let the diff match elements across renders; using an array index as a key breaks that matching when the list reorders, and state ends up attached to the wrong item.

18. How do you make a website responsive, and what is the difference between a media query and a container query?

The foundations come before any query:

  • <meta name="viewport" content="width=device-width, initial-scale=1"> — without it, mobile browsers pretend to be 980px wide.
  • Fluid units — percentages, fr, rem, and clamp() for type that scales between a floor and a ceiling.
  • max-width: 100% on images and embedded media.
  • Layout primitives that reflow on their own, such as grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) and flex-wrap.

Media queries respond to the viewport. They are the right tool for page-level decisions — how many columns the page skeleton has, whether the navigation collapses.

Container queries respond to the size of an element's own container:

.card-wrap { container-type: inline-size; }
@container (min-width: 400px) {
  .card { display: grid; grid-template-columns: 120px 1fr; }
}

This is what media queries could never do: the same card component now lays out correctly whether it sits in a wide main column or a narrow sidebar, without knowing anything about the page. For component libraries and design systems, container queries are the correct tool and media queries are a workaround.

19. What is CORS, and how would you fix a CORS error?

Browsers enforce the same-origin policy: a page at one origin — scheme, host, and port together — cannot read a response from a different origin unless that server allows it. CORS is the mechanism a server uses to grant that permission through response headers.

Simple requests — a GET or POST with a small set of allowed headers — are sent immediately, and the browser checks Access-Control-Allow-Origin on the response before letting JavaScript read it. Anything else triggers a preflight: the browser first sends an OPTIONS request asking whether the real request is permitted.

How to fix it, in order:

  • The fix belongs on the server. Nothing you write in the browser can grant your own permission — that is the entire point.
  • Set Access-Control-Allow-Origin to the specific origin, and Access-Control-Allow-Methods and Access-Control-Allow-Headers to cover what the client actually sends.
  • Make sure the server answers OPTIONS with a 2xx and no body. A preflight hitting an auth middleware that returns 401 is an extremely common cause.
  • If you need cookies, set Access-Control-Allow-Credentials: true — and note that you may not use * for the origin in that case.

Note: A proxy in your dev server is a legitimate local workaround, but say plainly that it is not a production fix. And a CORS error never means the request was blocked from being sent — it means the response was blocked from being read, so the server has already done the work.

20. What is the difference between localStorage, sessionStorage and cookies?

Three client-side storage mechanisms with different lifetimes, sizes, and — most importantly — different exposure.

  • localStorage — around 5-10MB, persists until explicitly cleared, shared across every tab on the origin, and never sent to the server. Synchronous string-only API.
  • sessionStorage — the same API and roughly the same size, but scoped to a single tab and cleared when that tab closes. Two tabs on the same site have entirely separate sessionStorage.
  • Cookies — only about 4KB, and sent with every HTTP request to the origin, which is both their purpose and their cost. They have an explicit expiry and, crucially, security attributes the other two lack.

The point interviewers are usually driving at: where to keep an authentication token. localStorage is readable by any JavaScript on the page, so a single XSS vulnerability hands over every user's session. A cookie marked httpOnly cannot be read by JavaScript at all; adding secure restricts it to HTTPS and sameSite defends against CSRF. That is why httpOnly cookies are the recommended place for session tokens and localStorage is not.

Note: For structured or larger data, IndexedDB is the right answer — asynchronous, far larger, and it stores real objects rather than strings.

21. What is semantic HTML, and why does it matter for accessibility and SEO?

Semantic HTML means choosing elements for their meaning rather than their appearance — <header>, <nav>, <main>, <article>, <button>, <h1> to <h6> — instead of a page built from <div> and <span> styled to look right.

Why it matters:

  • Accessibility: Browsers expose semantics to the accessibility tree. A screen reader user can jump between landmarks and headings, and hears “button” for a <button>. A clickable <div> is announced as plain text, cannot be focused with Tab and does not respond to Enter or Space unless you rebuild all of that by hand.
  • Free behaviour: Native elements bring focus handling, keyboard support, form submission and validation with no JavaScript.
  • SEO: Search engines use headings, <article>, <time> and links to understand structure and importance. A logical heading outline helps them extract the main topic.
  • Maintainability: Code that reads as a document outline is easier for the next developer to follow.
<!-- avoid -->
<div class='btn' onclick='save()'>Save</div>
<!-- prefer -->
<button type='button' onclick='save()'>Save</button>

Note: Use one <h1> per page and do not skip heading levels for styling. Pick the level by structure and change the size with CSS.

22. When should you use ARIA attributes, and what is meant by the first rule of ARIA?

ARIA (Accessible Rich Internet Applications) adds roles, states and properties to the accessibility tree so assistive technology can understand custom widgets. It changes only what is announced — never behaviour. It adds no focus, no keyboard handling and no styling.

The first rule of ARIA: if a native HTML element or attribute already gives you the semantics and behaviour you need, use it instead of adding ARIA. A <button> beats a <div role='button'>, and the native <dialog> beats a hand-built modal.

When ARIA is genuinely needed:

  • Custom widgets with no native equivalent: tabs (role='tablist', aria-selected), comboboxes, tree views.
  • Dynamic state: aria-expanded on a menu toggle, aria-pressed on a toggle button, aria-invalid on a failing field.
  • Relationships: aria-describedby linking an input to its error text, aria-controls.
  • Live updates: aria-live='polite' so a “Saved” message or search result count is announced.
  • Names for icon-only controls: aria-label='Close'.

Common mistakes: aria-hidden='true' on something focusable, roles without the matching keyboard support, and redundant roles such as role='button' on a real button.

Note: “No ARIA is better than bad ARIA.” Automated audits find pages with ARIA often have more errors, so test custom widgets against the ARIA Authoring Practices keyboard patterns.

23. How do you manage keyboard focus in a single-page application, for example for modals and route changes?

In a traditional site every navigation resets focus to the top of the new page. In an SPA the DOM changes under the user, so you must move focus deliberately or keyboard and screen reader users get lost.

Modals and dialogs:

  • On open, move focus into the dialog — to the first field, or to the heading with tabindex='-1'.
  • Trap focus inside while open, so Tab does not reach the page behind. The native <dialog> element opened with showModal() does this for you and makes the rest of the page inert.
  • Close on Escape.
  • On close, return focus to the control that opened it.
const opener = document.activeElement;
dialog.showModal();
dialog.addEventListener('close', () => opener.focus());

Route changes:

  • After the new view renders, move focus to its main heading (with tabindex='-1' so it is focusable but not in the tab order) or to a wrapper around the main content.
  • Update document.title so the new page is announced.
  • Some teams also announce the page name through an aria-live region.

Other essentials: never remove the focus outline without a visible replacement — use :focus-visible — and provide a skip link to the main content.

Note: Test by unplugging the mouse. If you cannot complete the main journey with Tab, Shift+Tab, Enter, Space and the arrow keys, the page is not accessible.

24. What are the WCAG POUR principles, and what do conformance levels A, AA and AAA mean?

The Web Content Accessibility Guidelines (WCAG) from the W3C are the standard most accessibility laws and procurement rules refer to. Version 2.2 is current. Its success criteria are grouped under four principles, remembered as POUR:

  • Perceivable: users can perceive the content — text alternatives for images, captions for video, sufficient colour contrast, and information not conveyed by colour alone.
  • Operable: everything works with a keyboard, users have enough time, nothing flashes dangerously, focus is visible, and touch targets are large enough.
  • Understandable: readable text, predictable navigation, clear labels and helpful error messages.
  • Robust: markup works with current and future assistive technologies — valid semantics and correct names, roles and values.

Conformance levels:

LevelMeaningExample
ABare minimum; failing blocks some users entirelyAlt text, keyboard access
AAThe usual legal and contractual target4.5:1 contrast for body text, visible focus
AAAEnhanced; not expected for whole sites7:1 contrast, sign language for video

In practice, teams target WCAG 2.2 AA, check it with automated tools such as axe or Lighthouse, then do manual keyboard and screen reader testing, because automation catches only around a third of issues.

Note: Mention that India's public-sector guidelines (GIGW) and the EU's accessibility rules both build on WCAG, so AA compliance is increasingly a business requirement, not a nice-to-have.

25. What are CSS cascade layers, and how do they change the way conflicting rules are resolved?

Cascade layers, declared with the @layer at-rule, let you group styles into named layers and decide their priority explicitly. In the cascade, layer order is checked before specificity, so a simple selector in a later layer beats a highly specific selector in an earlier one.

@layer reset, vendor, components, utilities;

@layer vendor {
.datepicker .header button.primary { color: grey; }
}
@layer components {
.btn { color: navy; } /* wins: later layer */
}

How the order works:

  • The first @layer statement fixes the order; layers declared later take precedence for normal declarations.
  • Unlayered styles beat all layered styles. That surprises people when they migrate gradually.
  • For !important declarations the order reverses — an important rule in an earlier layer beats one in a later layer, which lets a reset protect critical rules.
  • You can import third-party CSS straight into a layer: @import url(lib.css) layer(vendor);

Why it matters: it ends specificity wars. Instead of stacking selectors or adding !important to override a library, you put the library in a low layer and your components above it. Utility classes can sit in the top layer and always win without hacks.

Note: Pair this with the zero-specificity :where() for base styles. Together they give you predictable overrides, and layers are supported in all modern browsers since 2022.

26. What are CSS custom properties, and how do they differ from Sass or Less variables?

CSS custom properties (often called CSS variables) are properties you name with a double-dash prefix and read with var(). They are part of the live cascade in the browser.

:root { --brand: #0b5fff; --space: 1rem; }
.card { padding: var(--space); border-color: var(--brand, blue); }
.theme-dark { --brand: #7aa7ff; }

How they differ from preprocessor variables:

Custom propertiesSass variables
Resolved at runtime in the browserReplaced with fixed values at build time
Inherit and cascade down the DOMScoped lexically in the source file
Can change with media queries, classes or JavaScriptCannot change after compilation
Visible and editable in DevToolsGone from the output CSS

Where they shine:

  • Theming: dark mode or white-label brands by redefining a few tokens on a parent element.
  • Component APIs: a component reads --card-radius, and consumers override it without touching internals.
  • JavaScript bridges: el.style.setProperty('--x', '40px') to drive animations or pointer-following effects.

The second argument to var() is a fallback used when the property is undefined. The @property rule can register a type and initial value, which also lets custom properties be animated smoothly.

Note: Sass variables are still useful for build-time logic such as breakpoints inside media query conditions, where var() cannot be used. Many teams use both.

27. What is the difference between reflow, repaint and compositing, and how do you avoid layout thrashing?

After building the DOM and CSSOM, the browser runs a pipeline for each frame: Style → Layout → Paint → Composite. Where a change enters that pipeline decides its cost.

  • Reflow (layout): recalculating the size and position of elements. Triggered by changing width, height, margin, top, font size, adding DOM nodes or resizing the window. It is the most expensive because it can cascade to children and siblings, then requires paint and composite too.
  • Repaint: redrawing pixels without changing geometry — color, background, box-shadow, visibility.
  • Compositing: combining already-painted layers on the GPU. Changing transform or opacity on an element in its own layer skips layout and paint entirely, which is why smooth animations use them.

Layout thrashing happens when JavaScript alternates reads of layout properties (offsetHeight, getBoundingClientRect()) with writes, forcing a synchronous layout on every iteration.

// thrashing: read-write-read-write
items.forEach(el => { el.style.width = box.offsetWidth + 'px'; });

// fixed: read once, then write
const w = box.offsetWidth;
items.forEach(el => { el.style.width = w + 'px'; });

Other techniques: batch writes inside requestAnimationFrame, animate transform instead of left, use contain: layout or content-visibility: auto to limit the scope of work, and use ResizeObserver instead of polling sizes.

Note: Use will-change: transform sparingly. Every promoted layer costs GPU memory, and promoting hundreds of elements can make a page slower, especially on low-end phones.

28. What are the Core Web Vitals LCP, INP and CLS, and how would you improve each one?

Core Web Vitals are Google's user-centred metrics, measured on real users at the 75th percentile and used as a search ranking signal.

MetricMeasuresGood
LCP — Largest Contentful PaintLoading: when the main content appears2.5 s or less
INP — Interaction to Next PaintResponsiveness to taps, clicks and keys200 ms or less
CLS — Cumulative Layout ShiftVisual stability0.1 or less

Improving LCP:

  • Cut server response time with caching and a CDN.
  • Make the hero image discoverable in the HTML, preload it or set fetchpriority='high', and never lazy-load it.
  • Serve modern formats such as AVIF or WebP at the right size, and remove render-blocking CSS and scripts.

Improving INP (it replaced First Input Delay in March 2024):

  • Break long tasks over 50 ms into smaller chunks and yield to the main thread, for example with scheduler.yield() or setTimeout.
  • Ship less JavaScript, defer third-party scripts, and keep event handlers light — show feedback first, do heavy work after.
  • Avoid huge DOM updates on each keystroke; debounce or virtualise long lists.

Improving CLS:

  • Set width and height or aspect-ratio on images, videos and ad slots.
  • Reserve space for banners and embeds, and do not insert content above existing content.
  • Use font-display with fallback font metric overrides to reduce shifts when web fonts swap in.

Note: Distinguish lab data (Lighthouse) from field data (Chrome UX Report, the web-vitals library). Rankings use field data, and INP in particular barely shows up in a lab test with no real interactions.

29. How do you optimise image loading on a modern website?

Images are usually the heaviest bytes on a page and often the LCP element, so they deserve a deliberate strategy.

1. Choose the right format. AVIF and WebP are typically 25–50% smaller than JPEG at similar quality. Use SVG for icons and logos. The <picture> element offers modern formats with a fallback:

<picture>
<source type='image/avif' srcset='hero.avif'>
<source type='image/webp' srcset='hero.webp'>
<img src='hero.jpg' alt='Team at work' width='1200' height='600'>
</picture>

2. Serve the right size. Use srcset with width descriptors and sizes so a phone does not download a 2000px desktop image:

<img srcset='card-400.webp 400w, card-800.webp 800w'
sizes='(max-width: 600px) 100vw, 400px' src='card-800.webp' alt=''>

3. Prioritise correctly.

  • Hero or LCP image: fetchpriority='high', no lazy loading, and ideally present in the initial HTML rather than injected by JavaScript.
  • Below-the-fold images: loading='lazy' and decoding='async'.

4. Prevent layout shift. Always set width and height attributes or a CSS aspect-ratio so the browser reserves space.

5. Deliver efficiently. Use an image CDN that resizes and converts on the fly, compress at build time, and cache with long-lived, content-hashed URLs.

Note: CSS background images are invisible to the preload scanner. If your LCP image is a background, preload it with a link rel='preload' as='image' tag or switch to a real img element.

30. How do you load web fonts efficiently, and what do the different font-display values do?

Web fonts are render-critical: text styled with a font that has not arrived cannot be drawn in its final form. Poor font loading causes invisible text, layout shift and slower LCP.

font-display values control what happens during the load:

  • block: text is invisible for up to about 3 seconds, then falls back. Causes the flash of invisible text (FOIT).
  • swap: fallback text shows immediately and is swapped when the font arrives — good for brand fonts, but can cause a visible shift.
  • fallback: a very short invisible period (about 100 ms) and a short swap window; if the font is late, the fallback stays.
  • optional: the font is used only if it is available almost immediately, otherwise the fallback is kept for that page view. Best for CLS and body text.
  • auto: browser default, usually like block.

Loading techniques:

  • Self-host WOFF2 files instead of chaining requests to a third-party domain.
  • Preload only the one or two critical files: <link rel='preload' href='/f/inter.woff2' as='font' type='font/woff2' crossorigin>
  • Subset fonts with unicode-range — for example, separate Latin and Devanagari files — and prefer a single variable font over many weights.
  • Reduce swap shift by tuning the fallback with size-adjust, ascent-override and descent-override so it matches the web font's metrics.

Note: Font preload requires the crossorigin attribute even for same-origin files, because fonts are fetched in CORS mode. Without it, the browser downloads the font twice.

31. What does a JavaScript bundler do, and how do code splitting and dynamic import reduce load time?

A bundler such as Vite (Rollup under the hood), webpack, esbuild or Rspack starts at your entry file, follows every import to build a dependency graph, then outputs optimised files for the browser. Along the way it transpiles TypeScript and JSX, removes unused exports (tree shaking), minifies, and adds content hashes to filenames for caching.

Without splitting, every route and library ends up in one large bundle that must be downloaded, parsed and executed before the app is interactive. Code splitting breaks that into chunks loaded on demand.

Dynamic import is the main tool. import() returns a promise, and the bundler turns each call site into a separate chunk:

// route-level splitting in React
const Reports = React.lazy(() => import('./pages/Reports'));

<Suspense fallback={<Spinner />}>
<Reports />
</Suspense>

// load a heavy library only when needed
button.onclick = async () => {
const { jsPDF } = await import('jspdf');
new jsPDF().save('invoice.pdf');
};

Common splitting points:

  • Routes — the biggest win for most apps.
  • Heavy, rarely used features — charts, rich text editors, PDF export, maps.
  • Vendor chunks — libraries that change rarely get their own long-cached file.

Too many tiny chunks create request waterfalls, so measure with a bundle analyser and prefetch likely next routes when the browser is idle.

Note: Code splitting helps initial load but can delay the first interaction with a split feature. Prefetch chunks on hover or on idle so the user rarely waits.

32. How does HTTP caching work with Cache-Control and ETag, and how would you cache HTML versus static assets?

HTTP caching lets browsers and CDNs reuse responses instead of downloading them again. It is controlled mostly by response headers.

Key headers:

  • Cache-Control: max-age=N — the response is fresh for N seconds and can be used without contacting the server.
  • no-cache — may be stored, but must be revalidated with the server before each use. (It does not mean “do not cache”.)
  • no-store — never store at all; for sensitive responses.
  • private / public — whether shared caches such as CDNs may store it. Personalised pages must be private.
  • immutable — the file will never change, so do not revalidate even on reload.
  • ETag and Last-Modified — validators. The browser sends If-None-Match and gets a tiny 304 Not Modified if nothing changed.

The standard strategy:

ResourceHeaderWhy
Hashed JS, CSS, fonts (app.3f9a1c.js)public, max-age=31536000, immutableA new build produces a new filename
HTML documentsno-cache (private if personalised)Always check for a new deploy, which points to new asset names
API responsesShort max-age or no-storeDepends on how fresh data must be

This gives instant repeat visits while deploys take effect immediately, because the small HTML file is always revalidated.

Note: Never put a long max-age on HTML for logged-in users. A shared cache or stale page can serve one user's content or an expired session state to someone else.

33. What is a service worker, and what caching strategies can it implement?

A service worker is a script that runs in the background, separate from the page, and acts as a programmable network proxy for its scope. It can intercept every request with the fetch event, serve responses from the Cache API, and enable offline support, push notifications and background sync. It requires HTTPS (localhost is allowed) and has no DOM access.

Lifecycle: register → install (pre-cache the app shell) → activate (delete old caches) → controls pages. A new version waits until all tabs using the old one close, unless you call skipWaiting().

self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(hit => hit ?? fetch(e.request))
);
});

Common strategies:

  • Cache first: serve from cache, go to the network only on a miss. Best for hashed static assets and fonts.
  • Network first: try the network, fall back to cache when offline. Good for HTML and frequently changing API data.
  • Stale-while-revalidate: return the cached copy instantly and update the cache in the background for next time. Good for avatars, non-critical API data.
  • Network only / cache only: for analytics or payments, and for pre-cached shell files respectively.

Most teams use Workbox rather than writing this by hand, because cache versioning and expiry are easy to get wrong.

Note: A buggy service worker can keep serving a broken version long after you deploy a fix. Always ship a way to update or unregister it, and never cache authenticated HTML with cache first.

34. What is cross-site scripting, what are its main types, and how do you prevent it in a front-end application?

Cross-site scripting (XSS) is when an attacker gets their JavaScript to run in your page in another user's browser. The script runs with your origin's privileges, so it can read page data, call your APIs as the victim, capture keystrokes or steal tokens kept in localStorage.

Main types:

  • Stored: malicious input is saved (a comment, a profile name) and served to every viewer.
  • Reflected: input from the URL or a form is echoed straight back in the response.
  • DOM-based: client-side code takes data from location.hash, postMessage or an API and writes it into a dangerous sink such as innerHTML, without the server being involved.

Prevention:

  • Contextual output encoding. Frameworks such as React, Angular and Vue escape interpolated values by default — keep it that way.
  • Avoid dangerous sinks: innerHTML, outerHTML, document.write, eval, new Function, React's dangerouslySetInnerHTML. Use textContent for text.
  • Sanitise when HTML is genuinely required, for example rich text, with a maintained library such as DOMPurify.
  • Validate URLs before putting them in href or src, blocking the javascript: scheme.
  • Defence in depth: a strict Content Security Policy, HttpOnly cookies so scripts cannot read session tokens, and Trusted Types to lock down DOM sinks.
el.textContent = userInput;           // safe
el.innerHTML = DOMPurify.sanitize(html); // if HTML is needed

Note: Server-side validation does not fix XSS on its own. Encoding must happen at output time for the specific context — HTML body, attribute, URL or JavaScript.

35. How would you roll out a Content Security Policy on an existing site without breaking it?

A Content Security Policy (CSP) is a response header that tells the browser which sources of script, style, images, frames and connections are allowed. It is a strong second line of defence against XSS, but a strict policy dropped on a legacy site will break inline scripts, analytics and third-party widgets. Roll it out in stages.

1. Start in report-only mode. The browser reports violations without blocking anything:

Content-Security-Policy-Report-Only:
default-src 'self'; script-src 'self' 'nonce-r4nd0m' 'strict-dynamic';
object-src 'none'; base-uri 'self'; report-to csp-endpoint

2. Collect and triage reports for a few weeks. You will find inline event handlers, inline scripts, tag managers and CDNs you forgot about.

3. Refactor the code:

  • Move inline onclick handlers into addEventListener calls.
  • Give necessary inline scripts a per-request nonce generated on the server, or a hash.
  • Remove eval and string-based setTimeout.

4. Prefer a strict, nonce-based policy with 'strict-dynamic' over long domain allowlists, which are easy to bypass through JSONP endpoints on allowed CDNs.

5. Enforce by switching to the real Content-Security-Policy header, and keep reporting on so regressions show up.

Also add frame-ancestors to stop clickjacking and upgrade-insecure-requests for mixed content.

Note: Never use 'unsafe-inline' for scripts as a permanent fix — it disables most of the XSS protection a CSP provides. A nonce is the right answer for inline scripts you must keep.

36. What is CSRF, and how do SameSite cookies and anti-CSRF tokens protect against it?

Cross-site request forgery (CSRF) tricks a logged-in user's browser into sending a request the user never intended. Because browsers attach cookies automatically, a hidden form on evil.example that posts to bank.example/transfer arrives with the victim's valid session cookie, and the server cannot tell it apart from a real request.

CSRF differs from XSS: the attacker cannot read the response, but can trigger state-changing actions such as changing an email address, transferring money or deleting data.

Defences:

  • SameSite cookie attribute:
    • Strict — never sent on cross-site requests, even when following a link. Most secure, but users arriving from an email link appear logged out.
    • Lax — sent on top-level GET navigations from other sites, but not on cross-site POSTs, iframes or fetch calls. The default in Chrome for cookies without the attribute, and a good baseline.
    • None — always sent; requires Secure. Needed only for genuine cross-site use such as embedded widgets.
  • Synchronizer token: the server issues a random token per session, embeds it in forms or a meta tag, and rejects state-changing requests whose token does not match. An attacker's page cannot read it.
  • Double-submit cookie: the same token in a cookie and a custom header, which the server compares — common in SPAs.
  • Check Origin or Sec-Fetch-Site headers on the server for unsafe methods.
  • Never change state on GET requests.

Note: APIs authenticated with a bearer token in the Authorization header are not exposed to classic CSRF, because the browser does not attach that header automatically. The trade-off is that such tokens must be kept away from XSS.

37. Explain the JavaScript event loop, and the difference between microtasks and macrotasks.

JavaScript in the browser runs on a single main thread. The event loop is the mechanism that lets it handle timers, network responses and user input without blocking: long-running work is done by browser APIs, and their callbacks are queued to run when the call stack is empty.

One turn of the loop:

  1. Run one macrotask (task) to completion — a script, a setTimeout callback, a click handler, a message event.
  2. Drain the entire microtask queuePromise.then callbacks, await continuations, queueMicrotask, MutationObserver callbacks. Microtasks queued during this step also run before moving on.
  3. If it is time for a frame, run requestAnimationFrame callbacks, then style, layout and paint.
  4. Repeat.
console.log('1');
setTimeout(() => console.log('4'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('2');
// output: 1 2 3 4

Why it matters in front-end work:

  • A long synchronous task blocks rendering and input, which is what hurts INP. Break work into chunks and yield back to the loop.
  • An endless chain of microtasks can starve rendering just like a while loop, because the queue must be empty before paint.
  • setTimeout(fn, 0) is not immediate — it waits for the current task, all microtasks and possibly a render, and browsers clamp nested timers to at least 4 ms.

Note: Heavy computation belongs in a Web Worker, which runs on a separate thread with its own event loop and communicates through postMessage, keeping the main thread free for the UI.

38. How do event bubbling and capturing work, and what is event delegation?

When you click an element, the event travels through the DOM in three phases:

  1. Capturing: from window down through ancestors to the target.
  2. Target: at the element itself.
  3. Bubbling: back up from the target through each ancestor to window.

Listeners run in the bubbling phase by default. Pass { capture: true } to run during capture instead. event.target is the element that was actually clicked; event.currentTarget is the element whose listener is running. A few events such as focus, blur and mouseenter do not bubble — use focusin and focusout when you need bubbling versions.

Event delegation uses bubbling to attach one listener to a common ancestor instead of one per child:

list.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-id]');
if (!btn) return; // click was not on a delete button
removeItem(btn.dataset.id);
});

Benefits:

  • Fewer listeners — one handler for a table of a thousand rows, which saves memory and setup time.
  • Works for dynamic content — rows added later are handled automatically without rebinding.
  • Simpler cleanup — removing items never leaves orphaned listeners.

Use closest() because the click target is often an icon or span inside the button, not the button itself.

Note: React already delegates: it attaches listeners at the root container and dispatches synthetic events, which is why calling stopPropagation in a React handler does not stop a native listener attached to the same element.

39. In React, what is the difference between state and props, and why are state updates batched rather than applied immediately?

Props are inputs passed from a parent to a child. They are read-only inside the child — a component must never modify its own props. State is data a component owns and manages over time, created with useState or useReducer. Changing state schedules a re-render of that component and its children.

A useful rule: if a value can be computed from props or other state, it should not be state at all — derive it during render.

Why updates are not immediate: calling a setter does not change the variable in the current render. It asks React to render again, and each render sees a snapshot of state. React also batches updates — since React 18, multiple setter calls inside event handlers, promises and timeouts are combined into a single re-render. This avoids wasted renders and prevents the UI showing half-updated state.

const [count, setCount] = useState(0);

function addThree() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1); // count ends at 1
}

function addThreeCorrectly() {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1); // count ends at 3
}

Use the updater function form whenever the new value depends on the previous one.

Other rules:

  • Treat state as immutable. Create new objects and arrays ([...items, x]) instead of mutating, or React may not detect the change.
  • Lift state up to the closest common parent when two siblings need the same data.

Note: If you really need a synchronous DOM update after setting state, for example to measure an element, flushSync exists, but it is an escape hatch and hurts performance if overused.

40. How does useEffect work in React, what does the dependency array do, and what are the most common mistakes?

useEffect lets a component synchronise with something outside React — a subscription, a timer, a browser API, a non-React widget or a network request. The effect runs after React has committed the render to the screen.

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

The dependency array:

  • Omitted: runs after every render.
  • []: runs once after mount; cleanup runs on unmount.
  • [a, b]: runs after mount and again whenever a or b changes (compared with Object.is). Before re-running, React runs the previous cleanup.

Common mistakes:

  • Missing dependencies, which causes stale closures reading old values. Follow the react-hooks/exhaustive-deps lint rule instead of silencing it.
  • Objects or functions created during render as dependencies — they change identity every render and cause infinite loops. Move them inside the effect or memoise them.
  • Using effects for derived data — setting state from other state in an effect causes an extra render. Compute it during render.
  • Using effects for user events — logic that responds to a click belongs in the click handler.
  • Race conditions in data fetching — ignore stale responses with a flag or AbortController in the cleanup.

Note: In development, StrictMode mounts, unmounts and remounts components to reveal missing cleanups, so an effect running twice locally is intentional. For data fetching, most teams now use a library such as TanStack Query or their framework's loaders.

41. How does React reconciliation decide whether to update or recreate a component, and what role do keys play?

Reconciliation is how React compares the new element tree from a render with the previous one and works out the minimum set of DOM changes. A general tree diff is too slow, so React uses two heuristics that make it roughly linear.

1. Different type means a new subtree. If an element changes type — a <div> becomes a <section>, or UserCard becomes AdminCard — React unmounts the old subtree, destroying its DOM nodes and all state, and mounts a fresh one. Same type means React keeps the instance and its state and only updates the changed props.

2. Keys identify children in a list. Without keys, React matches children by position. With keys, it matches by identity, so it can move, insert or remove items correctly.

{todos.map(t => <TodoItem key={t.id} todo={t} />)}

Practical consequences:

  • Keys must be stable and unique among siblings — a database id, not Math.random(), which remounts every item on every render.
  • Using the array index as key breaks when items are inserted, removed or reordered: state such as a typed input value stays attached to the wrong row.
  • Changing a key deliberately resets a component. <ProfileForm key={userId} /> gives a clean form when switching users, without effects to clear state.
  • Never define a component inside another component. The inner function is a new type on every render, so its subtree remounts and loses state and focus each time.

Note: State is tied to a position in the tree, not to the component function. Rendering the same component at the same position keeps its state, even when conditional logic makes it look like two different elements in your code.

42. When should you use React.memo, useMemo and useCallback, and when are they a waste?

All three are memoisation tools that skip work when inputs have not changed. They are performance optimisations, not correctness tools — the app should work identically without them.

  • React.memo(Component) skips re-rendering a component when its props are shallowly equal to last time.
  • useMemo(fn, deps) caches a computed value between renders.
  • useCallback(fn, deps) caches a function's identity between renders. It is useMemo that returns a function.
const Row = React.memo(function Row({ item, onSelect }) { /* ... */ });

function List({ items, query }) {
const visible = useMemo(
() => items.filter(i => i.name.includes(query)), [items, query]);
const onSelect = useCallback(id => setSelected(id), []);
return visible.map(i => <Row key={i.id} item={i} onSelect={onSelect} />);
}

Use them when:

  • A calculation is genuinely expensive — filtering or sorting thousands of rows, heavy formatting — and you have measured it in the React Profiler.
  • A value or callback is passed to a React.memo child; otherwise a new object or function every render defeats the memo.
  • A value is a dependency of an effect and must keep a stable identity.

They are a waste when the computation is trivial, the child is not memoised, or props change on every render anyway. Each hook adds memory and comparison cost, plus code noise.

Often a better fix is structural: move state down closer to where it is used, or pass slow children as children so they are not re-created by a parent's state change.

Note: The React Compiler, stable in 2025, adds this memoisation automatically at build time. In codebases that use it, manual useMemo and useCallback are needed far less often, so mention it if the team is on a recent React version.

43. How do you decide between local component state, React Context, and a state library such as Redux Toolkit or Zustand?

The first step is to recognise that “state” is several different things, and each has a natural home.

Kind of stateBest home
UI state for one component (open, hovered, input text)useState / useReducer
Server data (users, orders) — cached, refetched, sharedTanStack Query, SWR, RTK Query or framework loaders
URL state (filters, page, tab)The router's search params
Form stateReact Hook Form or local state
Rarely changing app-wide values (theme, locale, current user)Context
Frequently changing client state shared by distant componentsZustand, Redux Toolkit, Jotai

Why not put everything in Context? Every consumer re-renders when the context value changes. That is fine for a theme, but slow for a value that changes on every keystroke. Context is a dependency-injection mechanism, not a state manager with selectors.

When a library earns its place:

  • Many components across the tree read and update the same client state, such as a cart, a multi-step editor or a canvas.
  • You need fine-grained subscriptions so components re-render only for the slice they select.
  • You want devtools, time-travel debugging, middleware or predictable update logic in a large team — Redux Toolkit's strength.
  • Zustand suits smaller teams wanting a tiny API with selectors and no provider.

Note: A common anti-pattern is copying API responses into Redux and writing loading flags by hand. Moving server data to a query cache usually removes most of the global store, leaving very little true client state.

44. What is the difference between controlled and uncontrolled form inputs in React, and when would you use each?

The difference is who owns the current value — React state or the DOM.

Controlled input: React state is the single source of truth. The input displays the value from state and every change goes through a handler.

const [email, setEmail] = useState('');
<input value={email} onChange={e => setEmail(e.target.value)} />

Uncontrolled input: the DOM keeps the value. You set an initial value with defaultValue and read it when needed through a ref or FormData.

function onSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget);
save(data.get('email'));
}
<form onSubmit={onSubmit}><input name='email' defaultValue='' /></form>

Use controlled inputs when you need to:

  • Validate or format as the user types (for example, masking a phone number).
  • Enable or disable a submit button based on the value.
  • Keep several fields in sync or derive one from another.

Use uncontrolled inputs when:

  • You only need the values on submit — simpler code and no re-render on each keystroke.
  • Integrating with non-React code, or for file inputs, which are always uncontrolled.
  • Using React 19 form actions, which work naturally with FormData.

Libraries like React Hook Form use uncontrolled inputs with refs for performance, while still offering validation.

Note: Do not switch an input between modes. Passing value as undefined at first and a string later triggers the “changing an uncontrolled input to be controlled” warning — initialise state with an empty string instead.

45. How do you approach testing a front-end application, and what is the philosophy behind React Testing Library?

A balanced strategy uses different kinds of tests for different risks, often described as the testing trophy: static checks at the base, a large layer of integration tests, fewer unit tests for pure logic, and a small number of end-to-end tests.

  • Static analysis: TypeScript and ESLint catch typos and type errors for almost no cost.
  • Unit tests (Vitest or Jest): pure functions — price calculations, date formatting, reducers.
  • Integration or component tests: render a component or small feature with its children and assert on behaviour. This gives the most confidence per line of test code.
  • End-to-end tests (Playwright or Cypress): a real browser driving critical journeys — sign-up, login, checkout — against a running app.
  • Extras: accessibility checks with axe, and visual regression tests for design systems.

React Testing Library's philosophy: “The more your tests resemble the way your software is used, the more confidence they can give you.” Test behaviour, not implementation details. Query elements the way a user or screen reader finds them, and interact through real events.

test('shows an error for a bad email', async () => {
render(<SignupForm />);
await userEvent.type(screen.getByLabelText(/email/i), 'abc');
await userEvent.click(screen.getByRole('button', { name: /sign up/i }));
expect(await screen.findByText(/valid email/i)).toBeInTheDocument();
});

Prefer getByRole and getByLabelText over test ids, which also nudges you towards accessible markup. Mock the network at the boundary with Mock Service Worker rather than mocking your own modules.

Note: Tests that check internal state or call counts of private functions break on every refactor without catching real bugs. If a refactor keeps behaviour the same, good tests should still pass.

46. What are IndexedDB and the Cache API, and when would you use them instead of localStorage?

localStorage is convenient but limited: it is synchronous (so large reads block the main thread), stores only strings, is capped at about 5 MB per origin, and is not available in service workers. For anything larger or structured, the browser offers two better stores.

IndexedDB is an asynchronous, transactional database in the browser.

  • Stores structured data — objects, arrays, dates, Blobs and files — without JSON serialisation.
  • Supports indexes and key ranges for querying, and transactions for consistency.
  • Quotas are large, typically a share of free disk space, and it works in Web Workers and service workers.
  • The raw API is event-based and verbose, so most teams use a wrapper such as idb or Dexie.
import { openDB } from 'idb';
const db = await openDB('app', 1, {
upgrade(db) { db.createObjectStore('drafts', { keyPath: 'id' }); }
});
await db.put('drafts', { id: 42, body: 'Hello', updated: new Date() });

The Cache API stores HTTP Request and Response pairs. It is designed for service workers to cache pages, scripts, images and API responses for offline use, but is also available on the page.

Choosing:

NeedUse
Small preferences, such as themelocalStorage
Offline data, drafts, large lists, filesIndexedDB
Network responses for offline loadingCache API
Session tokenHttpOnly cookie, not any of these

Note: Browser storage can be evicted under storage pressure, and Safari may clear script-written storage after a period without visits. Call navigator.storage.persist() for important offline data, and always treat the server as the source of truth.

47. What is hydration in server-rendered React applications, and what causes hydration mismatch errors?

With server-side rendering, the server sends fully formed HTML so the user sees content quickly. But that HTML is inert — no event handlers are attached. Hydration is the step where React runs in the browser, renders the same component tree, matches it against the existing DOM and attaches event listeners and state, instead of rebuilding the DOM from scratch.

import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);

Hydration assumes the first client render produces exactly the same output as the server. If it does not, React reports a hydration mismatch and may discard and re-render that part of the tree, which costs performance and can cause flicker.

Common causes:

  • Time and randomness: new Date(), Math.random() or generated ids that differ between server and client. Use useId for ids.
  • Browser-only values during render: reading window, localStorage or the screen width to decide what to render.
  • Locale and time zone differences in date and number formatting.
  • Invalid HTML nesting, such as a <div> inside a <p>, which the browser's parser rearranges.
  • Browser extensions injecting attributes into the page.

Fixes: render a stable placeholder first and update in useEffect after mount; pass the server's value (such as the time zone) down as data; or mark genuinely unavoidable differences with suppressHydrationWarning on that single element.

Note: Hydration has a cost: the page can look ready before it responds to clicks. React Server Components, streaming with Suspense and selective hydration reduce how much JavaScript must hydrate before the page becomes interactive.

48. What is the difference between debouncing and throttling, and when would you use each?

Both techniques limit how often a function runs in response to a rapid stream of events, protecting the main thread and your server.

Debounce waits until events have stopped for a set period, then runs once. Every new event resets the timer.

function debounce(fn, wait) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
input.addEventListener('input', debounce(e => search(e.target.value), 300));

Throttle runs the function at most once per interval, however many events arrive, so it keeps firing during continuous activity.

function throttle(fn, wait) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= wait) { last = now; fn(...args); }
};
}

When to use which:

DebounceThrottle
Search-as-you-type API callsScroll position tracking, infinite scroll checks
Auto-saving a draft after typing stopsResize or drag handlers that update the UI
Validating a username once the user pausesRate-limiting analytics or button spam

Rule of thumb: debounce when you only care about the final value; throttle when you need regular updates while the action continues.

Note: For visual updates tied to scrolling, requestAnimationFrame is often better than a fixed throttle because it syncs to the display's refresh rate. And for visibility checks, IntersectionObserver avoids scroll handlers entirely. Remember to cancel pending timers when a component unmounts.

49. What are Web Components, and how do custom elements and the shadow DOM work?

Web Components are a set of browser standards for building reusable, encapsulated elements that work in any framework — or none. They are built from three pieces:

  • Custom elements: define a new tag backed by a class that extends HTMLElement. Names must contain a hyphen, such as <user-avatar>.
  • Shadow DOM: attach a private DOM tree whose styles and markup are isolated from the rest of the page.
  • Templates and slots: <template> holds inert markup, and <slot> lets consumers project their own content into the component.
class UserBadge extends HTMLElement {
static observedAttributes = ['name'];
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML =
'<style>span{font-weight:600}</style><span></span><slot></slot>';
}
attributeChangedCallback(n, oldV, v) {
this.shadowRoot.querySelector('span').textContent = v;
}
}
customElements.define('user-badge', UserBadge);

Lifecycle callbacks: connectedCallback (added to the DOM), disconnectedCallback (removed — clean up listeners), attributeChangedCallback and adoptedCallback.

Shadow DOM encapsulation: outside CSS does not leak in and inside CSS does not leak out. Inherited properties and CSS custom properties do pass through, which is how you theme them, and ::part() exposes chosen internals for styling.

Where they fit: design systems shared across React, Angular and Vue teams, embeddable widgets and micro-frontends. Libraries like Lit reduce the boilerplate.

Note: Shadow DOM complicates forms and accessibility: labels cannot point across the shadow boundary with a for attribute, and form participation needs ElementInternals. Server rendering needs declarative shadow DOM.

50. How do stacking contexts work in CSS, and why does a high z-index sometimes have no effect?

z-index controls stacking order along the z-axis, but only within the same stacking context. A stacking context is a self-contained layer: its children are stacked among themselves, and then the whole group is placed as a single unit within its parent context.

Properties that create a new stacking context include:

  • A positioned element (relative, absolute) with a z-index other than auto; fixed and sticky always create one.
  • opacity less than 1.
  • transform, filter, perspective, clip-path or mask with any value other than none.
  • A flex or grid child with a z-index set.
  • isolation: isolate, will-change for these properties, mix-blend-mode and contain: paint.

Why z-index: 9999 fails: if a modal sits inside a card that has transform: translateY(0) and z-index: 1, the modal is trapped in the card's context. It can never appear above a sibling card with z-index: 2, whatever its own value.

.card   { transform: translateY(0); z-index: 1; position: relative; }
.modal { position: fixed; z-index: 9999; } /* still under .card-2 */
.card-2 { position: relative; z-index: 2; }

Fixes:

  • Render overlays at the end of <body> — a React portal or Angular CDK overlay.
  • Use the native <dialog> with showModal() or the Popover API; both use the browser's top layer, above every stacking context.
  • Define a small z-index scale as custom properties instead of arbitrary numbers.
  • Use isolation: isolate to contain a component's internal z-indexes intentionally.

Note: Chrome DevTools' Layers panel and the 3D view help you see stacking contexts when debugging a stubborn overlay.

Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up as