CodingLad
frontend

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

The Complete Frontend Interview Guide: DOM, React, Next.js & State Management
0 views
21 min read
#frontend
Table Of Content

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:

  1. Capturing phase (top → down): onClickCapture handlers run, firing Parent → Child.
  2. Target phase: The child receives the click.
  3. Bubbling phase (bottom → up): onClick handlers run, firing Child → 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 in useEffect, 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

useStateuseRef
Triggers re-render✅ Yes❌ No
Updates UI✅ Yes❌ No
Good forReactive UI valuesMutable 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

  • useMemo memoizes values → returns a value.
  • useCallback memoizes functions → returns a function.
  • useCallback(fn, deps) is conceptually useMemo(() => 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: useCallback never stops a re-render by itself. It preserves function identity so React.memo can make that decision. Without React.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
  • useMemo prepares values before the screen is painted.
  • useEffect runs 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:

  1. Wrap the child in React.memo() — only re-renders when its props change.
  2. Pass stable function props with useCallback.
  3. Pass stable object/array props with useMemo.
  4. Avoid inline objects/functions in JSX (e.g. onClick={() => ...} or style={{ color: 'red' }}) — these always look like new props to React.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.memo on the child + stable props from the parent (useCallback for functions, useMemo for 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/7params = { 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

UseBest for
Server ActionsInternal UI: form submissions, create/update/delete from UI, authenticated user flows
API RoutesExternal 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:

TopicSession TokenJWT
What is stored?Random session ID (reference to server data)Signed token containing user claims (e.g., user ID, expiry)
Server stateSession data stored in DB/RedisStateless – no session storage needed
RevocationEasy: delete the session on the serverHard: token valid until expiry, unless a deny list is used
ScalingNeeds shared session store across serversWorks across servers with just a shared signing key
TransportUsually HttpOnly cookieCookie 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 + SameSite cookies (never localStorage — 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 SameSite cookies.

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 CounterDisplay re-renders. ThemeDisplay ❌ does not.
  • Click "Toggle Theme" → only ThemeDisplay re-renders. CounterDisplay ❌ does not.

Interview line: Zustand creates a global hook-based store with fine-grained subscriptions; useContext broadcasts 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
BoilerplateMore (actions, reducers, store setup)Minimal
StructureOpinionated, strictFlexible
EcosystemLarge (middleware, DevTools, time-travel)Small
Data fetchingRTK Query (built-in)None built-in
Best forLarge teams, complex appsSmall–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:

  1. Component calls useGetPostsQuery().
  2. RTK Query dispatches getPosts.initiate.
  3. Middleware checks the Redux cache — serves from cache if available, fetches from server if not.
  4. Server responds → middleware dispatches a success action.
  5. 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" == 5true.
  • === (strict): compares value and type, no coercion — "5" === 5false.

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 lost

Interview tip: Use structuredClone for 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

PositionBehavior
relativeStays in normal flow; offsets from its own original position. Doesn't affect other elements.
absoluteRemoved from flow. Positioned against the nearest positioned ancestor (or viewport).
fixedRemoved from flow. Positioned against the viewport — stays in place on scroll.
stickyIn 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 Suspense for fallbacks.
  • For app-level state: useContext for simple cases → ZustandRedux 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 /app or /pages folder 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 (useCallback for functions, useMemo for 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.
  • null vs undefined: undefined = unset/missing; null = intentionally empty. Both falsy; typeof null is "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.