The Complete Frontend Interview Guide: DOM, React, Next.js & State Management

Table Of Content
- Part 1 — Browser Fundamentals
- Part 2 — React Core Concepts
- Part 3 — Hooks
- Why Hooks?
- useState — Component State
- useEffect — Side Effects
- How to handle API calls
- useRef — Persist Values, Access DOM
- useRef vs useState — Quick Diff
- useMemo — Memoize Values
- useCallback — Memoize Functions
- useMemo vs useCallback
- Why useCallback only helps with React.memo
- useEffect vs useMemo — Timing
- useContext — Avoid Prop Drilling
- Components outside the Provider get the default value
- Multiple consumers and nested Providers
- Part 4 — Performance
- Part 5 — Next.js
- React Router vs Next.js Routing
- React Router
- Next.js App Router (modern)
- Next.js Pages Router (older)
- Server Components vs Client Components
- Next.js API Routes
- Option A — API Routes (REST style)
- Option B — Server Actions ("use server")
- Best practice: use both
- Proxy Server & CORS (AxiosError)
- Session Token vs JWT
- How They Work
- When to Use Which
- Security Tips
- Offset vs Cursor Pagination
- Part 6 — State Management
- Part 7 — JavaScript Patterns
- Part 8 — Tailwind CSS Patterns
- Part 9 — Developer Workflow
- Final Tips
- TL;DR — One-Line Interview Answers
The Complete Frontend Interview Guide: DOM, React, Next.js & State Management
This post collects compact, interview-ready explanations and examples for common frontend topics. It's organized to follow a natural learning path — browser fundamentals first, then React core concepts, hooks, performance, state management, and finally JavaScript and Tailwind patterns. Use it as a cheat-sheet or study guide — copy the code examples into a sandbox and run them to internalize the behavior.
Part 1 — Browser Fundamentals
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
Example setup
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>
);
}What happens when you click the button?
Parent: Capturing
Child: Capturing
Child: Bubbling
Parent: Bubbling
Why this order?
React follows the standard DOM event phases:
- Capturing phase (top → down):
onClickCapturehandlers run, firingParent → Child. - Target phase: The child receives the click.
- Bubbling phase (bottom → up):
onClickhandlers run, firingChild → Parent.
Key rules
- Bubbling is default:
<div onClick={handler} /> - Capturing must be explicit:
<div onClickCapture={handler} />
Stopping bubbling
<div
onClickCapture={() => console.log("Parent: Capturing")}
onClick={() => console.log("Parent: Bubbling")}
>
<button
onClick={(e) => {
e.stopPropagation();
console.log("Child only");
}}
>
Click Me
</button>
</div>Console output: Parent: Capturing then Child only. The parent's bubbling handler never runs.
Real-world example: Modal close bug
Overlay = full-screen background. Modal = the dialog inside it.
// ❌ Buggy — clicking inside the modal closes it (click bubbles to overlay)
<div className="overlay" onClick={closeModal}>
<div className="modal">Modal Content</div>
</div>
// ✅ Fixed
<div className="overlay" onClick={closeModal}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
Modal Content
</div>
</div>One-line mental model: React listens once at the root and uses bubbling to know what you clicked.
Part 2 — React Core Concepts
React & Virtual DOM
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.
- Virtual DOM = a lightweight JS object copy of the UI structure.
- Goal = avoid re-painting the whole real DOM on every small change.
- Flow = On a state update, React builds a new virtual tree, diffs it against the previous one, then applies only the changed nodes to the real DOM.
- Result = Fewer, targeted real DOM updates → fewer reflows/repaints → faster UI.
Why isn't building the virtual DOM slow? Diffing JS objects in memory is cheap. Writing to the real DOM (style recalc, layout, paint) is expensive. React does fast virtual work to minimize slow real DOM work.
One-liner: React renders to the virtual DOM, diffs changes, and patches only what changed in the real DOM.
Props vs State
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 (useState in function components).
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</>
);
}Interview tip: If the UI should update, use state. If it's configuration passed from a parent, use props.
Part 3 — Hooks
Why Hooks?
Hooks let you use state, side effects, and React features inside function components — no class components or lifecycle methods needed. 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); sharing logic required HOCs or render props. - With hooks: Function components can have state, effects, refs, context, and more. Shared logic lives in 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.
useState — Component State
Holds mutable state in function components. Updates trigger a re-render. Use it 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>
</>
);
}useEffect — Side Effects
Runs after render. Great for API calls, subscriptions, DOM work, and logging.
// Runs every render
useEffect(() => {
console.log("Runs every render");
});
// Runs once (on mount)
useEffect(() => {
console.log("Runs once");
}, []);
// Runs when count changes
useEffect(() => {
console.log("Runs when count changes");
}, [count]);Note: Render = create Virtual DOM. Mount = first insertion into Real DOM.
Gotcha: Updating state inside an effect that lists that same state as a dependency causes an infinite loop.
// 🚨 Infinite loop
useEffect(() => {
setCount(count + 1);
}, [count]);How to handle API calls
Put the fetch inside an async function with try/catch, check response.ok, then 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();
return data;
} catch (error) {
console.error("Error fetching data:", error.message);
}
}In React: call fetchData() inside useEffect, then setData(result) (plus setLoading / setError) to render loading, error, and list states.
Interview tip: Try/catch, check
response.ok, parse with.json(). Run inuseEffect, store in state.
useRef — Persist Values, Access DOM
useRef() returns { current }. Updating ref.current does not trigger a re-render. Use it for DOM nodes, timers, and storing previous values.
import { useRef } from "react";
function CounterRef() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log("Count:", countRef.current); // UI won't update
};
return (
<>
<p>Open the console to see the count.</p>
<button onClick={handleClick}>Increase</button>
</>
);
}Rule: If the change should re-render the UI →
useState. If you need persistence without re-render →useRef.
useRef vs useState — Quick Diff
useState | useRef | |
|---|---|---|
| Triggers re-render | ✅ Yes | ❌ No |
| Updates UI | ✅ Yes | ❌ No |
| Good for | Reactive UI values | Mutable storage / DOM access |
useMemo — Memoize Values
Runs during render, but only when its dependencies change. Use it for expensive computations whose result you need in the JSX.
const double = useMemo(() => {
console.log("Calculating double...");
return count * 2;
}, [count]);You might think you can do the same thing with useEffect and extra state:
const [double, setDouble] = useState(0);
useEffect(() => {
console.log("Calculating double...");
setDouble(count * 2);
}, [count]);This works, but it introduces an extra render: React first renders with the old double, then runs the effect and triggers a second render. useMemo avoids that extra pass by computing the derived value during render and returning it directly.
useMemo returns a value you can use directly in JSX.
useCallback — Memoize Functions
Keeps a stable function reference across renders. Primarily useful when passing callbacks to memoized children — it only helps when the child is wrapped in React.memo.
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);useMemo vs useCallback
useMemomemoizes values → returns a value.useCallbackmemoizes functions → returns a function.useCallback(fn, deps)is conceptuallyuseMemo(() => fn, deps).
Why useCallback only helps with React.memo
Without React.memo: The child always re-renders when the parent does — useCallback is useless.
function Child({ onClick }) {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
}With React.memo: React does a shallow prop comparison. Same onClick reference → skip re-render ✅. New onClick reference → re-render ❌. Now useCallback matters.
const Child = React.memo(function Child({ onClick }) {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
});- ❌ Without
React.memo: Every parent render → child renders. - ✅ With
React.memo+useCallback: Parent renders → child skipped (if deps unchanged).
Important:
useCallbacknever stops a re-render by itself. It preserves function identity soReact.memocan make that decision. WithoutReact.memo, it has no effect on rendering.
useEffect vs useMemo — Timing
Render starts
↓
useMemo executes (if deps changed)
↓
JSX renders using memoized values
↓
DOM updates
↓
useEffect runs
useMemoprepares values before the screen is painted.useEffectruns after the screen is painted to perform side effects.
useContext — Avoid Prop Drilling
Consume shared/global data in any component inside the Provider. Context only applies to components wrapped by that Provider — not to every component globally.
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>;
}Components outside the Provider get the default value
const ThemeContext = createContext("light"); // default
function Footer() {
const theme = useContext(ThemeContext); // "light" if outside Provider
return <p>{theme}</p>;
}Multiple consumers and nested Providers
Any component inside the Provider can consume the context:
<ThemeContext.Provider value="dark">
<Navbar />
<Sidebar />
<Main />
</ThemeContext.Provider>Nested Providers work too — the nearest Provider wins:
<ThemeContext.Provider value="dark">
<Navbar /> {/* gets "dark" */}
<ThemeContext.Provider value="light">
<Sidebar /> {/* gets "light" */}
</ThemeContext.Provider>
</ThemeContext.Provider>Part 4 — Performance
Preventing Unnecessary Re-renders
Three techniques work together:
- Wrap the child in
React.memo()— only re-renders when its props change. - Pass stable function props with
useCallback. - Pass stable object/array props with
useMemo. - Avoid inline objects/functions in JSX (e.g.
onClick={() => ...}orstyle={{ color: 'red' }}) — these always look like new props toReact.memo.
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:
React.memoon the child + stable props from the parent (useCallbackfor functions,useMemofor objects). Avoid inline functions/objects in JSX.
Lazy Loading & Suspense
Lazy loading delays loading resources until they're needed — reducing initial bundle size.
Images (native browser):
<img src="large-image.jpg" loading="lazy" alt="example" />Components (React):
import React, { Suspense } from "react";
const LazyComponent = React.lazy(() => import("./LazyComponent"));
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>;Next.js dynamic import (with SSR control):
import dynamic from "next/dynamic";
const Heavy = dynamic(() => import("./Heavy"), {
ssr: false,
loading: () => <p>Loading...</p>,
});Suspense lets React "wait" for lazy-loaded components and show a fallback (spinner, skeleton) while loading. In Next.js, prefer dynamic() for SSR control; Suspense for data fetching is still evolving.
Interview tip:
Suspense= delay rendering until the resource is ready, show a fallback UI in the meantime.
Part 5 — Next.js
React Router vs Next.js Routing
React has no built-in routing — you use React Router and manually declare routes. Next.js has file-based routing built in.
React Router
import { BrowserRouter, Routes, Route } from "react-router-dom";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<User />} />
</Routes>
</BrowserRouter>
);
}Next.js App Router (modern)
Routes come from your folder structure under 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>;
}Multiple params:
app/users/[id]/posts/[postId]/page.jsx
URL /users/42/posts/7 → params = { id: "42", postId: "7" }
Next.js Pages Router (older)
Same idea — folder structure under pages/ defines routes (e.g. pages/blog/[slug].js → /blog/:slug).
Navigation in both: import Link from "next/link" → <Link href="/about">About</Link>.
Server Components vs Client Components
- No
"use client"directive → Server Component by default. Use for: fetching data securely (DB, API keys). - Add
"use client"when you need React hooks (useState,useEffect,useRef), event handlers, or browser APIs (window,document,localStorage).
Next.js API Routes
Next.js lets you write backend endpoints in the /api folder.
Option A — API Routes (REST style)
// 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.
Option B — Server Actions ("use server")
// 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: use both
| Use | Best for |
|---|---|
| Server Actions | Internal UI: form submissions, create/update/delete from UI, authenticated user flows |
| API Routes | External access: mobile apps, third-party webhooks (Stripe, PayPal), public APIs, OAuth callbacks |
Proxy Server & CORS (AxiosError)
A Next.js proxy doesn't bypass real failures — it avoids browser CORS restrictions by moving the request server-side (where CORS doesn't apply), then returns the result to the browser as same-origin.
Why you see AxiosError: Your browser calls https://api.example.com → CORS blocks the response → Axios surfaces a network error.
Option A — rewrites() (simple, mostly for dev):
// next.config.js
module.exports = {
async rewrites() {
return [
{ source: "/api/:path*", destination: "https://api.example.com/:path*" },
];
},
};Option B — API Route as proxy (most control):
// app/api/proxy/route.js
export async function GET(req) {
const url = new URL(req.url);
const upstream = `https://api.example.com${url.searchParams.get("path") || ""}`;
const res = await fetch(upstream, { cache: "no-store" });
return new Response(res.body, {
status: res.status,
headers: { "content-type": res.headers.get("content-type") || "application/json" },
});
}Important: A proxy fixes CORS and lets you attach secrets server-side — it doesn't fix backend downtime or 500 errors.
Session Token vs JWT
Two common authentication approaches power most web apps: session-based and JWT-based (token-based). Here’s how they stack up:
| Topic | Session Token | JWT |
|---|---|---|
| What is stored? | Random session ID (reference to server data) | Signed token containing user claims (e.g., user ID, expiry) |
| Server state | Session data stored in DB/Redis | Stateless – no session storage needed |
| Revocation | Easy: delete the session on the server | Hard: token valid until expiry, unless a deny list is used |
| Scaling | Needs shared session store across servers | Works across servers with just a shared signing key |
| Transport | Usually HttpOnly cookie | Cookie or Authorization: Bearer <token> header |
How They Work
Session-based
After login, the server creates a session and sends its ID (typically in an HttpOnly cookie). The browser automatically includes that cookie with every request, and the server looks up the session data. Revoking access is as simple as deleting that session from the store.
JWT-based
The server signs a token containing user info and sends it to the client. The client stores it (ideally in an HttpOnly cookie) and sends it with each request. The server validates the signature and expiry — no database lookup required. This makes JWTs stateless and highly scalable, but revoking them before expiry is trickier.
When to Use Which
- Sessions: great for server-rendered web apps where you need immediate logout and simple revocation.
- JWTs: shine in API-first systems, microservices, or any stateless architecture.
Security Tips
- Always use
HttpOnly+Secure+SameSitecookies (neverlocalStorage— it’s vulnerable to XSS). - Keep access tokens short-lived and use refresh tokens for long-term sessions.
- For session-based apps, protect against CSRF with anti-CSRF tokens or
SameSitecookies.
Offset vs Cursor Pagination
Offset pagination:
GET /api/posts?limit=20&offset=40
- ✅ Simple, supports page numbers
- ❌ Slow for large offsets, can miss/duplicate items if rows change between requests
Cursor pagination:
GET /api/posts?limit=20&cursor=2026-03-09T12:00:00.000Z
- ✅ Fast at scale, consistent results, great for infinite scroll
- ❌ Requires a stable sort key, can't jump to arbitrary pages
Interview line: Offset is easy but unstable/slow at scale; cursor is stable/fast but not page-number-friendly.
Part 6 — State Management
The Problem with a Single Context
When one Context holds multiple values, all consumers re-render whenever any value changes — even if they don't use the changed value.
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"); // ❌ runs even when only count changes
return <p>{theme}</p>;
}
function CounterDisplay() {
const { count } = useContext(AppContext);
console.log("CounterDisplay rendered");
return <p>{count}</p>;
}When count changes, both ThemeDisplay and CounterDisplay re-render because the Provider value object is new on every update.
The common workaround — split contexts: ThemeContext and CounterContext separately. Theme updates → only theme consumers re-render. Downside: "Provider hell" with deeply nested Providers.
Zustand — Fine-Grained Subscriptions
Zustand uses selectors to subscribe components to specific slices. It re-renders a component only when that slice changes.
import { create } from "zustand";
const useStore = create((set) => ({
theme: "dark",
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
toggleTheme: () =>
set((s) => ({ theme: s.theme === "dark" ? "light" : "dark" })),
}));function ThemeDisplay() {
const theme = useStore((s) => s.theme); // subscribes only to theme
const toggleTheme = useStore((s) => s.toggleTheme);
console.log("ThemeDisplay rendered");
return (
<>
<p>Theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</>
);
}
function CounterDisplay() {
const count = useStore((s) => s.count); // subscribes only to count
const inc = useStore((s) => s.inc);
console.log("CounterDisplay rendered");
return (
<>
<p>Count: {count}</p>
<button onClick={inc}>+</button>
</>
);
}- Click "+" → only
CounterDisplayre-renders.ThemeDisplay❌ does not. - Click "Toggle Theme" → only
ThemeDisplayre-renders.CounterDisplay❌ does not.
Interview line: Zustand creates a global hook-based store with fine-grained subscriptions;
useContextbroadcasts changes to all consumers.
Redux (Redux Toolkit) — Same Idea, More Structure
Redux with useSelector gives the same fine-grained subscriptions — a component only re-renders when the slice it selects changes.
// 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
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>
);
}Same behavior: click "+" → only CounterDisplay re-renders; click "Toggle Theme" → only ThemeDisplay re-renders.
Redux vs Zustand
| Redux (Redux Toolkit) | Zustand | |
|---|---|---|
| Boilerplate | More (actions, reducers, store setup) | Minimal |
| Structure | Opinionated, strict | Flexible |
| Ecosystem | Large (middleware, DevTools, time-travel) | Small |
| Data fetching | RTK Query (built-in) | None built-in |
| Best for | Large teams, complex apps | Small–medium apps |
Prefer Redux when you need RTK Query or strong DevTools / middleware. Zustand has no built-in data-fetching layer.
RTK Query — Automated Data Fetching
Without RTK Query, you manage fetching, loading, error, caching, and re-fetching yourself. RTK Query automates all of it.
1. Create an API slice:
// 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 store:
import { configureStore } from "@reduxjs/toolkit";
import { postsApi } from "./services/postsApi";
export const store = configureStore({
reducer: { postsApi: postsApi.reducer },
middleware: (getDefaultMiddleware) => [
...getDefaultMiddleware(),
postsApi.middleware, // network + cache logic
],
});3. Use it in a component — no fetch, no useEffect:
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:
- Component calls
useGetPostsQuery(). - RTK Query dispatches
getPosts.initiate. - Middleware checks the Redux cache — serves from cache if available, fetches from server if not.
- Server responds → middleware dispatches a success action.
- Reducer stores it →
state.postsApi.queries["getPosts(...)"].data.
If two components call useGetPostsQuery(), RTK Query makes one HTTP request and both read from the shared cache.
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.
Part 7 — JavaScript Patterns
Remove Duplicates from an Array
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 ===
==(loose): compares values after type coercion —"5" == 5→true.===(strict): compares value and type, no coercion —"5" === 5→false.
Rule of thumb: Prefer ===. Use == only when you explicitly want coercion (e.g. x == null to catch both null and undefined).
null vs undefined
undefined= never set / missing property / no return value.typeof undefined === "undefined".null= intentionally empty — you assigned it.typeof null === "object"(historical quirk).
let a; // undefined
const b = null; // null
const o = {};
o.xyz; // undefined (missing property)Use x == null to check for both in one expression.
Debounce
Waits until the user stops triggering an event for X ms, then runs the function once. Prevents multiple rapid triggers from causing multiple executions.
t=0ms keypress → start timer
t=100ms keypress → reset timer
t=200ms keypress → reset timer
t=500ms no input for 300ms → function runs once
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const handleInput = debounce((value) => {
console.log("Searching for:", value);
}, 300);Interview tip: Debounce = wait X ms after the last event before running the function.
Deep Clone an Object
// ✅ Preferred (handles Date, Map, Set, undefined, circular refs)
const clone = structuredClone(obj);
// ⚠️ JSON-safe only (loses Date, Map, Set, undefined, functions)
const clone = JSON.parse(JSON.stringify(obj));Why the difference matters:
const obj = {
createdAt: new Date("2026-02-10"),
scores: new Map([["math", 90]]),
tags: new Set(["react"]),
meta: undefined,
};
// structuredClone ✅
const a = structuredClone(obj);
console.log(a.createdAt instanceof Date); // true
console.log(a.scores instanceof Map); // true
// JSON clone ❌
const b = JSON.parse(JSON.stringify(obj));
console.log(b.createdAt); // "2026-02-10T00:00:00.000Z" (string)
console.log(b.scores); // {} — Map silently lostInterview tip: Use
structuredClonefor anything beyond plain JSON-safe data.
Part 8 — Tailwind CSS Patterns
Center a div Horizontally and Vertically
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>
);
}items-center = vertical, justify-center = horizontal. min-h-screen gives the container height to center within.
Responsive note: Add breakpoint prefixes like sm:, md:, lg: for responsive sizing — e.g. className="text-base md:text-lg lg:text-xl".
Hover Background Transition
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>
);
}Key classes: hover:bg-* sets the hover color; transition-colors animates it; duration-300 controls the speed.
relative, absolute, fixed, sticky
| Position | Behavior |
|---|---|
relative | Stays in normal flow; offsets from its own original position. Doesn't affect other elements. |
absolute | Removed from flow. Positioned against the nearest positioned ancestor (or viewport). |
fixed | Removed from flow. Positioned against the viewport — stays in place on scroll. |
sticky | In flow until a scroll threshold (e.g. top-0), then sticks like fixed within its parent. |
Interview tip: relative = offset from self, in flow; absolute = out of flow, vs positioned ancestor; fixed = out of flow, vs viewport; sticky = in flow then sticks.
To know more about Tailwind CSS (display types, flexbox, grid, responsive breakpoints, and more), see Tailwind CSS Essentials.
Part 9 — Developer Workflow
AI Tools That Speed Things Up
- Defining folder structure with ChatGPT before coding.
- Creating an API docs file (endpoints, request/response, auth) to use as context.
- Cursor Agent for coding according to the generated structure and API details file.
- MCP Figma server to replicate the Figma UI exactly in code.
Final Tips
- DOM = bridge between HTML and JS. Know tree structure and common manipulations.
- React: Virtual DOM + components. Know lifecycle + hooks.
- Props are read-only; state is reactive and local.
useEffect= after-render side effects. Dependency array controls when it 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:
useContextfor simple cases → Zustand → Redux as scale and complexity grow. - Practice by building small examples and inspecting console logs to see when effects and memos run.
TL;DR — One-Line Interview Answers
- DOM: Programmatic tree of the document; JS uses it to manipulate UI.
- Event bubbling vs capturing: Bubbling = event goes up (target → root); capturing = goes down (root → target). Default is bubbling.
- React: UI library with a component model and virtual DOM.
- Virtual DOM: In-memory UI representation; React diffs it to minimize real DOM updates.
- File-based routing (Next.js): Routes come from
/appor/pagesfolder structure. - API routes (Next.js): Backend 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 custom hooks for reusable logic.
useState: Mutable state that triggers re-renders.useEffect: Run side effects after render.useRef: Mutable storage / DOM reference, no re-render.useMemo: Memoize a value during render (only if deps change).useCallback: Memoize a function reference.useContext: Consume shared data; re-renders all consumers on any Provider value change.- Prevent child re-renders:
React.memo+ stable props (useCallbackfor functions,useMemofor objects); avoid inline objects/functions in JSX. - Lazy loading: Load only when needed (images/components).
Suspense: Show a fallback while waiting for a lazy resource.- Zustand vs
useContext: Zustand = fine-grained hook store;useContext= broadcasts to all consumers. - Redux vs Zustand: Redux = structured, more boilerplate, great for large apps + RTK Query; Zustand = minimal, hook-based.
- Session vs JWT: Session = opaque ID + server store; JWT = signed self-contained token (harder revocation).
- Offset vs cursor pagination: Offset is simple but slow/unstable at scale; cursor is fast/stable but no page-jumping.
- Next.js proxy vs
AxiosError: Proxy avoids browser CORS by forwarding server-side; doesn't fix real backend errors. - Remove array duplicates:
[...new Set(arr)]for primitives. ==vs===:===strict (type + value);==coerces types first.nullvsundefined:undefined= unset/missing;null= intentionally empty. Both falsy;typeof nullis"object".- Debounce: Wait X ms after the last event before running the function.
- Deep clone:
structuredClone(obj)preferred;JSON.parse(JSON.stringify(obj))for plain JSON-safe data only. - Center with Tailwind:
min-h-screen flex items-center justify-center. - Hover transition (Tailwind):
hover:bg-* transition-colors duration-300. relative/absolute/fixed/sticky: relative = in flow, offset from self; absolute = out of flow, vs ancestor; fixed = out of flow, vs viewport; sticky = in flow then sticks.