Frontend Interview Q&A — DOM, React, Next.js, Hooks, Performance & Zustand

Table Of Content
- DOM — What and Why
- Event Bubbling and Event Capturing
- undefinedOne-line mental model: React listens once (at the root) and uses undefinedbubbling to know what you clicked.
- React & Virtual DOM — Quick Refresher
- React.js vs Next.js Routing
- React.js routing (React Router)
- Next.js routing (built-in, file-based)
- App Router example: dynamic and multiple params
- Next.js API routes
- Option A — API routes only (no "use server")
- Option B — Server Actions ("use server")
- Best practice: mixed approach
- Server Components vs Client Components
- SSG, SSR, ISR — when to use what
- Props vs State — Short and Sweet
- Hooks — useState, useEffect, useContext, useRef, useMemo, useCallback
- undefinedInterview tip: undefinedHooks let function components use state and side effects; they replace class lifecycle patterns and make logic reusable via custom hooks.
- useRef vs useState — Concrete Diff
- useEffect vs useMemo — Timing and Purpose
- How to Prevent Re-rendering of a Child Component
- Performance Optimization: useMemo & useCallback
- Lazy Loading — What It Is and How to Use It
- Suspense — Wait & Show Fallback
- Zustand vs useContext — The Practical Difference
- Redux vs Zustand
- JavaScript Q&A
- Tailwind CSS Q&A
- Final Tips & Quick Cheatsheet
- TL;DR (One-Line Answers for Interviews)
Frontend Interview Q&A — DOM, React, Next.js, Hooks, Performance & Zustand
This post collects compact, interview-ready explanations and examples for common frontend topics: DOM, React & Virtual DOM, Next.js routing & API routes, props vs state, important hooks (useState, useEffect, useContext, useRef, useMemo, useCallback), lazy loading & Suspense, and a quick Zustand vs useContext comparison. Use it as a cheat-sheet or study guide—copy code examples into a sandbox and run them to internalize the behavior.
DOM — What and Why
DOM (Document Object Model) is the browser's tree-like representation of an HTML document. It's the bridge between HTML and JavaScript: the browser parses HTML → builds a DOM tree → each element becomes an object/node that JS can read and manipulate.
Example HTML:
<html>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>DOM Tree:
Document
└── html
└── body
├── h1
└── p
Why it matters: with the DOM you can:
- Change text (
element.innerText) - Change styles (
element.style.color) - Add/remove elements
- Handle events (
click,submit, etc.)
Interview tip: One-liner: DOM is the programmatic representation of the page that lets JavaScript manipulate UI elements.
Event Bubbling and Event Capturing
In React, you can see bubbling vs capturing clearly with onClick and onClickCapture.
Example setup (same DOM, React style)
function App() {
return (
<div
onClick={() => console.log("Parent: Bubbling")}
onClickCapture={() => console.log("Parent: Capturing")}
style={{ padding: "40px", border: "2px solid blue" }}
>
<button
onClick={() => console.log("Child: Bubbling")}
onClickCapture={() => console.log("Child: Capturing")}
>
Click Me
</button>
</div>
);
}
export default App;What happens when you click the button?
Console output (order matters):
Parent: Capturing
Child: Capturing
Child: Bubbling
Parent: Bubbling
Why this order?
React follows the same DOM event phases:
-
Capturing phase (top → down)
Order:Parent → Child
onClickCapturehandlers run in this phase. -
Target phase
The child receives the click. -
Bubbling phase (bottom → up)
Order:Child → Parent
onClickhandlers run in this phase.
Important React rules to remember
-
Bubbling is default
<div onClick={handler} /> -
Capturing must be explicit
<div onClickCapture={handler} />
Stop bubbling in React
<div
onClickCapture={() => console.log("Parent: Capturing")}
onClick={() => console.log("Parent: Bubbling")}
>
<button
onClick={(e) => {
e.stopPropagation();
console.log("Child only");
}}
>
Click Me
</button>
</div>What happens:
-
Capturing phase
- Parent (
onClickCapture) ✅ runs - Child (
onClickCapture) ❌ not defined (nothing runs)
- Parent (
-
Target phase
- Child (
onClick) ✅ runs →e.stopPropagation()is called here
- Child (
-
Bubbling phase
- Parent (
onClick) ❌ stopped (never runs)
- Parent (
Console output: Parent: Capturing then Child only.
Real-world React example: Modal close bug 🚨
Overlay = full-screen background layer. Modal = the box/dialog inside that layer.
Buggy version:
<div className="overlay" onClick={closeModal}>
<div className="modal">Modal Content</div>
</div>Clicking inside the modal closes it (because the click bubbles up to overlay).
Fixed version:
<div className="overlay" onClick={closeModal}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
Modal Content
</div>
</div>Now:
- Click outside → closes
- Click inside → stays open
Event delegation in React
function List() {
const handleClick = (e) => {
if (e.target.tagName === "LI") {
console.log(e.target.textContent);
}
};
return (
<ul onClick={handleClick}>
<li>Apple</li>
<li>Banana</li>
<li>Mango</li>
</ul>
);
}- Click on Apple → console:
Apple - Click on Banana → console:
Banana - Click on Mango → console:
Mango
One handler on the parent; works even when items are dynamic.
One-line mental model: React listens once (at the root) and uses bubbling to know what you clicked.
React & Virtual DOM — Quick Refresher
React is a UI library built on a component model. It uses a virtual DOM (an in-memory representation) to compute minimal updates to the real DOM—that's why React apps are performant and maintainable.
- Virtual DOM = in-memory representation of the UI (a lightweight copy of the structure the real DOM has / should have).
- Goal = avoid updating the whole real DOM on every small change (e.g. a title). Only update what actually changed.
- Flow = On a change (e.g. state update), React builds a new virtual tree and diffs it with the previous one, figures out exactly which nodes in the real DOM need to change, and applies only those updates to the real DOM.
- Result = Fewer, targeted real DOM updates → fewer reflows/repaints → faster and smoother UI.
The "change" doesn't happen to the old virtual DOM in place. React creates a new virtual tree for the new state, compares old and new (diff), and from that gets the minimal list of real DOM updates. So: new virtual tree → diff → patch only the affected parts of the real DOM.
Why isn't updating the virtual DOM slower? Updating the virtual DOM (build new tree + diff) is cheap (just JavaScript objects in memory). Updating the real DOM is expensive (style recalc, layout, paint). We do a little fast work so we can do fewer expensive real DOM updates → faster overall.
One-liner: React renders components to the virtual DOM, diffs changes, and applies only the necessary updates to the real DOM.
React.js vs Next.js Routing
React itself has no built-in routing. You typically use React Router. Next.js has routing built in and is file-based.
React.js routing (React Router)
You manually declare routes in code:
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import User from "./pages/User";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<User />} />
</Routes>
</BrowserRouter>
);
}Navigation:
import { Link } from "react-router-dom";
<Link to="/about">About</Link>;Next.js routing (built-in, file-based)
Routes come from your folder structure. Two systems:
A) App Router (modern) — app/
app/
page.jsx → /
about/page.jsx → /about
users/[id]/page.jsx → /users/:id
Dynamic route page:
export default function UserPage({ params }) {
return <div>User ID: {params.id}</div>;
}Navigation:
import Link from "next/link";
<Link href="/about">About</Link>;B) Pages Router (older) — pages/
Same idea: folder structure under pages/ defines routes (e.g. pages/blog/[slug].js → /blog/:slug).
App Router example: dynamic and multiple params
1. Folder structure (this creates the route):
app/
users/
[id]/
page.jsx
Maps to URLs like /users/123, /users/abc.
2. page.jsx:
export default function UserPage({ params }) {
return (
<div>
<h1>User Profile</h1>
<p>User ID: {params.id}</p>
</div>
);
}Multiple params:
app/
users/
[id]/
posts/
[postId]/
page.jsx
URL: /users/42/posts/7 → params = { id: "42", postId: "7" }
Next.js API routes
Next.js allows backend endpoints inside the /api folder (Pages Router: pages/api/*; App Router: app/api/*).
Option A — API routes only (no "use server")
You can avoid "use server" by doing all server work through the API folder.
How it works: All server logic lives in app/api/*. Client components call APIs with fetch. Very familiar (REST style).
Example:
// app/api/posts/route.js
export async function POST(req) {
const body = await req.json();
// DB write
return Response.json({ ok: true });
}"use client";
async function createPost() {
await fetch("/api/posts", {
method: "POST",
body: JSON.stringify({ title: "Hello" }),
});
}Pros: Clear separation; works for mobile / external clients; easy to debug; familiar backend pattern.
Option B — Server Actions ("use server")
How it works: Server functions live next to UI. Called directly from components. No REST API needed.
Example:
// app/actions.js
"use server";
export async function createPost(formData) {
const title = formData.get("title");
// DB write
}<form action={createPost}>
<input name="title" />
<button>Create</button>
</form>Pros: Less code; no fetch / API layer.
Best practice: mixed approach
In real-world Next.js apps, use both:
| Use | Best for |
|---|---|
| Server Actions ("use server") | Internal UI actions: form submissions, create/update/delete from UI, button-triggered actions, authenticated user flows |
API Routes (app/api/*) | External or public access: mobile apps (Flutter, iOS, Android), third-party webhooks (Stripe, PayPal), public APIs, OAuth callbacks |
Server Components vs Client Components
- If you do NOT add
"use client"→ it's a Server Component by default. Use for: fetching data securely (DB, API keys). - Add
"use client"at the top when you need: React hooks (useState,useEffect,useRef), event handlers (onClick,onChange), or browser APIs (window,document,localStorage).
SSG, SSR, ISR — when to use what
SSG (Static Site Generation)
Use when: content is the same for everyone, changes rarely, you want fastest load + best SEO.
Examples: marketing pages, docs, blog posts, help pages; public product pages where prices don't change often.
SSR (Server-Side Rendering) / dynamic
Use when: data must be fresh on every request, or depends on request info (cookies, headers, auth); SEO matters and content is per-user or per-request.
Examples: user dashboard (role-based), admin pages, "My orders", "My profile", location/cookie-based content.
ISR (Incremental Static Regeneration)
Use when: content is public and SEO important; data changes sometimes; you want near-SSG speed but periodic freshness.
Examples: blog list page updated hourly; product catalog updated every few minutes; news category pages (not live).
Interview tip: Know when to use SSG/SSR/ISR and when to prefer client-side fetching.
Props vs State — Short and Sweet
Props — read-only data passed from parent to child.
function Parent() {
return <Greeting name="Dipto" />;
}
function Greeting(props) {
return <h1>Hello, {props.name} 👋</h1>;
}- Props are immutable inside the child.
State — component-owned, mutable data (use useState in functional components).
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</>
);
}- Use state when UI needs to update in response to changes.
Interview tip: "If the UI should update, use state. If it's configuration passed from a parent, use props."
Hooks — useState, useEffect, useContext, useRef, useMemo, useCallback
Why are hooks used in React / Next.js?
Short answer: Hooks let you use state, side effects, and React features inside function components without class components or lifecycle methods. They keep logic reusable (custom hooks), avoid wrapper hell, and make code easier to follow.
- Before hooks: State and lifecycle lived in class components (
this.state,componentDidMount, etc.); sharing logic meant HOCs or render props. - With hooks: Function components can have state (
useState), effects (useEffect), refs (useRef), context (useContext), and more. You can extract shared logic into custom hooks (e.g.useAuth,useFetch).
Interview tip: Hooks let function components use state and side effects; they replace class lifecycle patterns and make logic reusable via custom hooks.
useEffect — Side Effects
- Runs after render. Great for API calls, subscriptions, DOM work, logging.
// every render
useEffect(() => {
console.log("Runs every render");
});
// once (on mount)
useEffect(() => {
console.log("Runs once");
}, []);
// run when count changes
useEffect(() => {
console.log("Runs when count changes");
}, [count]);Render = create Virtual DOM
Mount = first insertion into Real DOM
Gotcha: updating state inside an effect that depends on that state can cause infinite loops:
useEffect(() => {
setCount(count + 1); // 🚨 infinite loop if dependency includes count
}, [count]);How to handle API calls (React)
Short answer: Put the fetch in an async function with try/catch; check response.ok and throw on error. In React, call it from useEffect and store the result in state.
async function fetchData() {
try {
const response = await fetch("https://example.com/posts");
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error("Error fetching data:", error.message);
}
}
// Call the function (in React, call inside useEffect and set state with the result)
fetchData();In React: Call fetchData() inside useEffect, then setData(result) (and optionally setLoading / setError) so you can render loading, error, and list states.
Interview tip: Use try/catch, check response.ok, parse with .json(). In React, run the fetch in useEffect and store the result in state.
useContext — Avoid Prop Drilling
- Consume shared/global data in any component inside the provider. Context applies only to components wrapped by the Provider — not globally to every component.
import { createContext, useContext } from "react";
const ThemeContext = createContext();
function App() {
return (
<ThemeContext.Provider value="dark">
<Navbar />
</ThemeContext.Provider>
);
}
function Navbar() {
const theme = useContext(ThemeContext);
return <p>Theme: {theme}</p>;
}Tree: ThemeContext.Provider → Navbar ✅ has access. So useContext(ThemeContext) in Navbar → "dark".
What if a component is NOT wrapped?
function App() {
return (
<>
<ThemeContext.Provider value="dark">
<Navbar />
</ThemeContext.Provider>
<Footer />
</>
);
}ThemeContext.Provider→Navbar✅ gets"dark".Footer❌ is not inside the provider — it cannot read the context value. It gets the default value (if you passed one tocreateContext).
Example with default value:
const ThemeContext = createContext("light");
function Footer() {
const theme = useContext(ThemeContext);
return <p>{theme}</p>;
}If Footer is outside the Provider, output: light.
Can multiple components use the same context? Yes — as long as they are inside the Provider:
<ThemeContext.Provider value="dark">
<Navbar />
<Sidebar />
<Main />
</ThemeContext.Provider>Can we nest Providers? Yes — and the nearest Provider wins:
<ThemeContext.Provider value="dark">
<Navbar />
<ThemeContext.Provider value="light">
<Sidebar />
</ThemeContext.Provider>
</ThemeContext.Provider>Navbar→"dark"(outer provider)Sidebar→"light"(inner provider; nearest wins)
useState — Component state
- Holds mutable state in function components. Updates trigger a re-render.
- Use when the UI must change in response to user input or other events.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</>
);
}useRef — Persist Values, Access DOM
useRef()returns{ current }. Updatingref.currentdoes not trigger re-renders.- Use for DOM nodes, timers, storing previous values.
import { useRef } from "react";
function CounterRef() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log("Count:", countRef.current);
};
return (
<>
<p>Open console to see count (UI won't update)</p>
<button onClick={handleClick}>Increase</button>
</>
);
}Rule: If changing the value should re-render UI → use useState. If you need persistence without re-render → use useRef.
Real DOM vs useRef for elements
In plain JavaScript you typically do:
<button id="child">Click me</button>const btn = document.getElementById("child");
btn.style.background = "red";➡️ You ask the browser: “Hey DOM, give me that button.”
This is direct Real DOM access.
In React, the DOM is managed through the virtual DOM, and manual DOM queries can cause unexpected UI bugs. Instead of “finding” elements by id, you keep a reference with useRef:
import { useRef } from "react";
function App() {
const buttonRef = useRef(null);
const handleClick = () => {
buttonRef.current.style.background = "red";
};
return (
<button ref={buttonRef} onClick={handleClick}>
Click Me
</button>
);
}Here:
- No
document.getElementById - No string IDs
- React hands you the exact DOM node via
ref
useMemo — Memoize Values (During Render)
- Use for expensive computations. Runs during render but only if dependencies changed.
const double = useMemo(() => {
console.log("Calculating double...");
return count * 2;
}, [count]);useMemo returns a value that you can use immediately in JSX.
useCallback — Memoize Functions
- Use for stable function references when passing callbacks to memoized children. It only helps when the child is wrapped in
React.memo.
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);useCallback(fn, deps) is conceptually similar to useMemo(() => fn, deps).
useMemo vs useCallback quick:
useMemomemoizes values; returns a value.useCallbackmemoizes functions; returns a function.
Why useCallback helps only with React.memo
A child re-renders when (1) its parent re-renders and (2) React thinks its props may have changed.
Case 1 — Child is NOT memoized:
function Child({ onClick }) {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
}Parent re-renders → Child re-renders always. It doesn't matter whether the function reference is the same or different. useCallback is useless here.
Case 2 — Child IS memoized:
const Child = React.memo(function Child({ onClick }) {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
});Now React does shallow prop comparison: same onClick reference → skip re-render ✅; new onClick reference → re-render ❌. useCallback now matters.
Visual comparison:
- ❌ Without
React.memo: Parent render → Child render; Parent render → Child render; Parent render → Child render. - ✅ With
React.memo+useCallback: Parent render → Child render; Parent render → Child skipped; Parent render → Child skipped.
Important nuance: useCallback never stops a re-render by itself. It only (1) preserves function identity and (2) allows React.memo to do its job. React.memo decides whether to re-render; useCallback makes that decision possible by keeping the same function reference.
What useCallback actually does:
const handleClick = useCallback(fn, deps);Meaning: "Store this function and return the same function object on the next render as long as deps don't change." It does not prevent renders, compare props, or optimize the UI by itself — it only guarantees stable identity.
Without useCallback: Every render creates a new function: () => setCount(c => c + 1) → new object in memory → fn1 !== fn2 !== fn3, so React.memo sees a new prop and re-renders the child.
useRef vs useState — Concrete Diff
useStatechanges trigger re-renders and update UI.useRefchanges do not trigger re-renders; good for storing mutable data or DOM references.
Example (counting clicks):
useStateversion updates UI count.useRefversion updatescountRef.currentand logs to console—UI doesn't change.
Interview tip: Explain intent: use state for reactive UI, use ref for persistent mutable storage or DOM access.
useEffect vs useMemo — Timing and Purpose
useMemoruns during render (computes values used in render), but only when dependencies change.useEffectruns after render (for side effects).
Timeline:
Render starts
↓
useMemo executes if needed
↓
JSX renders using memos
↓
DOM updates
↓
useEffect runs
Interview line: useMemo prepares values before painting the screen; useEffect runs after the screen is painted to perform side effects.
How to Prevent Re-rendering of a Child Component
Short answer: (1) Wrap the child in React.memo() so it only re-renders when its props change. (2) Pass stable props — use useCallback for functions and useMemo for objects/arrays so the parent doesn’t pass new references every render. (3) Avoid creating objects or functions inline in JSX (e.g. onClick={() => ...} or style={{ color: 'red' }}), or the child will always see “new” props and re-render even with memo.
// Child only re-renders when onClick or count change
const Child = React.memo(({ onClick, count }) => (
<button onClick={onClick}>Count: {count}</button>
));
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount((c) => c + 1), []);
return <Child onClick={handleClick} count={count} />;
}Interview tip: Use React.memo on the child and stable props from the parent (useCallback for functions, useMemo for objects); avoid inline objects/functions in JSX.
Performance Optimization: useMemo & useCallback
Both are performance hooks:
useMemoprevents expensive recalculation.useCallbackprevents re-creation of function references which would cause prop-changes and re-renders in memoized children.
Example with a memoized child:
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click Me</button>;
});In parent:
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);useCallback keeps handleClick stable so Child doesn't re-render unnecessarily.
Lazy Loading — What It Is and How to Use It
Lazy loading delays loading resources (components, images, routes) until they're needed.
Images (native):
<img src="large-image.jpg" loading="lazy" alt="example" />Browser fetches the image when it enters the viewport.
Components (React):
import React, { Suspense } from "react";
const LazyComponent = React.lazy(() => import("./LazyComponent"));
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>;Next.js dynamic import:
import dynamic from "next/dynamic";
const Heavy = dynamic(() => import("./Heavy"), {
ssr: false,
loading: () => <p>Loading...</p>,
});Suspense — Wait & Show Fallback
Suspense lets React "wait" for lazy-loaded components and show a fallback (spinner, skeleton) while loading:
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>fallbackis shown whileLazyComponentdownloads.- In Next.js, prefer
dynamic()for SSR control;Suspensefor data fetching is still evolving.
Interview tip: Explain Suspense as "delay rendering until resource is ready and show a fallback UI".
Zustand vs useContext — The Practical Difference
Zustand is a lightweight global store using hooks. It's not the same as useContext.
Why Zustand is different/better than plain useContext for larger state:
- Components subscribe to specific slices of the store → only they re-render when their slice changes.
useContextre-renders all consumers when the Provider value changes.- Zustand has minimal boilerplate, supports async actions, and offers fine-grained updates.
The problem with a single Context
const AppContext = createContext();
function Provider({ children }) {
const [theme, setTheme] = useState("dark");
const [count, setCount] = useState(0);
return (
<AppContext.Provider value={{ theme, count, setCount }}>
{children}
</AppContext.Provider>
);
}
function ThemeDisplay() {
const { theme } = useContext(AppContext);
console.log("ThemeDisplay rendered");
return <p>{theme}</p>;
}
function CounterDisplay() {
const { count } = useContext(AppContext);
console.log("CounterDisplay rendered");
return <p>{count}</p>;
}When count changes (e.g. setCount(c => c + 1)):
Console output: ThemeDisplay rendered ❌ (unnecessary) and CounterDisplay rendered.
Why? The Provider value object is new on every update, so React notifies all consumers. ThemeDisplay only reads theme but still re-renders.
How to “fix” Context (and why people split contexts)
✅ Split contexts: ThemeContext and CounterContext separately. Then theme updates → only theme consumers re-render; count updates → only counter consumers re-render.
Downside: many Providers and Provider nesting (“Provider hell”).
How Zustand handles this
Zustand does not use React Context for state. Components subscribe to slices via selectors; Zustand compares the selected values and only re-renders if that slice changed.
Example:
import { create } from "zustand";
const useStore = create((set) => ({
theme: "dark",
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
setTheme: (theme) => set(() => ({ theme })),
toggleTheme: () =>
set((s) => ({
theme: s.theme === "dark" ? "light" : "dark",
})),
}));Using it in components:
Theme component:
function ThemeDisplay() {
const theme = useStore((s) => s.theme);
const toggleTheme = useStore((s) => s.toggleTheme);
console.log("ThemeDisplay rendered");
return (
<>
<p>Theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</>
);
}Counter component:
function CounterDisplay() {
const count = useStore((s) => s.count);
const inc = useStore((s) => s.inc);
console.log("CounterDisplay rendered");
return (
<>
<p>Count: {count}</p>
<button onClick={inc}>+</button>
</>
);
}What happens when you click buttons?
- Click "+" →
CounterDisplayre-renders. ThemeDisplay ❌ does not re-render. - Click "Toggle Theme" →
ThemeDisplayre-renders. CounterDisplay ❌ does not re-render.
Why this works (core idea): Zustand uses selectors — e.g. useStore((s) => s.theme) and useStore((s) => s.count). Each component subscribes only to what it needs and re-renders only when that slice changes.
Interview line: Zustand creates a global hook-based store with fine-grained subscriptions; useContext broadcasts changes to all consumers.
Same idea with Redux (Redux Toolkit)
Redux with useSelector also gives fine-grained subscriptions: a component re-renders only when the slice it selects changes.
store.js:
// store.js
import { configureStore, createSlice } from "@reduxjs/toolkit";
const appSlice = createSlice({
name: "app",
initialState: {
theme: "dark",
count: 0,
},
reducers: {
inc: (state) => {
state.count += 1;
},
toggleTheme: (state) => {
state.theme = state.theme === "dark" ? "light" : "dark";
},
},
});
export const { inc, toggleTheme } = appSlice.actions;
export const store = configureStore({
reducer: {
app: appSlice.reducer,
},
});ThemeDisplay.jsx (subscribes only to theme):
import { useDispatch, useSelector } from "react-redux";
import { toggleTheme } from "./store";
export default function ThemeDisplay() {
const theme = useSelector((s) => s.app.theme);
const dispatch = useDispatch();
console.log("ThemeDisplay rendered");
return (
<div>
<p>Theme: {theme}</p>
<button onClick={() => dispatch(toggleTheme())}>Toggle Theme</button>
</div>
);
}CounterDisplay.jsx (subscribes only to count):
import { useDispatch, useSelector } from "react-redux";
import { inc } from "./store";
export default function CounterDisplay() {
const count = useSelector((s) => s.app.count);
const dispatch = useDispatch();
console.log("CounterDisplay rendered");
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(inc())}>+</button>
</div>
);
}Same behavior: click "+" → only CounterDisplay re-renders; click "Toggle Theme" → only ThemeDisplay re-renders. useSelector selects a slice; React-Redux re-renders the component only when that selection changes.
Redux vs Zustand
Short answer: Both are global state libraries. Redux is opinionated (actions, reducers, single store, often with Redux Toolkit), has a big ecosystem (middleware, DevTools, time-travel), and suits very large apps or teams that want strict structure. Zustand is minimal: no actions/reducers by default, store is a hook, less boilerplate, and is often enough for small–medium apps.
Use Redux (Redux Toolkit) instead of Zustand when: you need RTK Query (or strong middleware/DevTools). Zustand has no built-in data-fetching layer.
What problem does RTK Query solve?
Normally, when you fetch data you manage everything yourself:
useEffect(() => {
setLoading(true);
fetch("/posts")
.then((r) => r.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((err) => setError(err));
}, []);You manually handle: fetching, loading, error, caching, re-fetching, and syncing across components.
RTK Query automates all of this. You define an API slice; components use a hook like useGetPostsQuery(). RTK Query handles request, cache, loading, and error state.
1. Create an API slice (RTK Query):
// services/postsApi.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com/",
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => "posts",
}),
getPostById: builder.query({
query: (id) => `posts/${id}`,
}),
}),
});
export const { useGetPostsQuery, useGetPostByIdQuery } = postsApi;2. Configure the Redux store:
// store.js
import { configureStore } from "@reduxjs/toolkit";
import { postsApi } from "./services/postsApi";
export const store = configureStore({
reducer: {
//where the RTK Query cache lives in Redux state
postsApi: postsApi.reducer, // ← RTK Query cache + metadata live here
},
middleware: (getDefaultMiddleware) => [
//Intercepts actions and FETCHES DATA FROM THE SERVER, Middleware = network + cache logic engine
...getDefaultMiddleware(),
postsApi.middleware,
],
});3. Use RTK Query in a component (no fetch, no useEffect):
// Posts.jsx
import { useGetPostsQuery } from "./services/postsApi";
export default function Posts() {
const { data, error, isLoading } = useGetPostsQuery();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return (
<ul>
{data.slice(0, 5).map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Exact flow (step-by-step):
- Component calls the hook →
useGetPostsQuery(). - RTK Query dispatches an action →
getPosts.initiate. - Middleware sees the action → checks cache in
state.postsApi. If data exists → serve from cache. If not → fetch from the server. - Server responds → middleware dispatches a success action.
- Reducer stores the result →
state.postsApi.queries["getPosts(...)"].data.
Example — two components using the same query:
function A() {
useGetPostsQuery();
}
function B() {
useGetPostsQuery();
}RTK Query makes one HTTP request, stores the result in the Redux store, and both components read from the same cache → no duplicate network calls.
Interview tip: Redux = structured, predictable, more boilerplate; Zustand = minimal, hook-based, less boilerplate. Pick Redux for scale and process (or when you need RTK Query); Zustand for simplicity and speed.
JavaScript Q&A
How to remove duplicate values from an array?
Short answer: Use Set (for primitives) or filter + indexOf (for order + primitives). For objects, use a Map or reduce with a key.
Primitives (shortest):
const arr = [1, 2, 2, 3, 1, 4];
const unique = [...new Set(arr)]; // [1, 2, 3, 4]Interview tip: One line: [...new Set(arr)] — Set keeps only unique values.
== vs ===?
Short answer:
==(loose): compares values after type coercion (e.g."5" == 5→true).===(strict): compares value and type — no coercion (e.g."5" === 5→false).
Rule of thumb: Prefer === to avoid surprises; use == only when you explicitly want coercion (e.g. treat null and undefined the same: x == null).
Interview tip: === is strict (type + value); == coerces types first.
null vs undefined?
Short answer:
undefined= “no value” — uninitialized variable, missing property, or function with no return. Type is"undefined".null= “intentionally empty” — you assign it when something is meant to have no value. Type is"object"(historical quirk).
let a; // a is undefined
const b = null; // b is explicitly null
const o = {}; // o.xyz is undefined (missing property)Use x == null to check for both in one go (x === null || x === undefined).
Interview tip: undefined = never set / missing; null = explicitly empty. Both are falsy; typeof null is "object".
What is debounce and how to implement it?
Short answer: Debounce waits until the user stops triggering an event for a certain time, then runs the function once. It prevents multiple rapid triggers from causing multiple executions.
Timeline (e.g. 300ms delay):
- t=0 ms — keypress → debounce starts timer
- t=100 ms — keypress → timer reset
- t=200 ms — keypress → timer reset
- t=300 ms — keypress → timer reset
- t=600 ms — no typing for 300 ms → function runs once:
Searching for: finalText
Not this (without debounce): Searching for: a → Searching for: ab → Searching for: abc → … on every keystroke.
What debounce avoids: unnecessary API calls, repeated fetches, wasted CPU work, and spamming the backend.
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
// Example: handle input without firing on every keypress
const handleInput = debounce((value) => {
console.log("Searching for:", value);
}, 300);Interview tip: Debounce = wait X ms after the last event before running the function.
String palindrome check
Short answer: Compare the string with its reverse. Normalize first (lowercase, remove non-alphanumeric) if you want to ignore case and punctuation.
function isPalindrome(s) {
const cleaned = s.toLowerCase().replace(/[^a-z0-9]/g, "");
return cleaned === cleaned.split("").reverse().join("");
}Example: isPalindrome("A man, a plan, a canal: Panama")
Step-by-step:
- Convert to lowercase:
"a man, a plan, a canal: panama" - Remove non-alphanumeric characters:
"amanaplanacanalpanama" - Reverse it:
"amanaplanacanalpanama" - Compare: cleaned === reversed →
true
✅ Output: true
Interview tip: Palindrome = same forwards and backwards; reverse and compare.
How to deep clone an object
Short answer: Use structuredClone(obj) (built-in, handles most types). For simple JSON-safe data, JSON.parse(JSON.stringify(obj)) works but fails for Date, undefined, functions, circular refs. For full control, write a recursive cloner or use a library (e.g. lodash cloneDeep).
// Built-in (preferred when supported)
const clone = structuredClone(obj);
// JSON-safe only (no Date, undefined, functions, circular refs)
const clone = JSON.parse(JSON.stringify(obj));Example object (no circular refs):
const obj = {
id: 1,
name: "User",
createdAt: new Date("2026-02-10"),
scores: new Map([
["math", 90],
["cs", 95],
]),
tags: new Set(["react", "redux"]),
meta: undefined,
};✅ Using structuredClone:
const clone = structuredClone(obj);
// type checks
console.log(clone.createdAt instanceof Date); // true
console.log(clone.scores instanceof Map); // true
console.log(clone.tags instanceof Set); // true
console.log("meta" in clone); // true
// safe mutation
clone.scores.set("ai", 100);
clone.tags.add("zustand");
// original untouched
console.log(obj.scores.has("ai")); // false
console.log(obj.tags.has("zustand")); // falseResult: Date stays Date, Map stays Map, Set stays Set, undefined is preserved, and the clone is fully independent.
❌ Using JSON clone (major breakage):
const clone = JSON.parse(JSON.stringify(obj));What you actually get:
console.log(clone.createdAt); // "2026-02-10T00:00:00.000Z" (string)
console.log(clone.scores); // {} ❌ Map lost
console.log(clone.tags); // {} ❌ Set lost
console.log("meta" in clone); // false ❌ removedEven if you mutate clone.scores.ai = 100, it's meaningless — the original Map is gone. Data is silently corrupted, not just slightly different.
Interview tip: Deep clone = copy so nested changes don’t affect the original; structuredClone or JSON.parse(JSON.stringify(obj)) for plain data.
Tailwind CSS Q&A
How to center a div horizontally and vertically with Tailwind?
Short answer: Use a flex container with items-center (vertical) and justify-center (horizontal), plus a height (h-screen / min-h-screen) so there’s space to center in.
export default function CenteredBox() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="bg-slate-800 text-white p-8 rounded-lg">
Centered content
</div>
</div>
);
}Responsive note: Tailwind makes pages responsive with breakpoint prefixes like sm:, md:, lg: etc. Example: className="text-base md:text-lg lg:text-xl" changes font size at different screen widths.
How to make a div’s background color change gradually on hover?
Short answer: Use hover:bg-* for the hover color and add transition-colors (and duration-*) so the color change is animated instead of instant.
function Button() {
return (
<button className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded transition-colors duration-300">
Hover me
</button>
);
}You can combine with gradients if you want more complex effects, but the key pieces for “gradually change color” are hover:bg-… + transition-colors + duration-….
relative, sticky, fixed, absolute — what's the difference?
Short answer: All are CSS position values (Tailwind: relative, sticky, fixed, absolute). They change how the element is positioned and what "containing block" it uses.
| Position | Behavior |
|---|---|
relative | Element stays in normal flow; offset by top/right/bottom/left from its original spot. Doesn't affect other elements' layout. |
absolute | Removed from flow. Positioned relative to the nearest positioned ancestor (position not static) or the viewport. Use with top/right/bottom/left. |
fixed | Removed from flow. Positioned relative to the viewport (stays in place on scroll). Use for sticky headers, modals, floating buttons. |
sticky | Hybrid: acts like relative until a scroll threshold (e.g. top: 0), then "sticks" like fixed within its parent. Good for sticky nav bars. |
Tailwind: relative, absolute, fixed, sticky — then use top-*, left-*, etc. for offsets.
Interview tip: relative = offset from own position, stays in flow; absolute = out of flow, relative to positioned ancestor; fixed = out of flow, relative to viewport; sticky = in flow until scroll, then sticks.
Final Tips & Quick Cheatsheet
- DOM = bridge between HTML and JS. Know tree structure and common DOM manipulations.
- React: Virtual DOM + components. Know lifecycle + hooks basics.
- Props are read-only; state is reactive and local.
useEffect= after-render side effects. Dependency array controls runs.useRef= persistent mutable value & DOM access without re-render.useMemo= memoize values (during render),useCallback= memoize functions.- Lazy-load large components & images. Use
Suspensefor fallbacks. - For app-level state, know tradeoffs:
useContext(simple) vs Zustand/Redux (scalable). - Practice by building small examples and inspecting console logs to see when effects/memos run.
TL;DR (One-Line Answers for Interviews)
- DOM: Programmatic tree of the document used by JS to manipulate UI.
- Event bubbling vs capturing: Bubbling = event goes up (target → root); capturing = event goes down (root → target). Default is bubbling; use
truefor capture. - React: UI library with component model and virtual DOM.
- File-based routing (Next.js): Routes come from
/pagesor/appfolder structure. - API routes (Next.js): Server endpoints in
/api. - Props vs State: Props = parent → child read-only; State = component-owned, updates UI.
- Why hooks: Let function components use state and side effects; replace class lifecycles; enable reusable logic via custom hooks.
useEffect: run side effects after render.- API calls (React):
useStatefor data/loading/error,useEffectto fetch; useAbortControllerin cleanup to cancel on unmount. useMemo: compute/memoize value during render (only if deps change).useCallback: memoize function reference.useRef: mutable storage / DOM reference, no re-render.- Prevent child re-renders:
React.memoon the child + stable props (useCallbackfor functions,useMemofor objects); avoid inline objects/functions in JSX. - Lazy loading: load only when needed (images/components).
Suspense: show fallback while waiting for lazy resource.- Zustand vs
useContext: Zustand = fine-grained hook store;useContext= broadcast values to consumers. - Redux vs Zustand: Redux = structured, more boilerplate, great for large apps; Zustand = minimal, hook-based, less boilerplate.
- Remove array duplicates:
[...new Set(arr)]for primitives. ==vs===:==== strict (type + value);=== loose, coerces types.nullvsundefined:undefined= unset/missing;null= intentionally empty. Both falsy;typeof nullis"object".- Debounce: wait X ms after the last event before running a function.
- Palindrome check: reverse string and compare.
- Deep clone:
structuredClone(obj)orJSON.parse(JSON.stringify(obj))for JSON-safe data. call/apply/bind: All setthis.call(thisArg, ...args)andapply(thisArg, [args])run now;bind(thisArg, ...)returns a new function to call later.- Center with Tailwind:
min-h-screen flex items-center justify-center. - Hover background transition (Tailwind):
hover:bg-* transition-colors duration-300. relative/absolute/fixed/sticky: relative = offset from self, in flow; absolute = out of flow, vs positioned ancestor; fixed = out of flow, vs viewport; sticky = in flow then sticks.