Back to Categories
React / Next.js

React / Next.js

Hooks, reconciliation, React Server Components, App Router, ISR and Server Actions

26Questions

๐Ÿง  Simple Definition (Word-for-word)

React's Virtual DOM is an in-memory representation of the real DOM. Reconciliation is React's diffing algorithm that compares the old Virtual DOM with the new Virtual DOM when state changes, and updates only the modified elements in the real DOM. Keys are unique identifiers that provide a stable identity to list items, allowing React to quickly identify additions, removals, or re-ordering without destroying and rebuilding elements.


โšก Super Simple Line

Reconciliation = compare Old vs New Virtual DOM and batch-update only the differences to the real DOM.


๐Ÿงช Role of Keys Example

// Bad (using index)
{items.map((item, index) => <li key={index}>{item.text}</li>)}

// Good (using stable unique ID)
{items.map(item => <li key={item.id}>{item.text}</li>)}

If list items are reordered or sorted, using index as a key causes React to re-evaluate and re-render every item in the list. Using a stable unique ID allows React to simply swap DOM nodes, optimizing rendering speed.


โšก Diffing Rules

  • Different Element Types: React will tear down the entire old DOM tree and build the new one from scratch.

  • Same Element Types: React compares attributes/classes, updates changed attributes, and recursively diffs child nodes.

  • Keys in Lists: Stable, unique keys allow React to identify moved nodes and prevent full component re-mounts.


โšก One-line Interview Answer

Reconciliation is the process of diffing Virtual DOM trees to minimize real DOM updates, where stable keys serve as persistent element IDs to prevent redundant re-renders.

๐Ÿง  Simple Definition

A controlled component is a form element whose value is managed by React state, while an uncontrolled component manages its own state inside the DOM and is accessed using refs.


โšก Super Simple Line

Controlled = React controls the value
Uncontrolled = DOM controls the value


๐ŸŸข Controlled Component

React state is the single source of truth.

function App() {
  const [name, setName] = useState("");

  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
  );
}

๐Ÿง  What happens?

User types
โ†“
onChange fires
โ†“
React state updates
โ†“
Input value updates

React always knows the current value.


๐ŸŒ Mental Model

Input
  โ†“
React State
  โ†“
Input

React is in control.


๐Ÿ”ต Uncontrolled Component

The DOM stores the value.

function App() {
  const inputRef = useRef();

  const handleSubmit = () => {
    console.log(inputRef.current.value);
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleSubmit}>
        Submit
      </button>
    </>
  );
}

๐Ÿง  What happens?

User types
โ†“
DOM stores value
โ†“
React doesn't know it
โ†“
Read value using ref when needed

๐ŸŒ Mental Model

Input (DOM)
  โ†“
Stores value itself

React is not managing it.


๐Ÿ”ฅ Key Differences

Feature

Controlled

Uncontrolled

Source of truth

React state

DOM

Uses state

Yes

No

Uses refs

Optional

Yes

Validation

Easy

Harder

React awareness

Always knows value

Doesn't know until read

Preferred in React

โœ… Yes

Sometimes


๐Ÿงช Example Use Cases

Controlled

Login form
Signup form
Search input
Real-time validation

Because React always knows the value.


Uncontrolled

Simple forms
File inputs
Legacy integrations

๐Ÿšจ Interview Trap

File input is usually uncontrolled

<input type="file" />

Because browsers don't allow React to control its value.


๐Ÿง  Why Controlled Components are Preferred

Because React can:

  • Validate instantly

  • Show errors

  • Disable buttons

  • Transform values

  • Sync UI with state


โšก One-line Interview Answer

A controlled component stores its value in React state and updates it through event handlers, while an uncontrolled component stores its value in the DOM and is accessed using refs when needed.


๐Ÿš€ 10-Second Version

Controlled components are managed by React state, while uncontrolled components let the DOM manage the value and use refs to access it.


๐Ÿง  Memory Trick

Controlled = React controls
Uncontrolled = DOM controls

๐ŸŽฏ Interview Gold Sentence

"I generally prefer controlled components because React becomes the single source of truth, making validation, conditional UI updates, and form state management much easier. Uncontrolled components are useful when direct DOM access is sufficient, such as file inputs."

๐Ÿง  Simple Definition (Word-for-word)

useRef is a React hook that returns a mutable reference object whose current property persists across renders. Unlike useState, changing a ref's current value does not trigger a component re-render. It is used to directly reference DOM nodes, or to persist mutable data that shouldn't affect the visual rendering lifecycle.


โšก Super Simple Line

useState = state changes trigger re-render (drives UI).
useRef = reference changes do not trigger re-render (persists data/DOM refs).


๐Ÿงช Code Example (DOM Access vs Persistent Tracker)

import { useState, useRef, useEffect } from "react";

function Tracker() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);
  const inputRef = useRef(null);

  // Tracks renders without infinite loop re-renders
  useEffect(() => {
    renderCount.current += 1;
  });

  const focusInput = () => {
    inputRef.current.focus(); // Direct DOM access
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
      <button onClick={() => setCount(count + 1)}>Increment UI State</button>
      <p>Render count: {renderCount.current}</p>
    </div>
  );
}

๐Ÿ“Š Comparison Table

FeatureuseRefuseState
Triggers Re-render?โŒ Noโœ… Yes
Accesses DOM?โœ… Yes (via ref attribute)โŒ No (virtual representation)
Usage ScopeStoring timer IDs, DOM nodes, previous state valuesForm input value, server data, toggle states
UpdatesSynchronous (immediate value change)Asynchronous (batched state updates)

โšก One-line Interview Answer

useRef persists values across renders without triggering a re-render and provides direct access to DOM nodes, while useState triggers a re-render on every state update to reflect changes in the UI.

๐Ÿง  Simple Definition

The Rules of Hooks are guidelines that ensure React can correctly track hook state between renders.


โšก Super Simple Line

Hooks must always be called in the same order on every render.


๐ŸŒ Why do these rules exist?

React identifies hooks by their order:

useState(...)
useEffect(...)
useMemo(...)

React remembers:

Hook #1 = useState
Hook #2 = useEffect
Hook #3 = useMemo

If the order changes:

React gets confused โŒ

๐Ÿ”ฅ Rule 1

Only Call Hooks at the Top Level

โœ… Correct

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

  useEffect(() => {
    console.log(count);
  }, []);

  return <div>{count}</div>;
}

โŒ Wrong

if (isLoggedIn) {
  useEffect(() => {
    console.log("Hello");
  }, []);
}

Why?

Sometimes hook runs
Sometimes it doesn't

Hook order changes.


๐Ÿ”ฅ Rule 2

Never Call Hooks Inside Loops

โŒ Wrong

for (let i = 0; i < 5; i++) {
  useEffect(() => {});
}

Because:

Number of hooks may change

๐Ÿ”ฅ Rule 3

Never Call Hooks Inside Nested Functions

โŒ Wrong

function helper() {
  useState(0);
}

React only expects hooks:

During component render

๐Ÿ”ฅ Rule 4

Only Call Hooks in React Components or Custom Hooks

โœ… Correct

function UserProfile() {
  const [user] = useState();
}

โœ… Also Correct

function useUser() {
  const [user] = useState();
}

โŒ Wrong

function fetchUser() {
  useState();
}

Regular JS function.


๐ŸŒ Mental Model

React keeps a list:

Render #1
---------
1. useState
2. useEffect
3. useMemo

Next render must be:

1. useState
2. useEffect
3. useMemo

Same order every time.


๐Ÿšจ Common Interview Trap

โŒ Conditional Hook

if (loading) {
  useEffect(() => {});
}

โœ… Move condition inside hook

useEffect(() => {
  if (loading) {
    // logic
  }
}, [loading]);

๐Ÿง  Easy Memory Trick

Hooks can be called:

โœ… Top level
โœ… React component
โœ… Custom hook

Hooks cannot be called:

โŒ if statements
โŒ loops
โŒ nested functions
โŒ regular JS functions

โšก One-line Interview Answer

The Rules of Hooks state that hooks must be called only at the top level of React function components or custom hooks and must never be called conditionally, inside loops, or inside nested functions, ensuring React can preserve hook order between renders.


๐Ÿš€ 10-Second Version

Hooks must always be called at the top level of React components or custom hooks and in the same order on every render.


๐Ÿง  Memory Trick

Top level only
Same order always

๐ŸŽฏ Interview Gold Sentence

"React relies on the order of hook calls to associate state with hooks. That's why hooks must always be called at the top level of components or custom hooks and never conditionally or inside loops."

๐Ÿง  Simple Definition (Word-for-word)

useCallback memoizes a function definition to prevent it from being recreated on every render. useMemo memoizes the computed result of an expensive calculation. React.memo is a higher-order component that prevents a functional component from re-rendering unless its props change. They fail to prevent re-renders when passing inline object reference props, unmemoized inline functions, or when props change on every cycle.


โšก Super Simple Line

useCallback = caches functions.
useMemo = caches values.
React.memo = caches components.


๐Ÿงช Code Example

import React, { useState, useCallback, useMemo } from "react";

// Child component memoized
const ChildComponent = React.memo(({ onClick }) => {
  console.log("Child render");
  return <button onClick={onClick}>Click Child</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(0);

  // useCallback prevents Child from rendering when "other" changes
  const handleClick = useCallback(() => {
    console.log("Clicked child");
  }, []);

  // useMemo prevents recalculation unless count changes
  const expensiveCalculation = useMemo(() => {
    let sum = 0;
    for (let i = 0; i < 1000000; i++) sum += count;
    return sum;
  }, [count]);

  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <button onClick={() => setOther(other + 1)}>Change unrelated state</button>
    </div>
  );
}

๐Ÿšจ Performance Pitfalls: When They Fail

Memoization adds computing overhead. It can be counter-productive or fail when:

  • Unmemoized Object Props: If you use React.memo(Child), but pass <Child data={{ value: 5 }} />, the component will re-render anyway because {{ value: 5 }} creates a new object reference on every render.

  • Cheap Computations: Wrapping a simple operation like a + b in useMemo is slower than recalculating it because the overhead of dependency checking exceeds the computation cost.


โšก One-line Interview Answer

useCallback caches function instances, useMemo caches calculated results, and React.memo prevents component re-renders unless props change, though they fail if parent props are unmemoized object references.

๐Ÿง  Simple Definition

Context API allows sharing state globally in React, but it can cause unnecessary re-renders because every consumer re-renders when the context value changes.


โšก Super Simple Line

Context API = โ€œglobal state sharing, but can re-render too many componentsโ€


๐ŸŒ Core Idea

Context value changes
โ†“
All consumers re-render

Even if they only use a small part of the value.


๐Ÿงช Simple Example

const UserContext = createContext();

function App() {
  const [user, setUser] = useState({ name: "A" });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Child />
    </UserContext.Provider>
  );
}

๐Ÿง  What happens?

User changes
โ†“
New object created
โ†“
All consumers re-render โŒ

๐Ÿ”ฅ Performance Pitfall #1

โŒ Whole tree re-renders

Even if only setUser is used:

Every component using context re-renders

๐Ÿ”ฅ Pitfall #2

โŒ Object reference changes every render

value={{ user, setUser }}

This is always a new object:

New reference โ†’ triggers re-render โŒ

๐Ÿ”ฅ Pitfall #3

โŒ Mixed unrelated state in same context

value={{
  user,
  theme,
  notifications
}}

If theme changes:

user components still re-render โŒ

๐Ÿ”ฅ Pitfall #4

โŒ Context used for frequently changing state

Examples:

Mouse position
Typing input
Animations
Real-time data

๐Ÿ‘‰ causes constant re-renders


๐ŸŒ Mental Model

Context = global broadcast system
Everyone listening gets update

Even if they don't need it.


๐Ÿงช Example Problem

<UserContext.Provider value={user}>
  <Header />
  <Sidebar />
  <Footer />
</UserContext.Provider>

If user changes:

Header re-renders
Sidebar re-renders
Footer re-renders

Even if only Header needs it.


๐Ÿšจ Why this happens

Context triggers:

Any change in value โ†’ all consumers re-run

No fine-grained updates.


๐ŸŸข How to fix Context performance issues


1. Split contexts

UserContext
ThemeContext
AuthContext

Instead of one big context.


2. Memoize value

const value = useMemo(() => ({
  user,
  setUser
}), [user]);

3. Avoid passing objects directly

Bad:

value={{ user }}

Better:

split state or memoize

4. UseContext selector pattern (advanced)

Only subscribe to needed slice.


5. Combine with React.memo

Context + memoized children

๐Ÿง  Key Insight

Context is not a state management optimization tool โ€” it is a convenience tool, not a performance one.


โšก One-line Interview Answer

Context API can cause performance issues because any change in context value triggers re-render of all consuming components, especially when objects are recreated each render or unrelated state is grouped into a single context.


๐Ÿš€ 10-Second Version

Context API can cause unnecessary re-renders because all consumers re-render when the context value changes, especially when using objects or frequently changing state.


๐Ÿง  Memory Trick

Context change โ†’ all consumers update

๐ŸŽฏ Interview Gold Sentence

"The main performance issue with Context API is that any change in the context value causes all consuming components to re-render, especially when the value is an object recreated on every render or when unrelated state is grouped together, which is why splitting contexts and memoizing values is important."


๐Ÿง  Simple Definition (Word-for-word)

The useEffect hook handles side effects in React. Its lifecycle consists of running the effect after the component mounts/updates, and executing its cleanup function (if returned) right before the effect runs again and when the component unmounts. Circular dependencies occur when an effect updates a state variable that is also listed as a dependency, causing an infinite rendering loop, and they are resolved by using functional state updates or refs.


โšก Super Simple Line

useEffect runs side effects; return a cleanup function to cancel subscriptions/timers and prevent memory leaks.


๐Ÿงช Code Example (Cleanup & Circular Dependency Fix)

import { useState, useEffect } from "react";

// 1. Correct Effect with Cleanup
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1); // โœ… Functional update avoids circular dependency
    }, 1000);

    return () => clearInterval(interval); // โœ… Cleanup prevents memory leak
  }, []); // Empty dependency array because we use functional update
}

// 2. Circular Dependency (Broken Case)
// useEffect(() => {
//   setSeconds(seconds + 1); // โŒ updating state based on current value
// }, [seconds]); // โŒ causes infinite loop re-renders

๐Ÿšจ Resolving Circular Loops

  • Functional State Updates: Instead of writing setCount(count + 1) which depends on count, write setCount(prev => prev + 1) to remove count from the dependencies.

  • useRef: Store mutable values that do not affect the UI inside a ref instead of component state.


โšก One-line Interview Answer

useEffect handles mounting, updating, and unmounting phases, running cleanups on unmount or re-runs, and circular loops are resolved by decoupling state variables from dependencies via functional updates.

๐Ÿง  Simple Definition

useEffect runs after the browser paints the screen, while useLayoutEffect runs synchronously before the paint, blocking visual updates until it finishes.


โšก Super Simple Line

useEffect = runs after screen update
useLayoutEffect = runs before screen update


๐ŸŒ Core Idea

Render โ†’ DOM update โ†’ Paint screen โ†’ useEffect runs
Render โ†’ DOM update โ†’ useLayoutEffect runs โ†’ Paint screen

๐Ÿงช Simple Example

useEffect(() => {
  console.log("useEffect");
}, []);

useLayoutEffect(() => {
  console.log("useLayoutEffect");
}, []);

Output order:

useLayoutEffect
useEffect

๐Ÿง  Key Difference

๐ŸŸข useEffect

Does NOT block painting
Runs after UI is visible

๐Ÿ‘‰ user sees screen first


๐Ÿ”ต useLayoutEffect

Blocks painting
Runs before browser shows UI

๐Ÿ‘‰ user sees final corrected UI immediately


๐ŸŒ Mental Model


useEffect

Paint UI
โ†“
Then run logic

useLayoutEffect

Run logic
โ†“
Then paint UI

๐Ÿงช Real Example (DOM measurement)


Problem without useLayoutEffect

useEffect(() => {
  const rect = ref.current.getBoundingClientRect();
  setHeight(rect.height);
}, []);

๐Ÿ‘‰ user may see flicker


Correct approach

useLayoutEffect(() => {
  const rect = ref.current.getBoundingClientRect();
  setHeight(rect.height);
}, []);

๐Ÿ‘‰ no flicker


๐Ÿ”ฅ When to use useLayoutEffect


๐ŸŸข 1. DOM measurements

getBoundingClientRect()
scroll position
layout calculations

๐ŸŸก 2. Prevent flicker UI updates

Position adjustment before paint

๐Ÿ”ต 3. Synchronous DOM updates

must happen before user sees UI

๐ŸŸข When to use useEffect


API calls
logging
subscriptions
timers
data fetching

๐Ÿ‘‰ most cases


๐Ÿšจ Important Warning

โŒ useLayoutEffect blocks rendering

Heavy logic inside useLayoutEffect
โ†“
UI becomes slow

๐ŸŒ Real Mental Model

useLayoutEffect = โ€œfix layout before user sees itโ€
useEffect = โ€œdo side work after UI is shownโ€

๐Ÿ”ฅ Key Differences Table

Feature

useEffect

useLayoutEffect

Execution time

After paint

Before paint

Blocks UI

โŒ No

โœ… Yes

Use case

async tasks

DOM measurements

Performance

Better

Slower if misused


โšก One-line Interview Answer

useEffect runs asynchronously after the browser paints the UI, while useLayoutEffect runs synchronously before painting, blocking rendering until it completes, making it useful for DOM measurements and layout adjustments.


๐Ÿš€ 10-Second Version

useEffect runs after render and paint, while useLayoutEffect runs before paint and blocks rendering until it finishes.


๐Ÿง  Memory Trick

useEffect โ†’ after paint  
useLayoutEffect โ†’ before paint

๐ŸŽฏ Interview Gold Sentence

"I use useEffect for most side effects like data fetching and subscriptions, but I use useLayoutEffect when I need to read or modify the DOM before the browser paints to avoid layout shifts or visual flickering."


๐Ÿง  Simple Definition

React 18 Concurrent Features allow React to prepare multiple UI updates at the same time and interrupt, prioritize, or pause rendering work so the app stays responsive even under heavy load.


โšก Super Simple Line

Concurrent React = โ€œReact can pause, prioritize, and resume rendering to keep UI smoothโ€


๐ŸŒ Core Idea

Old React โ†’ renders everything immediately (blocking)

React 18 โ†’ breaks rendering into chunks + prioritizes important updates

๐Ÿงช Simple Example

Without concurrency (older behavior)

Heavy render starts
โ†“
UI freezes โŒ
โ†“
Then updates appear

With React 18 concurrency

Heavy render starts
โ†“
React pauses work
โ†“
User input handled
โ†“
Rendering resumes

๐Ÿ”ฅ Key React 18 Concurrent Features


๐ŸŸข 1. Automatic Batching

Before React 18:

setState();
setState();
// multiple re-renders โŒ

React 18:

Multiple updates grouped into one render โœ…

Example:

setCount(c => c + 1);
setFlag(true);

๐Ÿ‘‰ only ONE re-render


๐ŸŸก 2. startTransition API

startTransition(() => {
  setSearchResults(input);
});

What it means:

Urgent updates (typing) โ†’ high priority
Search rendering โ†’ low priority

๐Ÿ‘‰ UI stays responsive while heavy updates run


๐Ÿง  Mental Model

User typing โ†’ high priority
Big list rendering โ†’ low priority

๐Ÿ”ต 3. Concurrent Rendering

React can:

Start rendering
Pause it
Resume later
Discard outdated work

๐Ÿ‘‰ This prevents UI blocking


๐ŸŸฃ 4. Suspense Improvements

<Suspense fallback={<Loader />}>
  <DataComponent />
</Suspense>

Behavior:

Wait for data
Show fallback
Then swap UI smoothly

๐ŸŸ  5. useDeferredValue

const deferredValue = useDeferredValue(input);

Meaning:

Input updates immediately
Heavy UI lags slightly behind

๐Ÿ‘‰ keeps typing smooth


๐ŸŒ Real Mental Model

React splits work into chunks
โ†“
Prioritizes urgent updates
โ†“
Delays non-urgent rendering

๐Ÿšจ Why Concurrent Mode is important

Without it:

Large list render
โ†“
UI freezes โŒ

With it:

UI stays interactive โœ…

๐Ÿงช Real Example

Search UI

User types "react"
โ†“
Input update (urgent)
โ†“
Filter 10k results (non-urgent)

React 18:

Typing stays smooth
Results update slightly later

๐Ÿ”ฅ Key Benefits

  • Smooth UI under heavy load

  • Better responsiveness

  • Prioritized rendering

  • Non-blocking updates

  • Improved UX


โš ๏ธ Important Note

Concurrent features are enabled automatically in React 18 using createRoot, not render.

ReactDOM.createRoot(root).render(<App />);

โšก One-line Interview Answer

React 18 introduces concurrent features that allow rendering work to be interrupted, prioritized, and split into chunks, enabling smoother UI updates through features like automatic batching, startTransition, Suspense improvements, and deferred rendering.


๐Ÿš€ 10-Second Version

React 18 concurrency lets React pause and prioritize rendering work, making UI updates smoother using features like automatic batching, transitions, and Suspense.


๐Ÿง  Memory Trick

Concurrent React = Pause + Prioritize + Resume

๐ŸŽฏ Interview Gold Sentence

"React 18 concurrent features allow React to interrupt and prioritize rendering work, improving responsiveness through automatic batching, transitions, Suspense enhancements, and deferred rendering so that urgent UI updates like typing are not blocked by heavy computations."


๐Ÿง  Simple Definition

Redux and Zustand are both state management tools, but Redux is a structured, opinionated, large-scale state system, while Zustand is a lightweight, simple, and flexible state store for smaller to medium apps.


โšก Super Simple Line

Redux = โ€œstrict, scalable, boilerplate-heavyโ€
Zustand = โ€œsimple, fast, minimal boilerplateโ€


๐ŸŒ Core Idea

Both manage global state

Redux โ†’ centralized + strict rules
Zustand โ†’ minimal + flexible store

๐Ÿงช Simple Example Comparison

Redux style

action โ†’ reducer โ†’ store โ†’ dispatch โ†’ UI

๐Ÿ‘‰ many steps


Zustand style

set(state) โ†’ UI updates

๐Ÿ‘‰ very direct


๐Ÿ”ต Redux (Traditional Approach)


๐Ÿง  How it works

Action โ†’ Reducer โ†’ Store โ†’ Selector โ†’ UI

๐Ÿงช Example

dispatch({ type: "INCREMENT" });

๐ŸŸข Pros

  • Very predictable

  • Strong structure

  • Great for large teams

  • Good debugging tools (Redux DevTools)

  • Scales well


๐Ÿ”ด Cons

  • Boilerplate heavy

  • More setup

  • Slower to write

  • Overkill for small apps


๐ŸŸฃ Zustand (Modern Lightweight Store)


๐Ÿง  How it works

state + actions in one place

๐Ÿงช Example

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 }))
}));

In component:

const count = useStore((state) => state.count);

๐ŸŸข Pros

  • Minimal code

  • No boilerplate

  • Easy to learn

  • Fast setup

  • Flexible


๐Ÿ”ด Cons

  • Less structure

  • Harder to enforce patterns in large teams

  • Fewer built-in conventions


๐ŸŒ Mental Model


Redux

Strict system with rules:
Action โ†’ Reducer โ†’ Store

Zustand

Direct state access:
Store โ†’ UI

๐Ÿ”ฅ When to use Redux


๐ŸŸข Use Redux when:

  • Large enterprise apps

  • Multiple developers

  • Complex state logic

  • Need strict architecture

  • Need middleware (logging, saga, etc.)


Example:

E-commerce platforms
Admin dashboards
Banking systems

๐Ÿ”ฅ When to use Zustand


๐ŸŸข Use Zustand when:

  • Small to medium apps

  • Fast development needed

  • Simple global state

  • React-only apps


Example:

SaaS apps
Dashboards
Side projects
Startups

๐Ÿšจ Key Difference Summary

Feature

Redux

Zustand

Boilerplate

High

Very low

Learning curve

Hard

Easy

Performance

Good

Very good

Structure

Strict

Flexible

Setup time

Slow

Fast

Best for

Large apps

Small/medium apps


๐Ÿง  Key Insight

Redux is architecture-first, Zustand is simplicity-first.


โšก One-line Interview Answer

Redux is a predictable and structured state management library suited for large-scale applications, while Zustand is a lightweight and flexible alternative that reduces boilerplate and is ideal for small to medium-sized apps.


๐Ÿš€ 10-Second Version

Redux is a structured, scalable state management system, while Zustand is a simpler and lighter alternative with less boilerplate, better suited for smaller apps.


๐Ÿง  Memory Trick

Redux = Rules + structure  
Zustand = Simple + fast

๐ŸŽฏ Interview Gold Sentence

"I choose Redux when I need strict state management and scalability in large applications, but I prefer Zustand for smaller projects because it reduces boilerplate and allows me to manage global state in a much simpler and more direct way."


๐Ÿง  Simple Definition

Redux middleware is a layer between dispatching an action and the reducer, used to handle side effects like async API calls, logging, or conditional logic before reaching the reducer.


โšก Super Simple Line

Middleware = โ€œinterceptor between action and reducerโ€


๐ŸŒ Core Idea

dispatch(action)
   โ†“
middleware (do extra work)
   โ†“
reducer updates state

๐Ÿงช Why middleware exists?

Redux reducers must be:

pure functions โŒ no async logic allowed

So async work goes into middleware.


๐Ÿ”ฅ Example Without Middleware (bad)

dispatch({ type: "FETCH_USER" });

// reducer cannot fetch API โŒ

๐ŸŸข Redux Middleware Examples

  • logging

  • API calls

  • async flows

  • analytics tracking


๐ŸŸก Redux Thunk


๐Ÿง  Simple Definition

Thunk allows action creators to return functions instead of objects to handle async logic like API calls.


๐Ÿงช Example

const fetchUser = () => {
  return async (dispatch) => {
    dispatch({ type: "LOADING" });

    const res = await fetch("/api/user");
    const data = await res.json();

    dispatch({ type: "SUCCESS", payload: data });
  };
};

๐Ÿง  Mental Model

Action โ†’ function โ†’ async work โ†’ dispatch real action

๐ŸŸข Pros

  • simple

  • easy to learn

  • minimal setup

  • good for small/medium apps


๐Ÿ”ด Cons

  • logic can become messy

  • no strict structure

  • hard to manage complex flows


๐Ÿ”ต Redux Saga


๐Ÿง  Simple Definition

Redux Saga handles side effects using generator functions to manage complex async flows in a more structured and controllable way.


๐Ÿงช Example

function* fetchUserSaga() {
  try {
    yield put({ type: "LOADING" });

    const user = yield call(api.fetchUser);

    yield put({ type: "SUCCESS", payload: user });
  } catch (e) {
    yield put({ type: "ERROR" });
  }
}

๐Ÿง  Mental Model

Saga = background worker watching actions

๐ŸŸข Pros

  • very powerful

  • handles complex flows

  • better for large apps

  • supports cancel, retry, debounce


๐Ÿ”ด Cons

  • complex syntax (generators)

  • steep learning curve

  • more boilerplate


๐ŸŒ Thunk vs Saga (Core Difference)


Feature

Thunk

Saga

Style

Functions

Generator functions

Complexity

Simple

Advanced

Async handling

Direct

Controlled flow

Best for

Small apps

Large apps

Learning curve

Easy

Hard

Power

Basic

Advanced


๐Ÿง  Real Mental Model


Thunk

Action โ†’ async function โ†’ dispatch result

Saga

Action โ†’ watcher โ†’ worker โ†’ controlled flow

๐Ÿ”ฅ When to use Thunk


Simple API calls
Basic async logic
Small apps

๐Ÿ”ฅ When to use Saga


Complex workflows
Auth flows
Parallel requests
Retries/cancellation
Large-scale apps

๐Ÿงช Example Scenario


Login flow

Thunk

login โ†’ API โ†’ dispatch success

Saga

login request โ†’ watch โ†’ validate โ†’ retry โ†’ handle failure โ†’ success

๐Ÿšจ Common Interview Insight

Thunk is โ€œdo async work inside actionโ€, Saga is โ€œmanage async workflows externallyโ€


โšก One-line Interview Answer

Redux middleware is a layer that intercepts actions before they reach reducers to handle side effects. Thunk allows writing async logic inside action creators, while Saga uses generator functions to manage complex async workflows in a more structured and controllable way.


๐Ÿš€ 10-Second Version

Middleware handles side effects in Redux. Thunk is simple async logic inside actions, while Saga uses generators for more complex and controlled async workflows.


๐Ÿง  Memory Trick

Thunk = simple async function  
Saga = advanced async controller

๐ŸŽฏ Interview Gold Sentence

"Redux middleware allows handling side effects before actions reach reducers. I use Thunk for simple async operations like API calls, while Saga is better for complex workflows requiring control over retries, cancellation, and orchestration of multiple async tasks."


๐Ÿง  Simple Definition (Word-for-word)

What does TanStack Query solve?


โšก Super Simple Line

Direct answer: TanStack Query (formerly React Query) solves server state management โ€” the specific pain of fetching, caching, syncing, and updating data that lives on a server but needs to be displayed in your UI.


โšก Key Details & Explanation

What does TanStack Query solve?

Direct answer: TanStack Query (formerly React Query) solves server state management โ€” the specific pain of fetching, caching, syncing, and updating data that lives on a server but needs to be displayed in your UI. It's not a replacement for client state tools like Redux/Zustand; it's a specialized layer for async data.

The problem it solves, concretely:

Before TanStack Query, a typical useEffect + fetch pattern forces you to manually handle:

function Todos() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch('/api/todos')
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);
  // ...
}

This looks fine until you need: caching (so navigating away and back doesn't refetch), deduping (two components requesting the same data shouldn't fire two network calls), background refetching, retry logic, pagination state, "is this data stale?" tracking, race-condition handling (slow request resolving after a faster newer one), and refetch-on-window-focus. You end up reinventing all of this by hand, badly, across every component.

TanStack Query gives you that out of the box:

function Todos() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('/api/todos').then(res => res.json()),
  });
  // done โ€” caching, dedup, retries, refetch-on-focus all included
}

Core mechanism: It maintains a global, normalized cache keyed by queryKey (an array, e.g. ['todos', { status: 'done' }]). Every component calling useQuery with the same key subscribes to the same cache entry โ€” so data is shared, requests are deduped, and updates in one place propagate to every consumer.

It also tracks each cache entry's status (fresh, stale, inactive) on a timer, which is the foundation for invalidation.


How cache invalidation works

Direct answer: Invalidation means marking cached data as "stale" so TanStack Query knows to refetch it โ€” either immediately (if a component is actively observing it) or on the next trigger (refocus, remount, etc.).

The mechanism, step by step:

  1. Every query result is stored under its queryKey with a timestamp and a staleTime.

  2. While data is within staleTime, it's considered fresh โ€” components get it instantly from cache, no network call.

  3. Once staleTime elapses, the data is stale โ€” still shown immediately (cache-first), but eligible for a background refetch on the next trigger (refocus, reconnect, remount).

  4. queryClient.invalidateQueries() manually forces this stale transition right now, and if there's an active observer (a mounted component using that query), it triggers an immediate refetch.

Most common pattern โ€” invalidating after a mutation:

const queryClient = useQueryClient();

const { mutate } = useMutation({
  mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
  onSuccess: () => {
    // mark 'todos' as stale โ†’ triggers refetch for active observers
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Note invalidateQueries does fuzzy/partial matching by default โ€” { queryKey: ['todos'] } invalidates ['todos'], ['todos', 1], ['todos', { status: 'done' }], etc. You can use exact: true to scope it down.

Other invalidation triggers besides manual calls:

  • refetchOnWindowFocus (default: true)

  • refetchOnReconnect

  • refetchOnMount if the data is stale

  • A refetchInterval for polling


Common follow-up questions

  • "What's the difference between invalidateQueries and setQueryData?" โ€” invalidateQueries marks data stale and triggers a refetch from the server. setQueryData directly writes into the cache without a network call โ€” used for optimistic updates or when a mutation response already gives you the fresh data, so you don't need a round trip.

  • "How do you do optimistic updates?" โ€” In onMutate, snapshot the old cache value, call setQueryData with the optimistic value, then in onError roll back using the snapshot, and in onSettled call invalidateQueries to reconcile with the server's actual state.

  • "What's staleTime vs cacheTime/gcTime?" โ€” staleTime controls how long data is considered fresh (no auto-refetch). gcTime (renamed from cacheTime in v5) controls how long unused (no active observers) data stays in memory before being garbage collected entirely. People conflate these constantly โ€” staleness is about refetch behavior, gc time is about memory cleanup.

  • "How does it handle race conditions?" โ€” Each query execution is tracked, and only the result matching the latest invocation gets written to cache, so a slow stale request resolving late won't overwrite newer data.

Gotchas

  • Invalidating doesn't clear the cache โ€” it keeps showing old data (no loading flicker) while refetching in the background. People expect a blank/loading state and are surprised it shows stale-then-fresh.

  • invalidateQueries only triggers an immediate refetch if there's a mounted, active observer for that key. If no component is currently subscribed, it just marks it stale for next time.

How to phrase it out loud

"TanStack Query manages server state โ€” caching, deduping, and syncing async data โ€” instead of forcing you to hand-roll loading/error/cache logic with useEffect. Invalidation works by marking a cached query's queryKey as stale; if there's an active component subscribed to that key, it triggers an immediate background refetch, otherwise it just refetches the next time something mounts or refocuses the window. The most common use is calling invalidateQueries after a mutation succeeds, so related data stays in sync without manual cache surgery."


โšก One-line Interview Answer

What does TanStack Query solve?

๐Ÿง  Simple Definition (Word-for-word)

Optimistic UI updates is a UX pattern where the client interface is updated immediately to reflect a successful action before receiving confirmation from the server. If the server request succeeds, the transaction is finalized; if it fails, the UI is rolled back to the previous stable state. This can be handled natively using React 19's useOptimistic hook or using TanStack Query (React Query) mutation callbacks.


โšก Super Simple Line

Optimistic Update = update UI instantly assuming success, and roll back if the server fails.


๐Ÿงช TanStack Query Implementation Example

import { useMutation, useQueryClient } from "@tanstack/react-query";

function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateTodoOnServer,
    // Step 1: When mutate is called
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches (so they don't overwrite our optimistic update)
      await queryClient.cancelQueries({ queryKey: ["todos"] });

      // Snapshot the previous value
      const previousTodos = queryClient.getQueryData(["todos"]);

      // Optimistically update the cache
      queryClient.setQueryData(["todos"], (old) =>
        old.map((todo) => (todo.id === newTodo.id ? { ...todo, ...newTodo } : todo))
      );

      // Return context with snapshotted value
      return { previousTodos };
    },
    // Step 2: If the mutation fails, rollback
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(["todos"], context.previousTodos);
    },
    // Step 3: Always refetch after success or error to sync database
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["todos"] });
    }
  });
}

๐Ÿงช Native React 19 useOptimistic Example

import { useOptimistic } from "react";

function TodoList({ todos, addTodoAction }) {
  const [optimisticTodos, setOptimisticTodos] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { id: Date.now(), text: newTodo, sending: true }]
  );

  return (
    <form action={async (formData) => {
      const text = formData.get("todo");
      setOptimisticTodos(text); // Trigger optimistic UI immediately
      await addTodoAction(text); // Call Server Action
    }}>
      <input name="todo" />
      <ul>
        {optimisticTodos.map(t => <li key={t.id}>{t.text} {t.sending && "(Sending...)"}</li>)}
      </ul>
    </form>
  );
}

โšก One-line Interview Answer

Optimistic updates temporarily update state cache instantly for a snappier user experience, restoring previous values in cache if the server request fails.

๐Ÿง  Simple Definition (Word-for-word)

CSR, SSR, SSG, and ISR represent different rendering models. CSR (Client-Side Rendering) loads a bare HTML file and renders UI entirely in the browser. SSR (Server-Side Rendering) renders HTML on the server on every request. SSG (Static Site Generation) pre-renders HTML once at build time. ISR (Incremental Static Regeneration) allows static pages to regenerate in the background at set intervals. In the Next.js App Router, rendering is component-centric, split between Server Components (default, zero client JS) and Client Components (opt-in via 'use client').


โšก Super Simple Line

CSR = Client builds DOM.
SSR = Server builds DOM per request.
SSG = Build server creates HTML at build time.
ISR = SSG page updates dynamically in background.


๐Ÿ“Š Rendering Comparison

StrategyNext.js App Router ImplementationBest Used For
CSRComponents marked with 'use client' containing standard fetchDynamic dashboards, interactive tools
SSRServer components using dynamic fetch: fetch(url, { cache: 'no-store' })User-specific live feeds, pages with frequently changing data
SSGDefault Server Components with static pagesBlogs, marketing sites, documentation pages
ISRServer components with revalidation: fetch(url, { next: { revalidate: 60 } })Product catalogs, blog index listings with frequent edits

โšก App Router vs Pages Router Mechanics

  • Pages Router: Tied to page level. You export page-level functions like getStaticProps or getServerSideProps to instruct Next.js how to compile the route.

  • App Router: Built on React Server Components (RSC). Data fetching is inline. You write async/await directly inside components, and caching configurations determine whether a component resolves as static or dynamic.


โšก One-line Interview Answer

Next.js App Router relies on React Server Components by default to serve statically pre-rendered HTML, utilizing inline fetch configurations to switch dynamically between SSR, SSG, and ISR at the component level.

๐Ÿง  Simple Definition (Word-for-word)

I'd rather verify the current specifics here than rely on memory โ€” Next.js routing internals are exactly the kind of thing that shifts between versions, and I want to give you accurate detail instead of stale facts.Good โ€” confirms the core architecture and surfaces one genuinely current detail worth knowing (the "use cache" directive shift in Next.js 16).


โšก Super Simple Line

Here's the structured answer: App Router vs Pages Router โ€” key architectural differences?


โšก Key Details & Explanation

I'd rather verify the current specifics here than rely on memory โ€” Next.js routing internals are exactly the kind of thing that shifts between versions, and I want to give you accurate detail instead of stale facts.Good โ€” confirms the core architecture and surfaces one genuinely current detail worth knowing (the "use cache" directive shift in Next.js 16). Here's the structured answer:

App Router vs Pages Router โ€” key architectural differences?

Direct answer: They're two different routing/rendering systems within Next.js. Pages Router (the original, pages/ directory) is page-centric โ€” each file is a route, and data fetching happens via special exported functions at the page level. App Router (app/ directory, introduced in Next.js 13) is component-centric, built around React Server Components, where rendering location (server vs client) and data fetching happen per-component rather than per-page.


Routing convention

Pages Router

App Router

Directory

pages/

app/

File โ†’ route

pages/about.js โ†’ /about

app/about/page.tsx โ†’ /about

Dynamic route

pages/blog/[slug].js

app/blog/[slug]/page.tsx

Special files

_app.js, _document.js

layout.tsx, loading.tsx, error.tsx, page.tsx

The App Router's special files are a meaningful shift: instead of one global _app.js wrapper, each route segment can have its own layout.tsx, loading.tsx (automatic Suspense boundary), and error.tsx (automatic error boundary) โ€” colocated with the route itself.


Rendering model โ€” the real architectural difference

Pages Router: Everything is a Client Component by default. The server renders HTML once, then ships the full component code as JS to hydrate. Data fetching is tied to the page via getServerSideProps, getStaticProps, or getInitialProps โ€” these functions couple data fetching to the route level, meaning you can't easily fetch data inside a specific component without prop-drilling it from the top-level page.

App Router: Components are Server Components by default. They render on the server, never ship their JS to the browser, and can fetch data directly inside the component itself โ€” no special exported function needed, just async/await in the component body. You opt into client-side interactivity explicitly with a 'use client' directive at the top of a file.

// Pages Router โ€” data fetching forced to page level
export async function getServerSideProps() {
  const data = await fetch('...');
  return { props: { data } };
}
export default function Page({ data }) { /* ... */ }

// App Router โ€” fetch directly inside any server component
export default async function Page() {
  const data = await fetch('...').then(r => r.json());
  return <div>{data.title}</div>;
}

This is the "biggest conceptual shift" cited consistently across sources โ€” it isn't the syntax, it's thinking in terms of the server/client boundary rather than page-level data fetching functions.


Layouts

Pages Router has no native nested layout system โ€” layouts are wrappers around the page Component in _app.js, which makes persistent nested layouts difficult to manage and preserve state like scroll position during navigation.

App Router makes layouts first-class and nested: a layout.tsx in any folder wraps everything beneath it and persists across navigations within that segment โ€” it renders once and doesn't re-render when the user navigates between child routes, e.g. a dashboard sidebar stays mounted while only the inner content swaps.


Streaming & loading states

App Router supports React Suspense natively per-segment โ€” you can show the page shell immediately and a loading skeleton for the main content while data fetches, and SaaS teams now commonly stream dashboard shells immediately while slower analytics widgets load progressively in parallel, which was significantly harder to implement cleanly in the Pages Router. Pages Router has no equivalent granular per-section streaming โ€” it's closer to all-or-nothing per page.


Bundle size / performance

Because Server Components never ship to the client, App Router apps can ship meaningfully less JS โ€” Server Components reduce the JavaScript sent to the browser, leading to faster load times and better Core Web Vitals.


Caching โ€” worth knowing if asked about Next.js 16 specifically

This is a genuinely recent and important shift: Cache Components replaced the old implicit caching model entirely in Next.js 16 โ€” caching is now opt-in via a "use cache" directive, the opposite of how it worked before. Before this, App Router's automatic/implicit fetch caching was widely cited as the most confusing part of the system โ€” most engineering teams struggled with caching behavior first, since multiple caching layers interacted differently depending on fetch settings, rendering mode, and navigation state, sometimes causing real bugs like e-commerce teams discovering stale inventory data because route caching persisted longer than expected.


Common follow-up questions

  • "Which would you pick for a new project today?" โ€” App Router is now the recommended standard as of Next.js 13's introduction and 14's stabilization, and most sources converge on App Router for new projects, Pages Router acceptable for legacy maintenance. But mention the real exception: apps heavily dependent on client-side/browser-API-bound libraries โ€” like Web3 wallet libraries (wagmi, ethers, web3.js) โ€” are deeply client-side, and the 'use client' boundary dance gets exhausting when 80% of the app depends on browser APIs, making Pages Router a reasonable pick there.

  • "Are there stability/security concerns with App Router?" โ€” Worth mentioning if asked: there was a notable CVE history around React Server Components in late 2025/early 2026 โ€” four rounds of security patches in three months targeting RSC deserialization โ€” and Pages Router was immune to all of them, which matters for compliance-sensitive industries like fintech/healthcare.

  • "Can you migrate incrementally?" โ€” Yes โ€” Next.js supports both routers in the same project; you can keep pages/ working while adding new routes in app/, and this is the generally recommended migration path rather than a big-bang rewrite.

  • "What's a Server Component vs a Client Component, mechanically?" โ€” Good follow-up to be ready for separately โ€” Server Components run only on the server and their output is serialized into the response; Client Components hydrate and run in the browser like classic React. The 'use client' directive marks the boundary, not just one component โ€” everything imported beneath it in the tree also becomes part of the client bundle.

Gotchas

  • Don't say "App Router is just SSR by default" โ€” Server Components are a distinct concept from SSR. SSR is about when HTML is generated; Server Components are about where code executes and whether it ships to the client at all. You can have Server Components that are statically generated, ISR'd, or dynamically rendered per-request.

  • If discussing caching, don't describe the old "implicit fetch caching" as current behavior without caveat โ€” it changed materially in Next.js 16's Cache Components model.

How to phrase it out loud

"Pages Router is page-centric โ€” every file is a route, and data fetching happens through special functions like getServerSideProps at the page level, which forces prop-drilling into child components. App Router is component-centric, built on React Server Components โ€” components render on the server by default, can fetch data directly inline with async/await, and never ship their JS to the client unless you opt in with 'use client'. App Router also gives you native nested layouts that persist across navigation, and per-segment streaming with Suspense, neither of which Pages Router has natively. The trade-off is that App Router's caching model is more powerful but historically more confusing โ€” Next.js 16 actually overhauled this to make caching opt-in via a use cache directive instead of implicit."


โšก One-line Interview Answer

I'd rather verify the current specifics here than rely on memory โ€” Next.js routing internals are exactly the kind of thing that shifts between versions, and I want to give you accurate detail instead of stale facts.Good โ€” confirms the core architecture and surfaces one genuinely current detail worth knowing (the "use cache" directive shift in Next.js 16).

๐Ÿง  Simple Definition (Word-for-word)

What are React Server Components?


โšก Super Simple Line

Direct answer: React Server Components (RSC) are components that render only on the server and never ship their JavaScript to the browser.


โšก Key Details & Explanation

What are React Server Components?

Direct answer: React Server Components (RSC) are components that render only on the server and never ship their JavaScript to the browser. The server runs them, produces a serialized output (not HTML, something richer โ€” more on that below), and sends that down to the client, where it gets stitched together with any Client Components to form the final UI. The defining trait: their code, dependencies, and any server-only logic (DB calls, secrets, file system access) never reach the browser bundle at all.

This is a different axis from SSR. SSR is about when HTML gets generated (per-request vs build-time). RSC is about where a component's code lives and executes, permanently โ€” it's not a one-time render-to-HTML-then-hydrate step, it's a category of component that has no client-side existence at all.


The core mechanism

In a framework like Next.js App Router, every component is a Server Component by default. You opt into client-side behavior explicitly:

// Server Component (default โ€” no directive needed)
async function ProductList() {
  const products = await db.query('SELECT * FROM products'); // runs ONLY on server
  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  );
}
// Client Component โ€” explicit opt-in
'use client';

function AddToCartButton({ productId }) {
  const [pending, setPending] = useState(false); // hooks need the client
  return <button onClick={() => addToCart(productId)}>Add to cart</button>;
}

You can mix them โ€” a Server Component can render a Client Component as a child, and pass it serializable props. But a Server Component cannot be imported into a Client Component's render tree directly (because by the time you're on the client, the server component's code doesn't exist there) โ€” instead it gets passed down as children from a parent Server Component.


Why this matters โ€” what problem it solves

  1. Zero bundle cost for server-only logic. A component that fetches from a database, reads a file, or uses a heavy parsing library never adds a single byte to the client JS bundle, because it's never sent there.

  2. Direct backend access, no API layer needed. You can await db.query() or call an internal service directly inside the component โ€” no need to build a REST/GraphQL endpoint just to shuttle data to the client.

  3. Secrets stay secret. API keys, DB credentials โ€” these can live in a Server Component's code path without any risk of leaking into client-visible JS, since that code never gets bundled for the browser.

  4. Composability with streaming. Because the server can resolve different components independently, slow data-fetching components can stream in progressively (paired with Suspense) without blocking the rest of the page.


Server Components vs Client Components โ€” decision table

Server Component

Client Component

Runs on

Server only

Server (for SSR) + browser (hydration)

Can use hooks (useState, useEffect)

โŒ No

โœ… Yes

Can access DB/filesystem/secrets directly

โœ… Yes

โŒ No

Adds to client JS bundle

โŒ No

โœ… Yes

Can handle interactivity (onClick, etc.)

โŒ No

โœ… Yes

Default in App Router

โœ… Yes

Needs 'use client'


What actually gets sent over the wire

This is a detail that signals real understanding if you bring it up: RSC doesn't serialize to HTML strings the way traditional SSR does. It serializes to a special RSC payload format โ€” a tree-like representation that describes which Client Components to mount where, with their props, interleaved with the already-rendered output of Server Components. The client-side React runtime reconstructs the UI from this payload, hydrating only the Client Component "islands" while leaving the Server Component output as-is (no hydration needed for those โ€” there's nothing to attach event listeners to since they have none).


Common follow-up questions

  • "Why can't Server Components use useState?" โ€” State and effects are inherently about a live, interactive instance running in a browser tab over time. A Server Component runs once, produces output, and is done โ€” there's no persistent instance on the server to hold state across renders (the server isn't tracking "your" component instance the way a browser is).

  • "How do Server and Client Components communicate?" โ€” Server โ†’ Client: via props (must be serializable โ€” no functions, no class instances, no Dates without conversion). Client โ†’ Server: via Server Actions (functions marked 'use server' that the client can invoke, which actually execute back on the server, like a built-in RPC mechanism).

  • "What's the catch / downside?" โ€” Mental model overhead is real. You now have to constantly think about which "side" a piece of code is allowed to run on, and a common beginner mistake is trying to use a hook or browser API inside a Server Component and getting a build error. Also, as noted in the App Router caching discussion, debugging the server/client boundary and caching interactions has been a genuine pain point for teams adopting this.

  • "Are Server Components the same as SSR?" โ€” No โ€” explicitly call this out if asked, since it's the most common conflation. SSR renders a full page to HTML on each (or build-time) request and then hydrates the whole thing. RSC is about which individual components execute server-side permanently, with zero client-side footprint, and composes with SSR/SSG/ISR rather than replacing them โ€” you can statically generate a page that's entirely built from Server Components, for example.

  • "What's a Server Action and how is it different from RSC?" โ€” RSC is about rendering on the server. Server Actions are about mutating โ€” they let a Client Component trigger a server-side function (e.g., a form submit calling a DB write) without manually building an API route.

Gotchas

  • A very common interview trap: someone says "Server Components are rendered to HTML and sent down" โ€” not quite right. They're rendered to the RSC payload format, which is richer than HTML (it encodes the tree structure with placeholders for Client Components), and that payload is what enables things like re-fetching just a sub-tree on navigation without a full page reload.

  • Forgetting that 'use client' marks a boundary, not a single file โ€” every component imported underneath that file (unless it itself starts a server boundary again via specific patterns) becomes part of the client bundle too.

How to phrase it out loud

"Server Components are components that run exclusively on the server and never ship their code to the browser at all โ€” not 'render once and hydrate,' but genuinely no client-side existence. That means they can directly access databases or secrets without an API layer, and they add zero bytes to the client bundle. You opt into client-side behavior โ€” state, effects, event handlers โ€” explicitly with a 'use client' directive, and that marks a boundary: everything imported below it becomes part of the client bundle too. It's a different axis from SSR โ€” SSR is about when HTML gets generated, RSC is about where a component's code is allowed to live and execute, period."


โšก One-line Interview Answer

What are React Server Components?

๐Ÿง  Simple Definition (Word-for-word)

How does Next.js handle code splitting?


โšก Super Simple Line

Direct answer: Next.js automatically splits your JavaScript bundle into smaller chunks so that each page (or route) only loads the code it actually needs, instead of shipping one giant bundle for the whole app.


โšก Key Details & Explanation

How does Next.js handle code splitting?

Direct answer: Next.js automatically splits your JavaScript bundle into smaller chunks so that each page (or route) only loads the code it actually needs, instead of shipping one giant bundle for the whole app. This happens largely without you writing any special code โ€” it's a build-time optimization layered on top of webpack/Turbopack, plus some explicit APIs for splitting beyond the automatic defaults.


The default behavior โ€” automatic route-based splitting

Every route gets its own JS chunk. If you have app/dashboard/page.tsx and app/settings/page.tsx, visiting /dashboard does not download the code for /settings. This is true for both Pages Router and App Router โ€” it's foundational to how Next.js has always worked.

.next/static/chunks/
  โ”œโ”€โ”€ pages/dashboard.js   โ† only loaded when visiting /dashboard
  โ”œโ”€โ”€ pages/settings.js    โ† only loaded when visiting /settings
  โ””โ”€โ”€ framework.js, main.js โ† shared runtime, loaded everywhere

Shared code (React itself, common utilities used across multiple routes) gets extracted into shared chunks so it's downloaded once and cached, rather than duplicated into every page's bundle.


Explicit code splitting โ€” next/dynamic

For splitting within a page โ€” e.g., a heavy component that isn't needed immediately (a modal, a chart library, a rich text editor) โ€” you use next/dynamic to lazy-load it:

import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false, // skip server-rendering this component entirely
});

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart /> {/* only fetched when this renders */}
    </div>
  );
}

This is the standard pattern for: chart libraries (Recharts, Chart.js), rich text editors, anything behind a modal/tab that isn't visible on initial load, or browser-only libraries that can't run during SSR (ssr: false is common here, e.g. for libraries touching window).


How this interacts with Server Components (App Router specific)

This is worth bringing up since it connects to the earlier RSC discussion โ€” Server Components add a second, more powerful layer of "splitting" on top of bundler-level code splitting: a Server Component's code never enters the client bundle at all, not even as a lazily-loaded chunk. It's not "split and loaded on demand" โ€” it's "never shipped, period." So in App Router apps, a lot of what used to require manual next/dynamic splitting (e.g., a component using a heavy server-side parsing library) is now solved structurally just by keeping that component a Server Component.

next/dynamic is still relevant in App Router, but mainly for Client Components you want to defer โ€” e.g., a client-side chart library that's below the fold.


Other splitting mechanisms worth knowing

  • Per-component dynamic imports without next/dynamic โ€” plain React.lazy + Suspense also works and is supported, though next/dynamic is the Next.js-idiomatic wrapper with extra features (SSR toggle, custom loading state).

  • Shared chunk deduplication โ€” webpack's SplitChunksPlugin (used under the hood) automatically groups modules used by multiple pages into shared chunks so common dependencies aren't duplicated per-route.

  • Third-party script loading โ€” next/script lets you control when third-party scripts load (beforeInteractive, afterInteractive, lazyOnload), which isn't code splitting in the bundler sense but solves the same underlying goal: don't block initial load with code you don't need yet.


Common follow-up questions

  • "When would you use next/dynamic vs just relying on automatic splitting?" โ€” Automatic splitting handles the page-boundary case for free. You reach for next/dynamic when a single page contains something heavy that isn't needed for the initial render โ€” e.g., a modal that's closed by default, or a library only needed after a user interaction.

  • "What does ssr: false actually do and when do you need it?" โ€” It tells Next.js to skip rendering that component on the server entirely, rendering it only in the browser. You need this for components that depend on browser-only APIs (window, document, localStorage) that would throw or behave incorrectly during server rendering.

  • "How do you verify your code splitting is actually working?" โ€” next build outputs a per-route bundle size table in the terminal, and the @next/bundle-analyzer plugin gives a visual treemap of what's in each chunk โ€” useful for spotting an accidentally-bloated shared chunk (e.g., a huge library imported at the top level that ends up in every page).

  • "Does code splitting affect SEO or initial load negatively?" โ€” Generally it helps โ€” smaller initial JS payload means faster Time-to-Interactive. The risk is over-fragmenting: too many tiny chunks means more round trips, so there's a balance (which Next.js's bundler defaults are already tuned for).

Gotchas

  • A subtle one: next/dynamic with ssr: false means that component contributes nothing to the server-rendered HTML โ€” if you need it for SEO-relevant content, that's the wrong tool.

  • Importing a large library at the top of a shared layout or _app.js/root layout defeats route-based splitting โ€” it gets pulled into the shared chunk and loaded on every single page, even ones that don't use it. This is one of the most common real-world bundle bloat bugs.

  • Conflating "lazy loading a component" with "Server Component never shipping" โ€” they solve overlapping problems but are mechanically different: one is deferred-but-eventually-downloaded, the other is never-downloaded-at-all.

How to phrase it out loud

"Next.js automatically splits JS per route, so visiting one page doesn't download the code for every other page โ€” that's free, no setup needed. For splitting within a page, like a heavy modal or chart library you don't need on initial render, you use next/dynamic to lazy-load it, optionally skipping SSR for browser-only code. In the App Router, Server Components add a more aggressive layer on top of that โ€” their code never enters the client bundle at all, not even as a deferred chunk โ€” so a lot of what used to require manual splitting is now solved just by keeping a component server-only."


โšก One-line Interview Answer

How does Next.js handle code splitting?

๐Ÿง  Simple Definition (Word-for-word)

I want to make sure I'm giving you current Core Web Vitals metrics and Next.js-specific tooling rather than relying on memory, since both the metric set and the framework APIs shift over time.Good, this confirms the official thresholds (LCP โ‰ค2.5s, INP โ‰ค200ms, CLS โ‰ค0.1 โ€” note one source incorrectly claimed a 2.0s LCP bar, but Google's own docs confirm 2.5s is unchanged) and gives current Next.js-specific tooling.


โšก Super Simple Line

Here's the structured answer: How would you optimize Core Web Vitals in a Next.js app?


โšก Key Details & Explanation

I want to make sure I'm giving you current Core Web Vitals metrics and Next.js-specific tooling rather than relying on memory, since both the metric set and the framework APIs shift over time.Good, this confirms the official thresholds (LCP โ‰ค2.5s, INP โ‰ค200ms, CLS โ‰ค0.1 โ€” note one source incorrectly claimed a 2.0s LCP bar, but Google's own docs confirm 2.5s is unchanged) and gives current Next.js-specific tooling. Here's the structured answer:

How would you optimize Core Web Vitals in a Next.js app?

Direct answer: Core Web Vitals are three metrics โ€” Largest Contentful Paint (LCP) measuring loading performance, Interaction to Next Paint (INP) measuring responsiveness, and Cumulative Layout Shift (CLS) measuring visual stability โ€” and Next.js gives you built-in primitives for each, but the framework defaults only get you partway; the rest is architectural discipline. Google's recommended thresholds are LCP under 2.5 seconds, INP under 200 milliseconds, and CLS under 0.1, evaluated at the 75th percentile of real visitor data โ€” meaning 75% of your actual users need a good experience, not just your fastest test run.


1. LCP โ€” Largest Contentful Paint

This is statistically the hardest one to pass โ€” on mobile, only about 62% of pages achieve a good LCP, versus 77% for INP and 81% for CLS, so it's usually where you'll find the most leverage.

Next.js-specific fixes:

  • next/image โ€” automatically generates responsive sizes, serves WebP/AVIF, adds width/height for CLS prevention, and lazy-loads below-the-fold images. Critically: do not lazy-load the actual LCP element (usually a hero image) โ€” give it priority instead, which maps to preloading the LCP image with fetchpriority="high".

  • next/font โ€” self-hosts Google Fonts, generates fallback metrics automatically, and applies font-display swap, avoiding the render-blocking external font request.

  • Rendering strategy โ€” static generation gives pages near-zero TTFB from CDN edge, giving LCP a massive head start. If a page's data doesn't need to be request-fresh, prefer SSG/ISR over SSR purely for this reason.

  • Reduce TTFB generally โ€” reduce server response time with caching, a CDN, and efficient rendering; for SSR pages this means watching what blocks the response (e.g., a slow DB query awaited before any HTML streams).

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // tells the browser this is the LCP candidate โ€” don't lazy load
/>

2. INP โ€” Interaction to Next Paint

This is the metric most sites fail and the one requiring the deepest technical changes โ€” 43% of sites still fail the 200ms threshold. Unlike its predecessor FID, INP captures responsiveness across every interaction in a session, not just the first one, so a page that feels snappy on first click but janky on the fifth still fails.

Root cause, almost always: JavaScript blocking the main thread when the user interacts.

Next.js-specific fixes:

  • Server Components โ€” reduce client-side JavaScript, lowering INP by reducing main thread work during interactions. This connects directly back to the RSC discussion: less hydrated/interactive JS on the page means less for the main thread to chew through when a user clicks something.

  • Code splitting (next/dynamic) โ€” defers non-critical interactive widgets so they don't compete for main-thread time during initial interaction windows.

  • Break up long tasks โ€” break up long tasks into smaller chunks to avoid blocking the thread, e.g., chunking expensive client-side computation with setTimeout/scheduler APIs so the browser can paint between chunks.

  • Minimize work inside event handlers themselves โ€” defer non-essential logic (analytics calls, secondary state updates) out of the critical interaction path.

3. CLS โ€” Cumulative Layout Shift

The easiest of the three to fix mechanically โ€” CLS has the highest pass rate because explicit dimensions are a straightforward fix.

Next.js-specific fixes:

  • next/image again โ€” it adds width/height for CLS prevention automatically, so as long as you pass width/height (or use fill with a sized parent), you avoid the classic "image pops in and shoves content down" shift.

  • next/font again โ€” zero CLS from font loading, since it computes fallback metrics that match the real font's size, avoiding the reflow when a web font swaps in.

  • Generally: every image, video, iframe, and ad slot needs explicit width and height attributes, and reserve space for any dynamically-injected content (banners, ads, late-loading widgets) before it arrives.


How to measure and validate

  • Field data over lab data: Lighthouse scores do not directly affect Core Web Vitals โ€” Google uses real user data from CrUX to evaluate them, so a great Lighthouse score doesn't guarantee a passing CrUX assessment. Use Lighthouse for diagnosing, but validate against real user data (Search Console's Core Web Vitals report, or your own RUM/analytics) for truth.

  • All three must pass simultaneously: a URL group only gets a "Good" status when at least 75% of visits meet the Good threshold for all three metrics simultaneously โ€” fixing two out of three doesn't help if the third is still poor.

  • Regression detection: set alerts at 80% of Google's thresholds โ€” INP > 160ms, LCP > 2.0s, CLS > 0.08 โ€” so you catch a regression before it actually crosses into "poor" territory in production.


Common follow-up questions

  • "Why is INP harder to fix than LCP or CLS?" โ€” Unlike LCP issues, which are often fixed by compressing an image or enabling a cache, fixing INP requires major changes to JavaScript architecture, since you need to rethink how your code handles user events rather than just optimizing file size. It's a structural problem (how much JS runs and when), not a swap-this-asset problem.

  • "How does the App Router specifically help here versus Pages Router?" โ€” Tie back to the RSC conversation: less client JS by default (helps INP), built-in streaming/Suspense for progressive rendering (helps perceived LCP even when actual data is slow), but the caching complexity discussed earlier can also hurt LCP if misconfigured (e.g., serving stale-but-fast content is fine for LCP itself, but a caching bug causing unexpectedly slow uncached paths would hurt TTFB).

  • "What's the business case for caring about this?" โ€” Worth having a number ready: for every second of delay beyond the 2.5-second LCP threshold, bounce rates increase by 32%, and a one-second delay in load time reduces conversions by 7%. Frames it as a product/revenue issue, not just a dev nicety โ€” useful for an interview that wants to see business awareness, not just technical trivia.

  • "What page types tend to fail which metric?" โ€” Blog posts and homepages most commonly fail LCP due to image issues, product/category pages tend to struggle with CLS from dynamic content loading, and INP failures concentrate on pages with heavy JS interactions โ€” checkouts, landing pages with forms, filter-heavy listing pages. Good to mention if asked to prioritize across a real site.

Gotchas

  • Don't quote LCP's threshold as 2.0 seconds โ€” that's a claim circulating in some lower-quality sources, but Google's own documentation is explicit that the LCP good threshold is 2,500ms, and a claim that it dropped to 2.0s in 2026 is wrong.

  • Lighthouse โ‰  Core Web Vitals pass/fail โ€” conflating lab data with field data is a common interview-answer mistake.

  • priority on more than one image defeats its own purpose โ€” it should be reserved for the actual LCP candidate, not applied broadly "just in case."

How to phrase it out loud

"For LCP, I'd lean on next/image with priority on the hero element instead of lazy-loading it, use next/font to avoid render-blocking font requests, and prefer static generation or ISR over SSR where data freshness allows, since that gets TTFB near-zero from the CDN edge. For INP, the big lever is reducing client-side JS โ€” Server Components help structurally here โ€” plus code-splitting non-critical interactive widgets and breaking up long tasks so the main thread isn't blocked during a user interaction. For CLS, it's mostly discipline: explicit width/height on every image and embed, and next/image/next/font handle most of that automatically. I'd validate against real user field data โ€” Search Console's CrUX report โ€” rather than trusting Lighthouse alone, since Google's ranking signal is based on the 75th percentile of actual visits, not lab scores."


โšก One-line Interview Answer

I want to make sure I'm giving you current Core Web Vitals metrics and Next.js-specific tooling rather than relying on memory, since both the metric set and the framework APIs shift over time.Good, this confirms the official thresholds (LCP โ‰ค2.5s, INP โ‰ค200ms, CLS โ‰ค0.1 โ€” note one source incorrectly claimed a 2.0s LCP bar, but Google's own docs confirm 2.5s is unchanged) and gives current Next.js-specific tooling.

๐Ÿง  Simple Definition (Word-for-word)

export const revalidate = 60 in a route tells Next.js to regenerate the page in the background if a request comes in after 60 seconds.


โšก Super Simple Line

On-demand revalidation: call revalidatePath('/blog/slug') or revalidateTag('posts') inside a Route Handler or Server Action โ€” useful for CMS webhooks so pages update immediately when content changes without a full redeploy.


โšก Key Details & Explanation

export const revalidate = 60 in a route tells Next.js to regenerate the page in the background if a request comes in after 60 seconds. On-demand revalidation: call revalidatePath('/blog/slug') or revalidateTag('posts') inside a Route Handler or Server Action โ€” useful for CMS webhooks so pages update immediately when content changes without a full redeploy. In a real React or Next.js app, this matters because it affects rendering behavior, user experience, and performance. A strong answer should show where the code runs, when the UI updates, and what mistake could cause extra renders or broken hydration.


โšก One-line Interview Answer

export const revalidate = 60 in a route tells Next.js to regenerate the page in the background if a request comes in after 60 seconds.

๐Ÿง  Simple Definition (Word-for-word)

Bhai, think of Next.js Middleware as a proxy layer in front of your app .


โšก Super Simple Line

Request flow: Browser Request โ†“ Middleware โ†“ Route Matching โ†“ Page / Layout / Route Handler โ†“ Response Middleware runs before Next.js decides which page or API route to execute .


โšก Key Details & Explanation

Bhai, think of Next.js Middleware as a proxy layer in front of your app.

Request flow:

Browser Request
      โ†“
Middleware
      โ†“
Route Matching
      โ†“
Page / Layout / Route Handler
      โ†“
Response

Middleware runs before Next.js decides which page or API route to execute. It can:

  • Redirect

  • Rewrite URLs

  • Check authentication

  • Set headers

  • Set cookies

  • Block requests

  • Continue the request unchanged (Next.js)

Example:

// middleware.ts

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("token");

  if (!token) {
    return NextResponse.redirect(
      new URL("/login", request.url)
    );
  }

  return NextResponse.next();
}

When someone visits:

/dashboard

Next.js does:

Request /dashboard
      โ†“
middleware()
      โ†“
Has token?
   โ”œโ”€ Yes โ†’ render dashboard
   โ””โ”€ No  โ†’ redirect /login

A very common use case:

export const config = {
  matcher: ["/dashboard/:path*"],
};

Now middleware runs only for dashboard routes. (Next.js)

Another example, URL rewriting:

export function middleware(request: NextRequest) {
  return NextResponse.rewrite(
    new URL("/maintenance", request.url)
  );
}

User visits:

/

but actually receives:

/maintenance

while the browser URL stays /. (Next.js)

One important detail:

Middleware โ‰  Express middleware

It is not running inside your React tree. It runs before routing, often at the Edge runtime, acting more like a network proxy in front of your application. (Next.js)

For interviews, the concise answer is:

Next.js Middleware is code that executes before route matching and rendering. It intercepts incoming requests and can redirect, rewrite, modify headers/cookies, perform authentication checks, or return a response before the request reaches a page or route handler. (Next.js)


โšก One-line Interview Answer

Bhai, think of Next.js Middleware as a proxy layer in front of your app .

๐Ÿง  Simple Definition (Word-for-word)

Server Actions are async functions marked with 'use server' that run on the server when called from a client component โ€” typically from form submissions or event handlers.


โšก Super Simple Line

They replace API routes for simple mutations.


โšก Key Details & Explanation

Server Actions are async functions marked with 'use server' that run on the server when called from a client component โ€” typically from form submissions or event handlers. They replace API routes for simple mutations. They can directly access the database or ORM. Under the hood, they're a POST request to the same URL with a special action ID header. Support progressive enhancement โ€” forms work without JS. In a real React or Next.js app, this matters because it affects rendering behavior, user experience, and performance. A strong answer should show where the code runs, when the UI updates, and what mistake could cause extra renders or broken hydration.
When user submits:

1. Browser submits form
2. POST request sent to Server Action
3. addTodo() runs on server
4. Database updated
5. revalidatePath("/") invalidates cache
6. Next.js re-renders affected Server Components
7. New RSC payload returned
8. React merges payload into existing UI
9. Browser updates DOM

โšก One-line Interview Answer

Server Actions are async functions marked with 'use server' that run on the server when called from a client component โ€” typically from form submissions or event handlers.

๐Ÿง  Simple Definition (Word-for-word)

Hydration errors happen when the HTML generated on the server does not match what React renders on the client during the first render.


โšก Super Simple Line

In Next.js, the server sends HTML first, and then React attaches event handlers and makes it interactive in the browser.


โšก Key Details & Explanation

Hydration errors happen when the HTML generated on the server does not match what React renders on the client during the first render. In Next.js, the server sends HTML first, and then React attaches event handlers and makes it interactive in the browser. If the server output and client output are different, React warns about a hydration mismatch. Common causes are using Date.now(), Math.random(), browser-only APIs like window or localStorage, or rendering different content based on client-only state during the initial render. The fix is to make the first render deterministic and move browser-only logic into useEffect or a Client Component where appropriate. This matters because hydration problems can cause broken UI, incorrect content, or components that behave differently in production than in development.


โšก One-line Interview Answer

Hydration errors happen when the HTML generated on the server does not match what React renders on the client during the first render.

๐Ÿง  Simple Definition (Word-for-word)

Colocate state when only one component or a small subtree needs it.


โšก Super Simple Line

Lift state up when multiple sibling components need to read or update the same source of truth.


โšก Key Details & Explanation

Colocate state when only one component or a small subtree needs it. Lift state up when multiple sibling components need to read or update the same source of truth. Rule of thumb: keep state as close as possible to where it is used, and only move it upward when coordination is necessary. Over-lifting state causes extra re-renders and more complex prop drilling; over-colocating creates duplicated state and synchronization bugs. In a real React or Next.js app, this matters because it affects rendering behavior, user experience, and performance. A strong answer should show where the code runs, when the UI updates, and what mistake could cause extra renders or broken hydration.


โšก One-line Interview Answer

Colocate state when only one component or a small subtree needs it.

๐Ÿง  Simple Definition (Word-for-word)

Accessible Forms Labels โ€” always bind to inputs // Bad <input type="email" placeholder="Email" /> // Good <label htmlFor="email">Email</label> <input id="email" type="email" aria-describedby="email-error" /> Error messages โ€” announce to screen reader <input id="email" aria-invalid={!!error} aria-describedby="email-error" /> {error && ( <span id="email-error" role="alert"> {error} </span> )} role="alert" makes screen reader announce error on inject.


โšก Super Simple Line

No need user focus it.


โšก Key Details & Explanation

Accessible Forms

Labels โ€” always bind to inputs

// Bad
<input type="email" placeholder="Email" />

// Good
<label htmlFor="email">Email</label>
<input id="email" type="email" aria-describedby="email-error" />

Error messages โ€” announce to screen reader

<input
  id="email"
  aria-invalid={!!error}
  aria-describedby="email-error"
/>
{error && (
  <span id="email-error" role="alert">
    {error}
  </span>
)}

role="alert" makes screen reader announce error on inject. No need user focus it.

Fieldsets for grouped inputs

<fieldset>
  <legend>Notification preferences</legend>
  <label><input type="checkbox" name="email" /> Email</label>
  <label><input type="checkbox" name="sms" /> SMS</label>
</fieldset>

Required fields

<input required aria-required="true" />

Both. required triggers browser validation. aria-required tells screen readers.


Accessible Modals

Focus trap โ€” critical

On open: move focus into modal. On close: return focus to trigger.

import { useEffect, useRef } from "react";

function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);
  const triggerRef = useRef(null); // pass from parent

  useEffect(() => {
    if (isOpen) {
      // store trigger, focus first focusable inside modal
      const focusable = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      focusable?.[0]?.focus();
    }
  }, [isOpen]);

  // Return focus on close handled by caller restoring triggerRef.current.focus()
}

Use @radix-ui/react-dialog or react-aria โ€” they solve focus trap already. Don't reinvent.

ARIA roles

<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-desc"
>
  <h2 id="modal-title">Confirm delete</h2>
  <p id="modal-desc">This action cannot be undone.</p>
</div>

aria-modal="true" hides background content from screen reader virtual cursor.

Escape key

useEffect(() => {
  const handler = (e) => e.key === "Escape" && onClose();
  document.addEventListener("keydown", handler);
  return () => document.removeEventListener("keydown", handler);
}, [onClose]);

Block scroll + background interaction

// Scroll lock
useEffect(() => {
  if (isOpen) document.body.style.overflow = "hidden";
  return () => { document.body.style.overflow = ""; };
}, [isOpen]);

// Backdrop click closes
<div role="presentation" onClick={onClose} /> // backdrop

Quick Checklist

Thing

Must have

Form inputs

id + htmlFor bound

Errors

role="alert" + aria-describedby

Modal

role="dialog" + aria-modal + focus trap

Escape

closes modal

Focus

returns to trigger on close

Background

aria-hidden="true" when modal open


Best path โ€” use proven lib

  • Radix UI โ€” headless, full a11y, zero style lock-in

  • React Aria โ€” Adobe's lib, WCAG 2.1 AA out of box

  • Headless UI โ€” Tailwind team, solid

Build custom only if lib blocked. Focus trap + ARIA attributes hard to get right edge cases.


โšก One-line Interview Answer

Accessible Forms Labels โ€” always bind to inputs // Bad <input type="email" placeholder="Email" /> // Good <label htmlFor="email">Email</label> <input id="email" type="email" aria-describedby="email-error" /> Error messages โ€” announce to screen reader <input id="email" aria-invalid={!!error} aria-describedby="email-error" /> {error && ( <span id="email-error" role="alert"> {error} </span> )} role="alert" makes screen reader announce error on inject.

๐Ÿง  Simple Definition (Word-for-word)

React Fiber is a re-implementation of the React core algorithm that allows for incremental rendering.


โšก Super Simple Line

It breaks down rendering work into smaller units and prioritizes them based on their importance.


โšก Key Details & Explanation

React Fiber is a re-implementation of the React core algorithm that allows for incremental rendering. It breaks down rendering work into smaller units and prioritizes them based on their importance. This way, it can pause and resume work as needed, which improves performance and responsiveness, especially for complex applications.


โšก One-line Interview Answer

React Fiber is a re-implementation of the React core algorithm that allows for incremental rendering.

๐Ÿง  Simple Definition (Word-for-word)

useState is simpler and good for basic state management.


โšก Super Simple Line

useReducer is more powerful and better for complex state logic or when the next state depends on the previous one.


โšก Key Details & Explanation

  • useState is simpler and good for basic state management.
  • useReducer is more powerful and better for complex state logic or when the next state depends on the previous one.

โšก One-line Interview Answer

useState is simpler and good for basic state management.