๐ง 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)
useRefis a React hook that returns a mutable reference object whosecurrentproperty persists across renders. UnlikeuseState, changing a ref'scurrentvalue 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
| Feature | useRef | useState |
|---|---|---|
| Triggers Re-render? | โ No | โ Yes |
| Accesses DOM? | โ Yes (via ref attribute) | โ No (virtual representation) |
| Usage Scope | Storing timer IDs, DOM nodes, previous state values | Form input value, server data, toggle states |
| Updates | Synchronous (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)
useCallbackmemoizes a function definition to prevent it from being recreated on every render.useMemomemoizes the computed result of an expensive calculation.React.memois 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 + binuseMemois 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 oncount, writesetCount(prev => prev + 1)to removecountfrom 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
useEffectruns after the browser paints the screen, whileuseLayoutEffectruns 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, notrender.
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:
Every query result is stored under its
queryKeywith a timestamp and astaleTime.While data is within
staleTime, it's considered fresh โ components get it instantly from cache, no network call.Once
staleTimeelapses, the data is stale โ still shown immediately (cache-first), but eligible for a background refetch on the next trigger (refocus, reconnect, remount).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)refetchOnReconnectrefetchOnMountif the data is staleA
refetchIntervalfor polling
Common follow-up questions
"What's the difference between
invalidateQueriesandsetQueryData?" โinvalidateQueriesmarks data stale and triggers a refetch from the server.setQueryDatadirectly 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, callsetQueryDatawith the optimistic value, then inonErrorroll back using the snapshot, and inonSettledcallinvalidateQueriesto reconcile with the server's actual state."What's
staleTimevscacheTime/gcTime?" โstaleTimecontrols how long data is considered fresh (no auto-refetch).gcTime(renamed fromcacheTimein 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.
invalidateQueriesonly 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
queryKeyas 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 callinginvalidateQueriesafter 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
useOptimistichook 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
| Strategy | Next.js App Router Implementation | Best Used For |
|---|---|---|
| CSR | Components marked with 'use client' containing standard fetch | Dynamic dashboards, interactive tools |
| SSR | Server components using dynamic fetch: fetch(url, { cache: 'no-store' }) | User-specific live feeds, pages with frequently changing data |
| SSG | Default Server Components with static pages | Blogs, marketing sites, documentation pages |
| ISR | Server 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
getStaticPropsorgetServerSidePropsto instruct Next.js how to compile the route.App Router: Built on React Server Components (RSC). Data fetching is inline. You write
async/awaitdirectly 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 |
|
|
File โ route |
|
|
Dynamic route |
|
|
Special files |
|
|
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 inapp/, 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
getServerSidePropsat 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 ause cachedirective 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
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.
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.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.
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 ( | โ No | โ Yes |
Can access DB/filesystem/secrets directly | โ Yes | โ No |
Adds to client JS bundle | โ No | โ Yes |
Can handle interactivity ( | โ No | โ Yes |
Default in App Router | โ Yes | Needs |
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โ plainReact.lazy+Suspensealso works and is supported, thoughnext/dynamicis 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/scriptlets 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/dynamicvs just relying on automatic splitting?" โ Automatic splitting handles the page-boundary case for free. You reach fornext/dynamicwhen 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: falseactually 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 buildoutputs a per-route bundle size table in the terminal, and the@next/bundle-analyzerplugin 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/dynamicwithssr: falsemeans 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/dynamicto 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 itpriorityinstead, which maps to preloading the LCP image withfetchpriority="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/imageagain โ it adds width/height for CLS prevention automatically, so as long as you passwidth/height(or usefillwith a sized parent), you avoid the classic "image pops in and shoves content down" shift.next/fontagain โ 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.
priorityon 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/imagewithpriorityon the hero element instead of lazy-loading it, usenext/fontto 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, andnext/image/next/fonthandle 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 |
|
Errors |
|
Modal |
|
Escape | closes modal |
Focus | returns to trigger on close |
Background |
|
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.