React Interview Questions
React & frontend interview guide — 391 unique questions with theory, examples, and interview-ready answers across hooks, rendering, state, performance, JavaScript, and architecture.
Introduction
This guide merges 391 unique interview questions from 23 preparation notes into one reference — React fundamentals, hooks, rendering, state management, performance, JavaScript, TypeScript, Next.js, architecture, and behavioral rounds.
Questions are grouped into 14 topic areas, deduplicated by title, and stripped of company-specific references. Each entry keeps theory, trade-offs where relevant, concise interview answers, and practical code examples.
Quick index
React Fundamentals
Hooks
Rendering & Reconciliation
State Management
Routing & Navigation
API & Data Fetching
Performance
Architecture & Patterns
JavaScript Core
TypeScript
| # | Section |
|---|---|
| 236 | How does extends work in TypeScript and what is the difference between type and interface? |
| 237 | TypeScript in Large Codebases |
| 238 | Why do we use TypeScript? |
Next.js
Testing & Tooling
| # | Section |
|---|---|
| 241 | How would you implement A/B testing in a Next.js application? |
| 242 | Testing — RTL, Jest, Playwright |
| 243 | Webpack |
| 244 | What is Snapshot Testing? |
Behavioral & Soft Skills
General & Advanced
React Fundamentals
1. 50 components, one context — re-renders
Theory
When a Context value changes, every component that calls useContext for that context re-renders — regardless of whether it uses the part that changed.
50 components sharing one context → all 50 re-render on any context update.
This is why a fintech app asks this — they want you to know Context is not a free global store.
Pros & Cons
| Single large context | Split contexts / Zustand selectors |
|---|---|
| ❌ 50 re-renders on any change | ✅ Only subscribers to changed slice re-render |
| ✅ Simple setup | Slightly more setup |
| ❌ Performance death at scale | ✅ Scales to a fintech app-level apps |
Real-Life Example
// ❌ One context — 50 components all re-render when theme OR user changes
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("dark");
const [notifications, setNotifications] = useState([]);
// New object every render → ALL consumers re-render
const value = { user, setUser, theme, setTheme, notifications, setNotifications };
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// 50 components:
function Header() { const { user } = useContext(AppContext); ... }
function Sidebar() { const { theme } = useContext(AppContext); ... }
function NotifBell() { const { notifications } = useContext(AppContext); ... }
// theme changes → Header, Sidebar, NotifBell ALL re-render even if they don't use theme// ✅ FIX 1 — Split contexts
const UserContext = createContext()
const ThemeContext = createContext()
// theme change → only ThemeContext consumers re-render
// ✅ FIX 2 — Memoize value
const userValue = useMemo(() => ({ user, setUser }), [user])
// ✅ FIX 3 — Zustand with selectors
const theme = useStore((s) => s.theme) // only re-renders when theme changes
// ✅ FIX 4 — React 19 use(Context) + smaller providers per routeInterview answer (concise)
2. Component Performance — LCP, CLS, INP
Theory
Core Web Vitals are Google's user-centric metrics. They directly affect SEO, conversion, and how users perceive your app.
| Metric | What it measures | Good threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | When main content appears | ≤ 2.5s |
| CLS (Cumulative Layout Shift) | Visual stability — unexpected jumps | ≤ 0.1 |
| INP (Interaction to Next Paint) | Responsiveness to user input (replaced FID) | ≤ 200ms |
Other important: FCP (First Contentful Paint), TTFB (Time to First Byte), TBT (Total Blocking Time).
Pros & Cons — Optimization Approaches
| Approach | Helps | Trade-off |
|---|---|---|
| Code splitting | LCP, TBT | More chunks to manage |
| Image optimization (next/image, WebP) | LCP | Build pipeline complexity |
| Font preload + font-display: swap | LCP, CLS | FOUT if not tuned |
| Skeleton + reserved dimensions | CLS | Extra CSS |
| defer non-critical JS | TBT, INP | Delayed interactivity |
| Virtualization | INP on long lists | Scroll complexity |
LCP Optimization
// ❌ LCP killer — unoptimized hero image, render-blocking font
<img src="/hero-4mb.jpg" />
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />
// ✅ Optimized
import Image from "next/image";
<Image
src="/hero.webp"
alt="Product showcase"
priority // preload LCP candidate
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, 1200px"
/>
// Preload LCP image in <head>
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />CLS Prevention
// ❌ CLS — image loads, page jumps
<img src={product.image} />
// ✅ Reserve space
<div style={{ aspectRatio: "16/9", width: "100%" }}>
<img src={product.image} width={640} height={360} alt={product.name} />
</div>
// ✅ Skeleton with fixed height
function ProductCardSkeleton() {
return <div className="card-skeleton" style={{ height: 320 }} />;
}/* Font loading — prevent invisible text then swap jump */
@font-face {
src: url('/fonts/inter.woff2') format('woff2');
font-family: 'Inter';
font-display: swap; /* or optional for less CLS */
}INP Optimization
import { useTransition, memo, useDeferredValue } from 'react'
// Defer expensive filter on search
function ProductSearch({ products }) {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const filtered = useMemo(() => filterProducts(products, deferredQuery), [products, deferredQuery])
return (
<>
<input onChange={(e) => setQuery(e.target.value)} />
<ProductList products={filtered} />
</>
)
}
// Break up long tasks
function handleHeavyClick() {
requestIdleCallback(() => processAnalytics(data))
startTransition(() => setResults(computeExpensive()))
}3. Components in React
Theory
A component is an independent, reusable piece of UI. Components accept props, manage state, and return JSX. They can be nested — <App> contains <Header>, <Sidebar>, <Main>.
Two types: function components (modern standard) and class components (legacy).
Pros & Cons
| Small focused components | Large monolithic components |
|---|---|
| ✅ Easy to test and reuse | ❌ Hard to maintain |
| ✅ Clear responsibility | ❌ Bug in one place breaks all |
Real Example
function App() {
return (
<>
<Header />
<ProductList />
<Footer />
</>
)
}4. Controlled vs Uncontrolled Components
Theory
| Controlled | Uncontrolled | |
|---|---|---|
| Value source | React state | DOM itself |
| Updates | onChange + setState | ref.current.value |
| Validation | Real-time in React | On submit only |
| Preferred? | Yes — React standard | Legacy, file inputs |
Real Example
// Controlled — preferred
function LoginForm() {
const [email, setEmail] = useState('')
return <input value={email} onChange={(e) => setEmail(e.target.value)} />
}
// Uncontrolled — ref on submit
function LoginFormUncontrolled() {
const emailRef = useRef(null)
const submit = () => console.log(emailRef.current.value)
return <input ref={emailRef} defaultValue="" />
}5. Difference between Server Components and Client Components
Theory
| Server Component | Client Component | |
|---|---|---|
| Directive | Default in App Router | "use client" at top of file |
| Runs on | Server only | Server (SSR) + Browser |
| JS sent to browser | None for RSC itself | Full component bundle |
Can use hooks (useState) | No | Yes |
| Can use browser APIs | No | Yes |
| Can access DB/secrets | Yes | No |
| Can import Client Components | Yes (as children) | Yes |
| Can import Server Components | No | No (only receive as props/children) |
Composition pattern
// app/page.tsx — Server Component
import ClientCounter from './ClientCounter'
export default async function Page() {
const data = await fetchFromDB() // runs on server
return (
<main>
<h1>Dashboard</h1>
<ClientCounter initialCount={data.count} />
</main>
)
}// ClientCounter.tsx
'use client'
import { useState } from 'react'
export default function ClientCounter({ initialCount }) {
const [count, setCount] = useState(initialCount)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}When to use each
- Server: Static content, data fetching, heavy libraries, SEO-critical markup
- Client: Interactivity, effects, browser APIs, animations, form state
Interview answer
6. Explain React Server Components and their advantages
Theory
React Server Components (RSC) run only on the server. They never ship their JavaScript to the client. They can directly access backend resources (DB, file system, secrets) and pass rendered output or serializable props to Client Components.
RSC vs SSR vs Client Components
| Server Component | SSR (traditional) | Client Component | |
|---|---|---|---|
| Runs on server | ✅ | ✅ (initial HTML) | ❌ (runs in browser) |
| JS shipped to client | ❌ | ✅ (hydration bundle) | ✅ |
Can use useState | ❌ | ✅ (after hydration) | ✅ |
| Can access DB directly | ✅ | ❌ (usually) | ❌ |
| Interactivity | ❌ | ✅ | ✅ |
Composition model
// app/restaurants/page.tsx — Server Component (default in App Router)
import { db } from '@/lib/db'
import RestaurantList from './RestaurantList' // Client Component
export default async function RestaurantsPage() {
const restaurants = await db.restaurant.findMany() // direct DB access
return (
<main>
<h1>Restaurants</h1>
<RestaurantList initialData={restaurants} />
</main>
)
}// RestaurantList.tsx — Client Component
'use client'
import { useState } from 'react'
export default function RestaurantList({ initialData }) {
const [filter, setFilter] = useState('')
const filtered = initialData.filter((r) => r.name.toLowerCase().includes(filter.toLowerCase()))
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<ul>
{filtered.map((r) => (
<li key={r.id}>{r.name}</li>
))}
</ul>
</>
)
}Advantages
- Smaller client bundles — server-only code never downloads
- Zero client-side waterfall — data fetched on server in parallel
- Automatic code splitting — per-request component tree
- Secure data access — secrets stay on server
- Streaming — send HTML progressively with Suspense
Trade-offs to mention
- Mental model complexity (server vs client boundary)
- Caching and serialization constraints (props must be serializable)
- Tooling still evolving; debugging across boundaries
7. Functional vs Class Components
Theory
| Functional | Class | |
|---|---|---|
| Syntax | function Comp() {} | class Comp extends React.Component |
| State | useState, useReducer | this.state, this.setState |
| Lifecycle | useEffect | componentDidMount, etc. |
this | Not needed | Required |
| Status | Modern standard | Legacy, still in old codebases |
Hooks (React 16.8+) made function components fully capable.
Real Example
// Functional (modern)
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// Class (legacy)
class Counter extends React.Component {
state = { count: 0 }
render() {
return <button onClick={() => this.setState({ count: this.state.count + 1 })}>{this.state.count}</button>
}
}8. Grid Toggle Component
Theory
Build an N×N grid where clicking a cell toggles its state (on/off). Tests:
- State management — 2D grid in state
- Immutable updates — never mutate array directly
- Component design — separate Grid, Cell, maybe custom hook
- Event handling — click toggles correct cell
- Clean code — readable names, small functions
Common variations interviewers add:
- Toggle row/column on header click
- "Select all" / "Clear all"
- Count active cells
- Keyboard navigation
Basic Implementation
import { useState, useCallback } from 'react'
function createGrid(rows, cols, fill = false) {
return Array.from({ length: rows }, () => Array(cols).fill(fill))
}
function GridToggle({ rows = 3, cols = 3 }) {
const [grid, setGrid] = useState(() => createGrid(rows, cols))
const toggleCell = useCallback((rowIndex, colIndex) => {
setGrid((prev) =>
prev.map((row, r) => (r === rowIndex ? row.map((cell, c) => (c === colIndex ? !cell : cell)) : row)),
)
}, [])
const activeCount = grid.flat().filter(Boolean).length
return (
<div>
<p>Active cells: {activeCount}</p>
<div role="grid" aria-label={`${rows} by ${cols} toggle grid`} style={{ display: 'inline-grid', gap: 4 }}>
{grid.map((row, rowIndex) => (
<div key={rowIndex} role="row" style={{ display: 'flex', gap: 4 }}>
{row.map((isActive, colIndex) => (
<button
key={colIndex}
role="gridcell"
aria-pressed={isActive}
aria-label={`Row ${rowIndex + 1}, Column ${colIndex + 1}, ${isActive ? 'on' : 'off'}`}
onClick={() => toggleCell(rowIndex, colIndex)}
style={{
width: 48,
height: 48,
border: '2px solid #333',
borderRadius: 4,
cursor: 'pointer',
backgroundColor: isActive ? '#22c55e' : '#f3f4f6',
transition: 'background-color 0.15s',
}}
/>
))}
</div>
))}
</div>
</div>
)
}
export default GridToggleSenior Structure — Custom Hook + Subcomponents
import { useState, useCallback, useMemo } from 'react'
// --- Custom hook — logic separated from UI ---
function useGridToggle(initialRows = 3, initialCols = 3) {
const [grid, setGrid] = useState(() => createGrid(initialRows, initialCols))
const toggleCell = useCallback((row, col) => {
setGrid((prev) => prev.map((r, ri) => (ri === row ? r.map((cell, ci) => (ci === col ? !cell : cell)) : r)))
}, [])
const toggleRow = useCallback(
(row) => {
const allActive = grid[row].every(Boolean)
setGrid((prev) => prev.map((r, ri) => (ri === row ? r.map(() => !allActive) : r)))
},
[grid],
)
const toggleCol = useCallback(
(col) => {
const allActive = grid.every((row) => row[col])
setGrid((prev) => prev.map((row) => row.map((cell, ci) => (ci === col ? !allActive : cell))))
},
[grid],
)
const reset = useCallback(() => {
setGrid(createGrid(grid.length, grid[0]?.length ?? 0))
}, [grid])
const selectAll = useCallback(() => {
setGrid((prev) => prev.map((row) => row.map(() => true)))
}, [])
const activeCount = useMemo(() => grid.flat().filter(Boolean).length, [grid])
return { grid, toggleCell, toggleRow, toggleCol, reset, selectAll, activeCount }
}
// --- Cell component ---
function Cell({ isActive, row, col, onToggle }) {
return (
<button
type="button"
role="gridcell"
aria-pressed={isActive}
aria-label={`Cell ${row + 1}-${col + 1}, ${isActive ? 'active' : 'inactive'}`}
onClick={() => onToggle(row, col)}
className={`cell ${isActive ? 'cell--active' : ''}`}
/>
)
}
// --- Grid component ---
function GridToggle({ rows = 4, cols = 4 }) {
const { grid, toggleCell, toggleRow, toggleCol, reset, selectAll, activeCount } = useGridToggle(rows, cols)
return (
<div className="grid-toggle">
<header className="grid-toggle__header">
<span>
{activeCount} / {rows * cols} selected
</span>
<button type="button" onClick={selectAll}>
Select All
</button>
<button type="button" onClick={reset}>
Reset
</button>
</header>
<div role="grid" aria-rowcount={rows} aria-colcount={cols}>
{/* Column toggle headers */}
<div role="row" className="grid-toggle__col-headers">
<div style={{ width: 48 }} />
{Array.from({ length: cols }, (_, col) => (
<button
key={col}
type="button"
aria-label={`Toggle column ${col + 1}`}
onClick={() => toggleCol(col)}
className="grid-toggle__header-btn">
C{col + 1}
</button>
))}
</div>
{grid.map((row, rowIndex) => (
<div key={rowIndex} role="row" className="grid-toggle__row">
<button
type="button"
aria-label={`Toggle row ${rowIndex + 1}`}
onClick={() => toggleRow(rowIndex)}
className="grid-toggle__header-btn">
R{rowIndex + 1}
</button>
{row.map((isActive, colIndex) => (
<Cell key={colIndex} isActive={isActive} row={rowIndex} col={colIndex} onToggle={toggleCell} />
))}
</div>
))}
</div>
</div>
)
}CSS (optional — mention in interview)
.grid-toggle__row {
display: flex;
gap: 4px;
margin-bottom: 4px;
}
.cell {
transition:
background-color 0.15s,
transform 0.1s;
cursor: pointer;
border: 2px solid #374151;
border-radius: 6px;
background: #f9fafb;
width: 48px;
height: 48px;
}
.cell--active {
border-color: #16a34a;
background: #22c55e;
}
.cell:active {
transform: scale(0.95);
}
.grid-toggle__header-btn {
cursor: pointer;
width: 48px;
height: 32px;
font-size: 11px;
}Immutable Update — Explain This in Interview
// ❌ WRONG — mutates state directly
const toggleCell = (row, col) => {
grid[row][col] = !grid[row][col]
setGrid(grid) // same reference — React may not re-render!
}
// ✅ CORRECT — new array references at every level
const toggleCell = (row, col) => {
setGrid((prev) => prev.map((r, ri) => (ri === row ? r.map((cell, ci) => (ci === col ? !cell : cell)) : r)))
}Flat Array Alternative (some interviewers prefer)
function useGridFlat(rows, cols) {
const [cells, setCells] = useState(() => Array(rows * cols).fill(false))
const toggle = (index) => {
setCells((prev) => prev.map((cell, i) => (i === index ? !cell : cell)))
}
const getCell = (row, col) => cells[row * cols + col]
return { cells, toggle, getCell, rows, cols }
}What Interviewers Look For
| Criteria | How to demonstrate |
|---|---|
| State management | 2D array or flat array with clear indexing |
| Immutable updates | map — never direct mutation |
| Component design | Hook + presentational components |
| Event handling | Correct row/col passed to handler |
| Clean code | Named functions, no magic numbers |
| Bonus | a11y (aria-pressed), select all, reset |
9. Higher Order Components (HOC)
Theory
A HOC is a function that takes a component and returns an enhanced component with extra props or behavior: const Enhanced = withAuth(Wrapped).
Legacy pattern — custom hooks are preferred today for the same reuse without wrapper nesting.
Real Example
function withAuth(WrappedComponent) {
return function AuthComponent(props) {
const { user } = useAuth()
if (!user) return <Navigate to="/login" />
return <WrappedComponent {...props} user={user} />
}
}
export default withAuth(Dashboard)10. How do you integrate RTK Query with your React components?
Answer
You use the auto-generated hooks like useGetUsersQuery, useAddUserMutation, etc., directly in your components to fetch and manipulate data.
11. How do you structure reusable UI components in React?
Theory
A reusable component library follows atomic design principles: atoms (Button, Input) → molecules (SearchBar, FormField) → organisms (Header, ProductCard) → templates → pages. Components should be composable, accessible, themeable, and documented.
Pros & Cons
| Approach | Pros | Cons |
|---|---|---|
| Monolithic components | Fast to build initially | Hard to customize, prop explosion |
| Compound components | Flexible composition | Steeper learning curve |
| Headless + styled | Logic reuse across designs | Two layers to maintain |
| Design tokens | Consistent theming | Setup overhead |
Real-Life Example
packages/
├── tokens/ # Design tokens
│ ├── colors.json
│ ├── spacing.json
│ └── typography.json
├── primitives/ # Atoms — unstyled, accessible
│ ├── Button/
│ ├── Input/
│ └── Checkbox/
├── components/ # Molecules & organisms
│ ├── SearchBar/
│ ├── ProductCard/
│ └── Modal/
└── docs/ # Storybook// Compound component pattern — flexible Modal
const Modal = ({ children, isOpen, onClose }) => {
if (!isOpen) return null
return createPortal(
<ModalContext.Provider value={{ onClose }}>
<div className="modal-overlay" onClick={onClose}>
{children}
</div>
</ModalContext.Provider>,
document.body,
)
}
Modal.Header = ({ children }) => <header className="modal-header">{children}</header>
Modal.Body = ({ children }) => <div className="modal-body">{children}</div>
Modal.Footer = ({ children }) => <footer className="modal-footer">{children}</footer>
// Usage — compose as needed
;<Modal isOpen={open} onClose={close}>
<Modal.Header>
<h2>Confirm Order</h2>
</Modal.Header>
<Modal.Body>Your total is ₹499. Proceed?</Modal.Body>
<Modal.Footer>
<Button variant="ghost" onClick={close}>
Cancel
</Button>
<Button variant="primary" onClick={confirm}>
Confirm
</Button>
</Modal.Footer>
</Modal>Key principles:
- Accept
classNameand spread remaining props for extensibility - Use
forwardReffor focus management - Document with Storybook + TypeScript prop types
- Test accessibility with
@testing-library/jest-domand axe
| # | Topic | Key Point |
|---|---|---|
| 1 | Promise.all | All resolve or first reject; order preserved |
| 2 | Promise.any | First resolve wins; all reject = AggregateError |
| 3 | reduce polyfill | Accumulator + callback; handles sparse arrays |
| 4 | flatten | Recursive depth control; flat(Infinity) native |
| 5 | Auto-retry | Exponential backoff + jitter |
| 6 | Batch promises | Chunk size + delay between batches |
| 7 | Debounce | Wait for pause |
| 8 | Throttle | Once per interval |
| 9 | Series tasks | Callback chain or reduce with promises |
| 10 | Output prediction | Hoisting, closure, event loop, coercion |
| 11 | Object vs Map | String keys vs any key; JSON vs performance |
| 12 | PATCH vs PUT | Partial vs full replacement |
| 13 | Debounce vs Throttle | Stop vs rate-limit |
| 14 | JS Engine | Parse → interpret → JIT optimize → GC |
| 15 | Event Loop | Sync → microtasks → macrotask |
| 16 | Virtual DOM | JS tree + diffing heuristics |
| 17 | Keys | Stable identity for list reconciliation |
| 18 | useState internals | Hook linked list on Fiber |
| 19 | useState polyfill | Array + index reset on setState |
| 20 | Portals | Render outside parent DOM |
| 21 | Error Boundaries | Catch render errors; class only |
| 22 | Memoization | useMemo / useCallback / React.memo |
| 23 | SSR vs CSR | Server HTML vs client JS render |
| 24 | Module Federation | Runtime remote module sharing |
| 25 | Micro-frontends | Independent deployable frontend apps |
| 26 | SSR for SEO | Meta tags, structured data, SSG/ISR |
| 27 | tabIndex | 0 = focusable, -1 = programmatic only |
| 28 | Capturing/Bubbling | Top-down vs bottom-up event propagation |
| 29 | toString override | Possible but never mutate prototypes |
| 30 | Memory leaks | Cleanup effects, remove listeners |
| 31 | Performance | Profiler, Web Vitals, bundle analyzer |
| 32 | OAuth | Authorization without password sharing |
| 33 | SSO | One login, multiple apps via IdP |
| 34 | REST methods | GET read, POST create, PUT replace, PATCH partial, DELETE remove |
| 35 | FP | Pure functions, immutability, composition |
| 36 | Microservices | Small independent deployable services |
| 37 | CRA-like tool | CLI + template + bundler + dev server |
| 38 | UI components | Atoms → molecules → organisms + Storybook |
12. How does React decide whether a component should re-render?
Theory
A component re-renders when React schedules an update for it. Triggers include:
| Trigger | Example |
|---|---|
| State change | setState, useState setter, useReducer dispatch |
| Parent re-render | Child re-renders by default (unless memoized) |
| Context change | Consumer re-renders when context value changes |
| Force update | forceUpdate (class) — rarely used |
| External store | useSyncExternalStore subscription fires |
React does not re-render when:
- Props are the same reference and state unchanged (for memoized components)
- State update is bailed out because
Object.is(oldState, newState)is true
Re-render vs DOM update
Re-render = React calls your component function again and diffs the output. DOM update = Only happens if the diff produces changes (commit phase).
Practical Example
function Parent() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount((c) => c + 1)}>+</button>
<Child name="Amit" /> {/* re-renders on every Parent render */}
<MemoChild name="Rahul" /> {/* skips if props unchanged */}
</>
)
}
const MemoChild = React.memo(function MemoChild({ name }) {
console.log('MemoChild render')
return <p>{name}</p>
})Bailout conditions
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(0) // React bails out — state already 0, no re-render
}
return <button onClick={handleClick}>{count}</button>
}Interview answer
13. How would you design a reusable component library?
Architecture
packages/
├── tokens/ # Colors, spacing, typography (JSON / CSS vars)
├── primitives/ # Button, Input, Checkbox (unstyled logic)
├── components/ # Composed UI (DatePicker, Modal)
├── icons/
└── docs/ # StorybookDesign principles
| Principle | Implementation |
|---|---|
| Composable | Compound components (<Select><Select.Trigger /></Select>) |
| Accessible | WAI-ARIA, keyboard nav, focus management |
| Themeable | CSS variables or Tailwind preset |
| Typed | TypeScript with strict prop types |
| Tree-shakeable | ESM, per-component exports |
| Versioned | Semver, changelog, codemods for breaking changes |
Button example
// packages/components/src/Button/Button.tsx
import { forwardRef } from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import styles from './Button.module.css'
const buttonVariants = cva(styles.base, {
variants: {
variant: {
primary: styles.primary,
ghost: styles.ghost,
danger: styles.danger,
},
size: {
sm: styles.sm,
md: styles.md,
lg: styles.lg,
},
},
defaultVariants: { variant: 'primary', size: 'md' },
})
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean
loading?: boolean
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild, loading, children, disabled, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
ref={ref}
className={buttonVariants({ variant, size, className })}
disabled={disabled || loading}
aria-busy={loading}
{...props}>
{loading ? <Spinner /> : children}
</Comp>
)
},
)Tooling stack
- Storybook — documentation + visual testing
- Chromatic — visual regression CI
- Changesets — versioning and publishing
- Vitest + Testing Library — unit/a11y tests
- ESLint plugin — enforce import boundaries
Interview answer
14. JSX vs HTML — Key Differences
Theory
| JSX | HTML |
|---|---|
className | class |
htmlFor | for |
onClick (camelCase) | onclick (lowercase) |
Self-closing required (<img />) | Optional in HTML5 |
style={{ color: "red" }} (object) | style="color: red" (string) |
{} for JavaScript expressions | Not available |
| One root element or Fragment | Multiple roots OK in HTML5 |
Real Example
// ❌ Invalid JSX
<div class="card" onclick={handleClick} style="color: blue">
// ✅ Valid JSX
<div className="card" onClick={handleClick} style={{ color: "blue" }}>
{isLoggedIn ? <Dashboard /> : <Login />}
</div>15. Keep business logic separate from UI components
Theory
UI components render markup and handle user interaction. Business logic handles rules, calculations, validations, and data transformations. Mixing them makes components hard to test and reuse.
Separate into: services (API calls), utils (pure functions), hooks (stateful logic), and components (presentation).
Pros & Cons
| Separated logic | Mixed logic in components |
|---|---|
| ✅ Test business rules without rendering | ❌ Can't unit test calculations |
| ✅ Reuse logic in CLI, mobile, server | ❌ 300-line components |
| ✅ Designers can work on UI independently | ❌ Bug fixes risk breaking UI |
| ✅ Easier to swap UI framework | — |
Real-Life Example
// utils/pricing.ts — pure business logic, no React
export function calculateOrderTotal(items: CartItem[], coupon?: Coupon): OrderTotal {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const discount = coupon ? applyCoupon(subtotal, coupon) : 0
const tax = (subtotal - discount) * 0.05
const deliveryFee = subtotal > 500 ? 0 : 40
return {
subtotal,
discount,
tax,
deliveryFee,
total: subtotal - discount + tax + deliveryFee,
}
}
// services/orderService.ts — API layer
export const orderService = {
placeOrder: (payload: OrderPayload) => api.post('/orders', payload),
cancelOrder: (id: string) => api.patch(`/orders/${id}`, { status: 'cancelled' }),
}
// hooks/useCheckout.ts — stateful orchestration
function useCheckout() {
const [items] = useCart()
const total = useMemo(() => calculateOrderTotal(items), [items])
const placeOrder = async () => {
const result = await orderService.placeOrder({ items, total })
return result
}
return { items, total, placeOrder }
}
// components/CheckoutSummary.tsx — UI only
function CheckoutSummary() {
const { items, total, placeOrder } = useCheckout()
return (
<div>
<ItemList items={items} />
<PriceBreakdown total={total} />
<button onClick={placeOrder}>Place Order — ₹{total.total}</button>
</div>
)
}16. Keep components small and reusable
Theory
A React component should do one thing well. Small components are easier to read, test, reuse, and debug. The Single Responsibility Principle applies here — a ProductCard displays a product; it shouldn't also handle cart logic, API calls, and modal state.
Rule of thumb: If a component exceeds ~150 lines or has more than one reason to change, split it.
Pros & Cons
| Small components | Large monolithic components |
|---|---|
| ✅ Easy to test in isolation | ❌ Hard to reason about |
| ✅ Reusable across pages | ❌ Copy-paste duplication |
| ✅ Faster to review in PRs | ❌ One bug affects everything |
| ✅ Clear prop contracts | ❌ Hidden side effects |
Real-Life Example
// ❌ Bad — one giant component doing everything
function OrderPage() {
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('all')
// ... 200 more lines of fetch, filter, render, modal logic
}
// ✅ Good — composed from small, focused components
function OrderPage() {
const { orders, loading, error } = useOrders()
const [filter, setFilter] = useState('all')
if (loading) return <OrderListSkeleton />
if (error) return <ErrorBanner message={error} />
return (
<div>
<OrderFilter value={filter} onChange={setFilter} />
<OrderList orders={filterOrders(orders, filter)} />
</div>
)
}
function OrderList({ orders }) {
return (
<ul>
{orders.map((order) => (
<OrderCard key={order.id} order={order} />
))}
</ul>
)
}
function OrderCard({ order }) {
return (
<li className="order-card">
<OrderStatusBadge status={order.status} />
<span>{order.itemName}</span>
<span>₹{order.total}</span>
</li>
)
}17. Props in React
Theory
Props (properties) pass data from parent to child. They are read-only — a child must never mutate props. Props make components reusable with different data.
Pros & Cons
| Pros | Cons |
|---|---|
| Explicit data flow | Prop drilling at depth |
| Easy to test with different inputs | Many props → hard to maintain |
Real Example
function ProductCard({ name, price, onAddToCart }) {
return (
<div>
<h3>{name}</h3>
<p>₹{price}</p>
<button onClick={onAddToCart}>Add to Cart</button>
</div>
)
}
;<ProductCard name="Biryani" price={299} onAddToCart={() => addItem('biryani')} />18. React Fragments
Theory
Fragments (<>...</> or <React.Fragment>) let you group children without adding an extra DOM node. Useful when a component must return multiple siblings.
Pros & Cons
| Pros | Cons |
|---|---|
| No wrapper div pollution | Can't style the fragment itself |
| Cleaner DOM | Named Fragment needs React.Fragment for keys |
Real Example
function TableRow({ name, email }) {
return (
<>
<td>{name}</td>
<td>{email}</td>
</>
)
}
// With key in a list — must use React.Fragment
items.map((item) => (
<React.Fragment key={item.id}>
<dt>{item.label}</dt>
<dd>{item.value}</dd>
</React.Fragment>
))19. Star Rating Component (1–10)
Theory
Build an interactive star rating from 1 to 10 — click to rate, hover preview, accessible with keyboard and ARIA.
React Implementation
import { useState } from 'react'
function StarRating({
max = 10,
value,
onChange,
readOnly = false,
}: {
max?: number
value?: number
onChange?: (rating: number) => void
readOnly?: boolean
}) {
const [hover, setHover] = useState(0)
const [selected, setSelected] = useState(value ?? 0)
const display = hover || selected
const handleClick = (rating: number) => {
if (readOnly) return
setSelected(rating)
onChange?.(rating)
}
const handleKeyDown = (e: React.KeyboardEvent, rating: number) => {
if (readOnly) return
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick(rating)
}
if (e.key === 'ArrowRight') handleClick(Math.min(rating + 1, max))
if (e.key === 'ArrowLeft') handleClick(Math.max(rating - 1, 1))
}
return (
<div role="radiogroup" aria-label={`Rating out of ${max}`} onMouseLeave={() => !readOnly && setHover(0)}>
{Array.from({ length: max }, (_, i) => {
const rating = i + 1
const filled = rating <= display
return (
<button
key={rating}
type="button"
role="radio"
aria-checked={selected === rating}
aria-label={`${rating} out of ${max}`}
disabled={readOnly}
onClick={() => handleClick(rating)}
onMouseEnter={() => !readOnly && setHover(rating)}
onKeyDown={(e) => handleKeyDown(e, rating)}
style={{
background: 'none',
border: 'none',
cursor: readOnly ? 'default' : 'pointer',
fontSize: '1.5rem',
color: filled ? '#f59e0b' : '#d1d5db',
}}>
★
</button>
)
})}
<span aria-live="polite">{selected > 0 ? `${selected}/${max}` : ''}</span>
</div>
)
}
// Usage
;<StarRating max={10} onChange={(r) => console.log('Rated:', r)} />Vanilla JS Version
function createStarRating(container, max = 10, onRate) {
container.setAttribute('role', 'radiogroup')
container.innerHTML = ''
for (let i = 1; i <= max; i++) {
const star = document.createElement('button')
star.textContent = '★'
star.setAttribute('aria-label', `${i} out of ${max}`)
star.style.cssText = 'background:none;border:none;font-size:24px;cursor:pointer;color:#d1d5db'
star.onmouseenter = () => highlight(i)
star.onclick = () => {
onRate?.(i)
highlight(i, true)
}
container.appendChild(star)
}
container.onmouseleave = () => highlight(0)
function highlight(upTo, persist = false) {
container.querySelectorAll('button').forEach((btn, idx) => {
btn.style.color = idx < upTo ? '#f59e0b' : '#d1d5db'
})
}
}20. State in React
Theory
State is data that changes over time inside a component. When state updates, React re-renders the component. State is private to the component that owns it (unless lifted up or shared via context).
Managed with useState or useReducer.
Pros & Cons
| Local state | Global state for everything |
|---|---|
| ✅ Colocated, simple | ❌ Over-engineering |
| ✅ Component owns its data | ❌ Hard to trace changes |
Real Example
function SearchBar() {
const [query, setQuery] = useState('')
return <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
}21. State vs Props
Theory
| Props | State | |
|---|---|---|
| Source | Parent | Component itself |
| Mutable? | No (read-only) | Yes (via setter) |
| Purpose | Configure component | Track changing data |
| Analogy | Function arguments | Variables inside function |
Real Example
function Counter({ initialCount }) {
// prop — set once by parent
const [count, setCount] = useState(initialCount) // state — changes on click
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}22. Synthetic Events
Theory
React wraps native browser events in SyntheticEvent — a cross-browser normalized wrapper. React uses event delegation (events attached at root, not each node) for performance.
e.persist() is rarely needed in React 17+ (events no longer pooled).
Real Example
function SearchInput() {
const handleSubmit = (e) => {
e.preventDefault() // works on SyntheticEvent
fetchResults()
}
return (
<form onSubmit={handleSubmit}>
<input onChange={(e) => setQuery(e.target.value)} />
</form>
)
}23. Understand when components re-render
Theory
A component re-renders when:
- Its own state changes (
useState,useReducer) - Its parent re-renders (children re-render by default)
- Context it consumes changes
- Props change (for
React.memocomponents — shallow compare)
Re-render ≠ DOM update. React may re-run your component but skip DOM changes if output is identical.
Pros & Cons
| Understanding re-renders | Ignoring re-render causes |
|---|---|
| ✅ Targeted optimization | ❌ Random React.memo everywhere |
| ✅ Fewer performance bugs | ❌ Unnecessary API calls in effects |
| ✅ Correct dependency arrays | ❌ Stale UI state |
Real-Life Example
function App() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<ExpensiveChild name="Dashboard" /> {/* re-renders on every count click! */}
</>
)
}
// Fix — memoize child that doesn't depend on count
const ExpensiveChild = React.memo(function ExpensiveChild({ name }) {
console.log('ExpensiveChild rendered')
return <div>{name}</div>
})
// Debug re-renders in development
function DebugRender({ name }) {
useEffect(() => {
console.log(`${name} rendered at`, new Date().toISOString())
})
return null
}Re-render triggers cheat sheet:
| Trigger | Example |
|---|---|
| State update | setCount(1) |
| Parent re-render | Parent state changes → all children re-render |
| Context change | Theme context value changes |
| Force update | key prop change forces remount |
| No re-render | setCount(0) when count is already 0 (bailout) |
24. What are controlled components?
Answer
Controlled components are form elements whose values are controlled by React state. The value of the input field is set by the component's state.
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />25. What are uncontrolled components?
Answer
Uncontrolled components manage their own state using the DOM, typically accessed via refs in React.
const inputRef = useRef()
function handleSubmit() {
alert(`Input: ${inputRef.current.value}`)
}
;<input ref={inputRef} type="text" />26. What is a class component?
Answer
A class component is an ES6 class that extends React.Component and has a render() method. It can maintain its own state and lifecycle methods.
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>
}
}27. What is a functional component?
Answer
A functional component is a simple JavaScript function that returns JSX. It doesn't have its own state unless you use React hooks like useState.
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>
}28. What is a Higher Order Component (HOC)?
Theory
A Higher Order Component is a function that takes a component and returns a new enhanced component with additional props or behavior. Pattern: const Enhanced = withFeature(WrappedComponent).
HOCs were the primary reuse pattern before Hooks. They solve cross-cutting concerns: authentication, logging, data fetching, error handling.
Pros & Cons
| Pros | Cons |
|---|---|
| Reusable logic across components | "Wrapper hell" — deeply nested component tree |
| Separation of concerns | Props collision (must rename injected props) |
| Works with class components | Harder to trace in DevTools |
| Hooks and custom hooks are preferred today |
Real-Life Example
withAuth HOC
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, isLoading } = useAuth()
if (isLoading) return <Spinner />
if (!user) return <Navigate to="/login" />
return <WrappedComponent {...props} user={user} />
}
}
// Usage
function Dashboard({ user }) {
return <h1>Welcome, {user.name}</h1>
}
export default withAuth(Dashboard)withLoading HOC
function withLoading(WrappedComponent, fetchData) {
return function WithLoadingComponent(props) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchData(props).then((result) => {
setData(result)
setLoading(false)
})
}, [props.id])
if (loading) return <Spinner />
return <WrappedComponent {...props} data={data} />
}
}
const RestaurantPage = withLoading(
({ data }) => <RestaurantDetail restaurant={data} />,
(props) => axios.get(`/api/restaurants/${props.id}`).then((r) => r.data),
)Modern alternative — custom hooks (preferred)
// ✅ Today — custom hook instead of HOC
function useAuth() {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
apiClient
.get('/api/me')
.then((res) => setUser(res.data))
.finally(() => setIsLoading(false))
}, [])
return { user, isLoading, isAuthenticated: !!user }
}
function Dashboard() {
const { user, isLoading, isAuthenticated } = useAuth()
if (isLoading) return <Spinner />
if (!isAuthenticated) return <Navigate to="/login" />
return <h1>Welcome, {user.name}</h1>
}Interview answer
29. What is JSX?
Theory
JSX (JavaScript XML) is syntax that lets you write HTML-like markup inside JavaScript. It is
not HTML — it compiles to React.createElement() calls via Babel.
Rules: one parent element (or Fragment), camelCase attributes (className, onClick), expressions in {}.
Pros & Cons
| Pros | Cons |
|---|---|
| Readable, visual UI code | Can confuse HTML vs JSX differences |
| Catches errors at compile time | Needs build step (Babel) |
Real Example
// JSX
const element = (
<button className="btn" onClick={handleClick}>
Pay ₹{amount}
</button>
)
// Compiles to:
const element = React.createElement('button', { className: 'btn', onClick: handleClick }, 'Pay ₹', amount)30. What is React and why is it efficient?
Theory
React is a JavaScript library for building user interfaces using a component-based, declarative model. You describe what the UI should look like for a given state, and React handles updating the DOM when state changes.
React is efficient because it:
- Virtual DOM — compares trees and applies minimal DOM mutations instead of rewriting the entire page
- Batched updates — groups multiple state changes into one re-render (React 18 automatic batching)
- Fiber architecture — interruptible rendering; urgent updates (typing) aren't blocked by heavy work
- One-way data flow — predictable state propagation from parent to child
- Component reusability — write once, compose everywhere
Pros & Cons
| Pros | Cons |
|---|---|
| Large ecosystem and community | JSX learning curve for beginners |
| Reusable component model | Needs additional libraries for routing, state (not a full framework) |
| Strong hiring market | Frequent ecosystem changes (hooks, RSC, etc.) |
| Virtual DOM minimizes expensive DOM ops | Can over-render without memoization discipline |
Real-Life Example
// Declarative — you describe UI based on state
function OrderTracker({ orderId }) {
const { data: order, isLoading } = useOrder(orderId)
if (isLoading) return <Skeleton />
if (order.status === 'delivered') return <DeliveredBanner order={order} />
return (
<div>
<h2>Order #{order.id}</h2>
<StatusTimeline steps={order.timeline} />
<LiveMap driverLocation={order.driverLocation} />
</div>
)
}
// React efficiently updates only what changed:
// - Status text changes → only that DOM node updates
// - Driver location updates every 5s → only map marker moves
// - You never manually call document.getElementById or innerHTMLInterview answer (concise)
31. What is React Developer Tools?
Answer
A browser extension (Chrome/Firefox) that helps developers inspect the component hierarchy, state, props, hooks, and more in React applications.
32. What is React?
Theory
React is a JavaScript library for building user interfaces using reusable components. You describe UI as a function of state — when state changes, React updates the screen efficiently.
It is not a full framework (no built-in routing or API layer). It focuses on the view layer.
Pros & Cons
| Pros | Cons |
|---|---|
| Component reusability | Needs extra libraries (router, state) |
| Virtual DOM — efficient updates | JSX learning curve |
| Huge ecosystem and job market | Fast-moving ecosystem |
Real Example
function Welcome({ name }) {
return <h1>Hello, {name}</h1>
}
// Reuse anywhere: <Welcome name="Amit" />Hooks
33. Clean up subscriptions, timers, and event listeners inside useEffect
Theory
Effects that create subscriptions, timers, WebSockets, or event listeners must return a cleanup function. Without cleanup, you get memory leaks, stale state updates on unmounted components, and duplicate listeners.
Cleanup runs: (1) before the effect re-runs when deps change, (2) when the component unmounts.
Pros & Cons
| With cleanup | Without cleanup |
|---|---|
| ✅ No memory leaks | ❌ WebSocket stays open after navigate away |
| ✅ No "setState on unmounted component" warnings | ❌ Duplicate event listeners on re-render |
| ✅ Aborted stale API responses | ❌ Timers fire after unmount |
Real-Life Example
function LiveOrderTracker({ orderId }) {
const [status, setStatus] = useState('pending')
useEffect(() => {
// WebSocket
const ws = new WebSocket(`wss://api.example.com/orders/${orderId}`)
ws.onmessage = (e) => setStatus(JSON.parse(e.data).status)
// Timer
const pollInterval = setInterval(() => {
fetch(`/api/orders/${orderId}/status`)
.then((r) => r.json())
.then((data) => setStatus(data.status))
}, 5000)
// Window event
const handleVisibility = () => {
if (document.visibilityState === 'visible') ws.send('ping')
}
document.addEventListener('visibilitychange', handleVisibility)
// Cleanup everything
return () => {
ws.close()
clearInterval(pollInterval)
document.removeEventListener('visibilitychange', handleVisibility)
}
}, [orderId])
return <StatusBadge status={status} />
}Cleanup checklist:
| Resource | Cleanup |
|---|---|
setInterval / setTimeout | clearInterval / clearTimeout |
addEventListener | removeEventListener |
| WebSocket | ws.close() |
fetch / Axios | AbortController.abort() |
subscribe() (RxJS, store) | unsubscribe() |
34. Custom Hooks
Theory
A custom hook is a function starting with use that encapsulates reusable stateful logic. It can call other hooks. It is not a component — it returns data and functions.
Pros & Cons
| Pros | Cons |
|---|---|
| DRY — share logic across components | Must follow Rules of Hooks |
| Keeps components clean | Over-abstraction if too granular |
Real Example
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then(setData)
.finally(() => setLoading(false))
}, [url])
return { data, loading }
}
// Usage
function ProductPage() {
const { data, loading } = useFetch('/api/products')
if (loading) return <Spinner />
return <ProductList products={data} />
}35. Dependency Array
Theory
The second argument of useEffect controls when it re-runs:
| Array | Behavior |
|---|---|
| Omitted | Runs after every render |
[] | Runs once on mount |
[a, b] | Runs on mount + when a or b changes |
React compares deps with Object.is (shallow equality).
Pros & Cons
| Correct deps | Missing/wrong deps |
|---|---|
| ✅ Synced with latest state | ❌ Stale closures |
| ✅ Predictable behavior | ❌ Infinite loops with objects |
Real Example
useEffect(() => {
const timer = setInterval(() => tick(), 1000)
return () => clearInterval(timer) // cleanup
}, []) // mount only — empty array
useEffect(() => {
fetchOrders(userId)
}, [userId]) // re-run when userId changes36. Difference between useMemo, useCallback, and React.memo
Theory
| Hook/HOC | What it memoizes | When it helps |
|---|---|---|
useMemo | Computed value | Expensive calculations |
useCallback | Function reference | Stable ref for memoized children / effect deps |
React.memo | Component render | Skip re-render if props unchanged (shallow compare) |
Practical Example
function ProductList({ products, onSelect }) {
// useMemo — cache expensive filter/sort result
const sorted = useMemo(() => [...products].sort((a, b) => a.price - b.price), [products])
// useCallback — stable function reference for memoized child
const handleSelect = useCallback((id) => onSelect(id), [onSelect])
return sorted.map((p) => <ProductRow key={p.id} product={p} onSelect={handleSelect} />)
}
const ProductRow = React.memo(function ProductRow({ product, onSelect }) {
return (
<div onClick={() => onSelect(product.id)}>
{product.name} — ₹{product.price}
</div>
)
})Common mistake
// ❌ useMemo on cheap operations — adds overhead, no benefit
const doubled = useMemo(() => count * 2, [count])
// ❌ useCallback without React.memo child — pointless
const fn = useCallback(() => doThing(), [])
return <RegularChild onClick={fn} /> // RegularChild always re-renders anywayInterview answer
37. Explain how useState works internally
Theory
useState stores state on the Fiber node associated with the component. Each hook call creates a hook object linked in a singly-linked list on the Fiber (memoizedState).
On each render:
- React walks the hook list in the same order hooks were called
- Returns the current
memoizedStatevalue - The setter enqueues an update on the Fiber's update queue
- React schedules a re-render; during the next render, the hook returns the new value
Rules of Hooks exist because hook order must be identical every render.
Pros & Cons
| Pros | Cons |
|---|---|
| Simple API for local state | State updates are async (batched) |
| Triggers re-render automatically | Cannot call conditionally |
| Batching improves performance | Stale closure if deps not managed |
Real-Life Example
function Counter() {
const [count, setCount] = useState(0) // Hook #1 on Fiber
const [name, setName] = useState('') // Hook #2 on Fiber
// Internally on Fiber:
// memoizedState → { memoizedState: count, queue: [updates], next: hook2 }
// hook2 → { memoizedState: name, queue: [], next: null }
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}38. Fetching Data with useEffect
Theory
Classic pattern: useEffect + fetch + local state for data, loading, error. Always handle cleanup with AbortController to cancel in-flight requests on unmount.
Real Example
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const controller = new AbortController()
setLoading(true)
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((r) => {
if (!r.ok) throw new Error(r.statusText)
return r.json()
})
.then(setUser)
.catch((e) => {
if (e.name !== 'AbortError') setError(e)
})
.finally(() => setLoading(false))
return () => controller.abort()
}, [userId])
}39. Implement a basic version of useState
Theory
A minimal useState implementation uses a module-level array to store state per hook index, incrementing the index on each call. A render function re-executes the component. This mirrors React's hook linked list but simplified for learning.
Real-Life Example
let hooks = []
let hookIndex = 0
function useState(initialValue) {
const currentIndex = hookIndex
if (hooks[currentIndex] === undefined) {
hooks[currentIndex] = typeof initialValue === 'function' ? initialValue() : initialValue
}
const setState = (newValue) => {
const nextValue = typeof newValue === 'function' ? newValue(hooks[currentIndex]) : newValue
if (Object.is(hooks[currentIndex], nextValue)) return // bailout
hooks[currentIndex] = nextValue
hookIndex = 0 // reset for re-render
render() // trigger re-render
}
hookIndex++
return [hooks[currentIndex], setState]
}
// Usage
function Counter() {
const [count, setCount] = useState(0)
const [name, setName] = useState('Guest')
console.log(`Hello ${name}, count: ${count}`)
}
function render() {
hookIndex = 0
Counter()
}
render()
// setCount(1) → re-renders with updated count40. Memoization (useMemo, useCallback)
Theory
| Hook | Memoizes | Use when |
|---|---|---|
useMemo | Computed value | Expensive calculations |
useCallback | Function reference | Stable ref for memoized children |
React.memo | Component render | Props unchanged → skip re-render |
React compares with Object.is (shallow equality). Profile before optimizing.
Pros & Cons
| Pros | Cons |
|---|---|
| Skips expensive recalculations | Memory overhead for cached values |
| Prevents child re-renders | Shallow compare misses deep changes |
| Essential for virtualization | Overuse adds complexity |
Real-Life Example
function DataTable({ rows, filters }) {
const filtered = useMemo(() => rows.filter((r) => matchesFilters(r, filters)), [rows, filters])
const handleEdit = useCallback((id: string) => {
openEditor(id)
}, [])
return filtered.map((row) => <MemoRow key={row.id} row={row} onEdit={handleEdit} />)
}
const MemoRow = React.memo(function MemoRow({ row, onEdit }) {
return (
<tr onClick={() => onEdit(row.id)}>
<td>{row.name}</td>
</tr>
)
})Rule: Profile first → optimize hot paths only.
41. Rules of Hooks
Theory
- Only call hooks at the top level — not inside conditions, loops, or nested functions
- Only call hooks from React functions — components or custom hooks
Why: React stores hooks in a linked list indexed by call order. Conditional hooks break the order.
Real Example
// ❌ WRONG
if (loggedIn) {
const [user, setUser] = useState(null)
}
// ✅ CORRECT
const [user, setUser] = useState(null)
useEffect(() => {
if (loggedIn) fetchUser().then(setUser)
else setUser(null)
}, [loggedIn])42. Single state object vs individual useState
Theory
| Single object | Individual useState | |
|---|---|---|
| Update | setForm({...form, pan: x}) | setPan(x) |
| Re-render scope | Whole form re-renders | Whole component still re-renders* |
| Validation | Easy cross-field rules | Must coordinate multiple states |
| Submit | One object ready | Must assemble |
| Risk | Mutating state directly | 10+ setter names |
*Individual useState still re-renders the whole function component — memoized children help.
What breaks in each
| Single object breaks when... | Individual useState breaks when... |
|---|---|
You mutate: form.pan = x | Cross-field validation (PAN name match) |
| Spread typo misses fields | Submit assembly forgotten |
| One huge re-render tree | 10 useState calls — verbose |
| Deep nesting grows | Dependent fields get messy |
Real-Life Example
// Single object — good for submit, bad for perf without memo
const [form, setForm] = useState({ pan: '', aadhaar: '', name: '' })
const update = (field: string, value: string) => setForm((prev) => ({ ...prev, [field]: value }))
// Individual — good for simple fields
const [pan, setPan] = useState('')
const [aadhaar, setAadhaar] = useState('')
// useReducer — a fintech app sweet spot for complex forms
type Action =
| { type: 'SET_FIELD'; field: keyof FormState; value: string }
| { type: 'SET_ERRORS'; errors: FormErrors }
| { type: 'RESET' }
function formReducer(state: FormState, action: Action): FormState {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value }
case 'RESET':
return INITIAL
default:
return state
}
}
const [form, dispatch] = useReducer(formReducer, INITIAL)Interview recommendation: useReducer or React Hook Form for 10+ fields with cross-validation.
43. Use custom hooks to avoid duplicate logic
Theory
Custom hooks extract reusable stateful logic into functions prefixed with use. They let you
share behavior — data fetching, form handling, debouncing, local storage — across components without copy-pasting
useState + useEffect blocks.
A custom hook is not a component — it doesn't render UI. It returns data and functions.
Pros & Cons
| Custom hooks | Duplicate logic in components |
|---|---|
| ✅ DRY — write once, use everywhere | ❌ Bug fixes needed in multiple places |
| ✅ Testable in isolation | ❌ Inconsistent behavior across pages |
| ✅ Readable component code | ❌ Components become bloated |
| ✅ Composable (hooks calling hooks) | — |
Real-Life Example
// Custom hook — reusable across the app
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
setMatches(media.matches)
const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
media.addEventListener('change', handler)
return () => media.removeEventListener('change', handler)
}, [query])
return matches
}
// Usage — clean components
function SearchPage() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const isMobile = useMediaQuery('(max-width: 768px)')
const [recentSearches, setRecentSearches] = useLocalStorage<string[]>('recent', [])
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery)
}, [debouncedQuery])
return <SearchInput value={query} onChange={setQuery} compact={isMobile} />
}44. Use useMemo and useCallback only when necessary
Theory
useMemo caches a computed value. useCallback caches a function reference. Both have memory and comparison overhead — use them only when profiling shows a real problem or when passing callbacks to React.memo children.
Don't optimize prematurely. Simple components with cheap computations don't benefit.
Pros & Cons
| When to use | When to skip |
|---|---|
| Expensive filter/sort on large arrays | count * 2 |
| Stable ref for memoized children | Child isn't wrapped in React.memo |
| Referential equality for effect deps | Function only used inside same component |
| Virtualized list item callbacks | Components that always re-render anyway |
Real-Life Example
function ProductCatalog({ products, filters }) {
// ✅ useMemo — filtering 10,000 products is expensive
const filtered = useMemo(() => {
return products.filter((p) => matchesFilters(p, filters)).sort((a, b) => b.rating - a.rating)
}, [products, filters])
// ✅ useCallback — ProductRow is memoized
const handleAddToCart = useCallback((productId: string) => {
addToCart(productId)
}, [])
return filtered.map((p) => <ProductRow key={p.id} product={p} onAdd={handleAddToCart} />)
}
// ❌ Unnecessary memoization
function Greeting({ name }) {
const greeting = useMemo(() => `Hello, ${name}`, [name]) // pointless
const onClick = useCallback(() => alert(name), [name]) // pointless
return <p onClick={onClick}>{greeting}</p>
}Rule: Profile first with React DevTools Profiler → identify slow renders → then add memoization.
45. useCallback
Theory
useCallback(fn, deps) returns a memoized function reference. Prevents child re-renders when passing callbacks to React.memo children.
Pairs with React.memo — useless without memoized children.
Real Example
const MemoRow = React.memo(function Row({ item, onDelete }) {
return (
<tr>
<td>{item.name}</td>
<td>
<button onClick={() => onDelete(item.id)}>Delete</button>
</td>
</tr>
)
})
function Table({ items }) {
const handleDelete = useCallback((id) => {
setItems((prev) => prev.filter((i) => i.id !== id))
}, [])
return items.map((item) => <MemoRow key={item.id} item={item} onDelete={handleDelete} />)
}46. useEffect
Theory
useEffect(callback, deps) runs side effects after render — API calls, subscriptions, DOM manipulation, timers.
Return a cleanup function to prevent leaks on unmount or before re-run.
Real Example
useEffect(() => {
const controller = new AbortController()
fetch(`/api/users/${id}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
return () => controller.abort()
}, [id])47. useEffect as Lifecycle Replacement
Theory
| Lifecycle | Hook equivalent |
|---|---|
| componentDidMount | useEffect(() => {}, []) |
| componentDidUpdate | useEffect(() => {}, [dep]) |
| componentWillUnmount | useEffect(() => { return cleanup }, []) |
| componentDidUpdate (all) | useEffect(() => {}) — avoid |
One useEffect can combine mount + unmount + update with cleanup.
Real Example
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId)
connection.connect()
return () => connection.disconnect() // unmount + before re-run
}, [roomId]) // re-run when roomId changes (didUpdate)
}48. useEffect Hook
Theory
useEffect runs side effects after render — API calls, subscriptions, timers, DOM manipulation. It replaces class lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount).
Returns an optional cleanup function that runs before re-run or unmount.
Pros & Cons
| Pros | Cons |
|---|---|
| One hook for all side effects | Easy to create infinite loops |
| Cleanup built-in | Can become a "god effect" |
Real Example
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then(setUser)
}, [userId]) // re-fetch when userId changes
return user ? <ProfileCard user={user} /> : <Spinner />
}49. useEffect vs useMemo — Execution Order
Theory
When both have the same dependency array and deps change:
- Component function runs (render phase)
useMemoruns during render — computes cached value synchronously- JSX returned — React diffs virtual tree
- Commit phase — DOM updated
- Browser paints
useEffectruns — after paint (passive effect)
So useMemo always runs before useEffect — useMemo is part of render; useEffect is after commit.
Real Example
function Demo({ id }) {
console.log('1: render start')
const computed = useMemo(() => {
console.log('2: useMemo running')
return expensiveCalc(id)
}, [id])
useEffect(() => {
console.log('4: useEffect running')
}, [id])
console.log('3: render end, computed =', computed)
return <div>{computed}</div>
}
// When id changes:
// 1: render start → 2: useMemo → 3: render end → (paint) → 4: useEffectException: useLayoutEffect runs after DOM update but before paint — still after useMemo (which is during render).
50. useMemo
Theory
useMemo(() => compute(a, b), [a, b]) caches a computed value between renders. Recomputes only when deps change. Runs during render phase.
Use for expensive calculations — not for every value (memoization has overhead).
Real Example
function ProductList({ products, filter, sortBy }) {
const filtered = useMemo(() => {
return products.filter((p) => p.category === filter).sort((a, b) => a[sortBy] - b[sortBy])
}, [products, filter, sortBy])
return filtered.map((p) => <ProductCard key={p.id} product={p} />)
}51. useReducer
Theory
useReducer(reducer, initialState) manages complex state with predictable transitions — like Redux locally.
Pattern: (state, action) => newState. Good for forms, multi-step wizards, state machines.
Real Example
function reducer(state, action) {
switch (action.type) {
case 'ADD':
return { ...state, items: [...state.items, action.payload] }
case 'REMOVE':
return { ...state, items: state.items.filter((i) => i.id !== action.payload) }
case 'SET_LOADING':
return { ...state, loading: action.payload }
default:
return state
}
}
function Cart() {
const [state, dispatch] = useReducer(reducer, { items: [], loading: false })
return <button onClick={() => dispatch({ type: 'ADD', payload: product })}>Add</button>
}52. useRef & useRef vs useState
Theory
useRef(initial) returns { current: value } — mutating .current does not trigger re-render.
Uses: DOM refs, storing previous values, timers, mutable values across renders.
| useRef | useState | |
|---|---|---|
| Update triggers render? | No | Yes |
| Use for | DOM, timers, prev value | UI state |
Real Example
function AutoFocusInput() {
const inputRef = useRef(null)
const renderCount = useRef(0)
renderCount.current++
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
}53. useRef Hook
Theory
useRef returns a mutable object { current: value } that persists across renders without causing re-renders when changed. Uses: DOM references, storing previous values, timers, mutable flags.
Real Example
function SearchInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current?.focus() // DOM access
}, [])
return <input ref={inputRef} type="search" />
}54. useRef vs useState
Theory
| useState | useRef | |
|---|---|---|
| Triggers re-render? | Yes | No |
| Value access | Direct count | ref.current |
| Use for | UI data that affects render | DOM refs, timers, prev values |
| Async updates | Batched | Synchronous |
Real Example
function Timer() {
const [seconds, setSeconds] = useState(0)
const intervalRef = useRef(null) // no re-render needed for timer ID
const start = () => {
intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000)
}
const stop = () => clearInterval(intervalRef.current)
return (
<>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
)
}55. useState
Theory
useState(initialValue) returns [value, setter]. Initial value can be a function for lazy init: useState(() => expensive()).
Functional updates: setCount(c => c + 1) when new state depends on old.
Real Example
const [count, setCount] = useState(0)
const [user, setUser] = useState(() => JSON.parse(localStorage.getItem('user') ?? 'null'))
setCount((prev) => prev + 1)56. useState Hook
Theory
useState adds local state to function components. Returns [value, setter]. Updates are async and batched. Use functional updater when new state depends on old: setCount(c => c + 1).
Real Example
function OrderForm() {
const [quantity, setQuantity] = useState(1)
const [items, setItems] = useState<Item[]>([])
const addItem = (product: Product) => {
setItems((prev) => [...prev, { ...product, quantity }])
}
return (
<div>
<input type="number" value={quantity} onChange={(e) => setQuantity(Number(e.target.value))} />
<button onClick={() => addItem(selectedProduct)}>Add</button>
</div>
)
}57. What are Hooks? Explain useEffect, useCallback, and useMemo
Theory
Hooks are functions that let you use state, side effects, and other React features in function components — without class components. They follow the Rules of Hooks: only call at the top level, only in React functions.
| Hook | Purpose |
|---|---|
useState | Local component state |
useEffect | Side effects (API, subscriptions, DOM) |
useCallback | Memoize function reference |
useMemo | Memoize computed value |
useRef | Mutable ref without re-render |
useContext | Consume context |
Pros & Cons
| Pros | Cons |
|---|---|
| Simpler than class lifecycle | Rules of Hooks can confuse beginners |
| Reusable custom hooks | Easy to overuse useEffect |
| Less boilerplate | Stale closure bugs without proper deps |
Real-Life Example
useEffect — side effects
function LiveOrderTracker({ orderId }) {
const [status, setStatus] = useState('pending')
// Fetch on mount + when orderId changes; cleanup on unmount
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/orders/${orderId}`)
ws.onmessage = (event) => {
setStatus(JSON.parse(event.data).status)
}
ws.onerror = () => setStatus('error')
return () => ws.close() // cleanup — prevent memory leak
}, [orderId])
return <OrderStatusBadge status={status} />
}useMemo — expensive computation
function RestaurantList({ restaurants, filters }) {
// Only re-filter when restaurants or filters change
const filtered = useMemo(() => {
return restaurants
.filter((r) => !filters.cuisine || r.cuisine === filters.cuisine)
.filter((r) => !filters.minRating || r.rating >= filters.minRating)
.sort((a, b) => b.rating - a.rating)
}, [restaurants, filters])
return filtered.map((r) => <RestaurantCard key={r.id} restaurant={r} />)
}useCallback — stable function reference
const RestaurantCard = React.memo(function RestaurantCard({ restaurant, onFavorite }) {
return (
<div>
<h3>{restaurant.name}</h3>
<button onClick={() => onFavorite(restaurant.id)}>♥ Save</button>
</div>
)
})
function RestaurantList({ restaurants }) {
const [favorites, setFavorites] = useState([])
// Without useCallback, RestaurantCard re-renders every parent render
const handleFavorite = useCallback((id) => {
setFavorites((prev) => (prev.includes(id) ? prev.filter((f) => f !== id) : [...prev, id]))
}, [])
return restaurants.map((r) => <RestaurantCard key={r.id} restaurant={r} onFavorite={handleFavorite} />)
}Custom hook — combining hooks
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debounced
}
// Usage
const debouncedQuery = useDebounce(searchQuery, 300)
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery)
}, [debouncedQuery])58. What has been your experience with useCallback and useMemo in real projects?
Theory
| Hook | Memoizes | Prevents |
|---|---|---|
useCallback | Function reference | Child re-render (when passed to React.memo child) |
useMemo | Computed value | Expensive recalculation on every render |
Key insight: These are performance optimizations, not correctness tools. Use them after profiling shows a problem — not everywhere by default.
Pros & Cons
| Pros | Cons |
|---|---|
| Prevents expensive recalculations | Adds memory overhead (cached values) |
| Stabilizes references for memoized children | Shallow compare can miss deep changes |
| Fixes stale closure in effect deps | Overuse makes code harder to read |
| Essential for virtualization libraries | Premature optimization wastes dev time |
Real-Life Example — When they helped
// Project: Admin dashboard with 500-row data table
function AdminDashboard({ filters }) {
const { data: orders } = useOrders()
// useMemo — filtering 500 orders was taking 40ms per keystroke
const filteredOrders = useMemo(() => {
return orders.filter((order) => {
if (filters.status && order.status !== filters.status) return false
if (filters.city && order.city !== filters.city) return false
if (filters.search) {
return order.customerName.toLowerCase().includes(filters.search.toLowerCase())
}
return true
})
}, [orders, filters]) // only recalculate when these change
// useCallback — without this, every row re-rendered on parent render
const handleCancelOrder = useCallback((orderId: string) => {
cancelOrder(orderId)
toast.success('Order cancelled')
}, []) // stable reference
return (
<VirtualTable
rows={filteredOrders}
renderRow={(order) => <OrderRow key={order.id} order={order} onCancel={handleCancelOrder} />}
/>
)
}
const OrderRow = React.memo(function OrderRow({ order, onCancel }) {
return (
<tr>
<td>{order.id}</td>
<td>{order.customerName}</td>
<td>
<button onClick={() => onCancel(order.id)}>Cancel</button>
</td>
</tr>
)
})Real-Life Example — When NOT to use them
// ❌ Over-optimization — cheap operations
function UserGreeting({ name }) {
const greeting = useMemo(() => `Hello, ${name}`, [name]) // pointless
const handleClick = useCallback(() => console.log(name), [name]) // pointless
return <p onClick={handleClick}>{greeting}</p>
}
// ✅ Just write it simply
function UserGreeting({ name }) {
return <p onClick={() => console.log(name)}>Hello, {name}</p>
}My real project experience (answer template)
59. What is the role of the dependency array in useEffect?
Theory
The dependency array is the second argument of useEffect. It tells React when to re-run the effect:
| Dependency Array | Behavior |
|---|---|
| Omitted | Runs after every render |
[] (empty) | Runs once on mount; cleanup on unmount |
[a, b] | Runs on mount + whenever a or b changes |
| No array at all | Runs after every render (rarely intended) |
React compares dependencies using Object.is (shallow equality). If all deps are the same reference/value, the effect is skipped.
Pros & Cons of strict dependency rules
| Pros | Cons |
|---|---|
| Prevents stale closures | ESLint exhaustive-deps can feel noisy |
| Ensures effects sync with latest state | Easy to cause infinite loops with objects |
| Makes effect triggers explicit | Requires understanding reference equality |
Real-Life Example
function OrderTracker({ orderId }) {
const [status, setStatus] = useState('loading')
// [] — run once on mount (fetch initial data)
useEffect(() => {
fetchOrder(orderId).then(setStatus)
}, []) // ⚠️ Bug: orderId not in deps — stale if orderId changes
// ✅ Correct — re-fetch when orderId changes
useEffect(() => {
setStatus('loading')
fetchOrder(orderId).then(setStatus)
}, [orderId])
// [query] — re-run search when query changes
useEffect(() => {
const controller = new AbortController()
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then((r) => r.json())
.then(setResults)
return () => controller.abort() // cleanup prevents race condition
}, [query])
// WebSocket — setup on mount, cleanup on unmount
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/orders/${orderId}`)
ws.onmessage = (e) => setStatus(JSON.parse(e.data).status)
return () => ws.close() // cleanup — prevents memory leak
}, [orderId])
}Common mistakes
// ❌ Missing dependency — stale closure
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1) // always uses initial count (0)
}, 1000)
return () => clearInterval(interval)
}, [])
// ✅ Fix 1 — functional updater (no dep needed)
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
// ✅ Fix 2 — add count to deps
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(interval)
}, [count])
// ❌ Object/array in deps — infinite loop (new reference every render)
useEffect(() => {
fetchData(filters)
}, [filters]) // if filters = { status: "active" } created inline → infinite loop
// ✅ Fix — memoize or use primitive deps
const filters = useMemo(() => ({ status, city }), [status, city])
useEffect(() => {
fetchData(filters)
}, [filters])Cleanup function role
The return value of useEffect is a cleanup function that runs:
- Before the effect re-runs (deps changed)
- When the component unmounts
Use cleanup for: timers, subscriptions, event listeners, WebSockets, AbortControllers.
Interview answer (concise)
| # | Topic | One-liner |
|---|---|---|
| 1 | React & efficiency | Component-based, Virtual DOM, batched updates, Fiber |
| 2 | React internals | Fiber → render phase → commit → effects |
| 3 | Challenging task | STAR method, quantify impact, show trade-offs |
| 4 | JS coupling | Flexible language; loose coupling via modules, DI, composition |
| 5 | TypeScript | Static types, catch bugs early, better tooling |
| 6 | type vs interface | interface = objects + merging; type = unions + utilities |
| 7 | Redux flow | dispatch → reducer → store → UI; RTK simplifies |
| 8 | RTK vs Query | RTK = client state; Query = server state |
| 9 | bind vs apply | apply = invoke now with array args; bind = returns new function |
| 10 | useMemo/useCallback | Profile first; memoize expensive work + stable child refs |
| 11 | useEffect deps | Controls re-run timing; cleanup prevents leaks |
60. What is useRef?
Answer
useRef is a hook that returns a mutable ref object whose .current property persists across renders. Useful for accessing DOM nodes or keeping mutable variables.
const inputRef = useRef()
;<input ref={inputRef} />61. Why Do We Use Custom Hooks?
Theory
Custom hooks are functions starting with use that compose built-in hooks to extract
reusable stateful logic. They share logic, not state — each call gets its
own state.
Why use them?
| Reason | Example |
|---|---|
| Reuse logic | useFetch, useDebounce, useLocalStorage |
| Separate concerns | UI component stays clean; logic in hook |
| Testability | Test hook with @testing-library/react-hooks |
| Readability | Component reads like a spec: useAuth(), useCart() |
| Replace HOCs/render props | Modern pattern for shared behavior |
Real Example
// Custom hook — logic
function useLocalStorage(key, initialValue) {
const [stored, setStored] = useState(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value) => {
const next = value instanceof Function ? value(stored) : value
setStored(next)
localStorage.setItem(key, JSON.stringify(next))
}
return [stored, setValue]
}
// Component — UI only
function SettingsPage() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
)
}Rules Reminder
- Name must start with
use - Can call other hooks inside
- Same Rules of Hooks — top level only, no conditions
Rendering & Reconciliation
62. CSR vs SSR
Theory
| CSR | SSR | |
|---|---|---|
| Render location | Browser | Server |
| First HTML | Empty shell + JS bundle | Full HTML with data |
| SEO | Poor without extra work | Excellent |
| TTFB | Fast | Slower (server work) |
| Interactivity | After JS loads | After hydration |
| Best for | Dashboards, auth apps | SEO pages, public content |
Real Example
// CSR — Vite/CRA SPA
// index.html: <div id="root"></div>
// User sees blank → JS downloads → React renders
// SSR — Next.js
export async function getServerSideProps() {
const products = await fetchProducts()
return { props: { products } }
}
export default function Page({ products }) {
return <ProductList data={products} /> // HTML sent with data
}63. CSR vs SSR vs SSG & Hydration
Theory
| CSR | SSR | SSG | |
|---|---|---|---|
| Render | Browser | Server per request | Build time |
| First paint | Slow (JS first) | Fast (HTML first) | Fastest |
| SEO | Poor | Good | Good |
| Data freshness | Client fetch | Per request | Rebuild to update |
Hydration: SSR/SSG sends HTML; React attaches event listeners and state on the client without re-creating DOM.
Real Example
// Next.js SSR
export async function getServerSideProps() {
const posts = await fetchPosts()
return { props: { posts } }
}
// Next.js SSG
export async function getStaticProps() {
const posts = await fetchPosts()
return { props: { posts }, revalidate: 60 } // ISR
}64. CSR vs SSR vs SSG vs ISR
Theory
Four rendering strategies for web applications:
| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| When rendered | Browser | Server per request | Build time | Build + revalidate |
| HTML on first load | Empty shell | Full HTML | Full HTML | Full HTML (cached) |
| SEO | Poor | Excellent | Excellent | Excellent |
| TTFB | Fast | Slower | Fast (CDN) | Fast (CDN) |
| Data freshness | Client fetch | Always fresh | Stale until rebuild | Configurable TTL |
| Server cost | Low | High | Low | Low |
Pros & Cons
| Strategy | Best for |
|---|---|
| CSR | Dashboards, authenticated apps |
| SSR | Personalized, SEO-critical, always-fresh |
| SSG | Blogs, marketing, docs |
| ISR | E-commerce listings, large catalogs |
Real-Life Example
// CSR — Create React App / Vite SPA
// index.html: <div id="root"></div>
// JS downloads → React renders in browser
// SSR — Next.js getServerSideProps
export async function getServerSideProps({ params }) {
const product = await fetchProduct(params.id)
return { props: { product } }
}
// SSG — Next.js getStaticProps (build time)
export async function getStaticProps() {
const posts = await fetchAllPosts()
return { props: { posts } }
}
// ISR — revalidate every 60 seconds
export async function getStaticProps() {
const products = await fetchProducts()
return { props: { products }, revalidate: 60 }
}Decision tree:
Need SEO? → No → CSR
Need SEO? → Yes → Data changes frequently? → Yes → SSR or ISR
→ No → SSG65. Explain React Fiber Architecture
Theory
Fiber is React's reconciliation engine (React 16+). Each component, DOM node, or hook has a Fiber node — a JS object representing a unit of work.
Why Fiber replaced the old stack reconciler
| Old Stack Reconciler | Fiber |
|---|---|
| Synchronous, recursive | Incremental, interruptible |
| Blocks main thread | Can pause and resume |
| No prioritization | Priority lanes (urgent vs deferred) |
Fiber node structure (simplified)
{
type: 'div', // component type
stateNode: domNode, // real DOM node (for host components)
child: Fiber, // first child
sibling: Fiber, // next sibling
return: Fiber, // parent
alternate: Fiber, // previous version (double buffering)
pendingProps: {},
memoizedState: {}, // hooks linked list lives here
flags: Placement, // side effects to commit
}Two phases
┌─────────────────────────────────────────────────────────┐
│ RENDER PHASE (interruptible) │
│ - Walk Fiber tree │
│ - Call component functions │
│ - Diff children │
│ - Mark effects (Placement, Update, Deletion) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ COMMIT PHASE (synchronous, cannot interrupt) │
│ 1. Before mutation (getSnapshotBeforeUpdate) │
│ 2. Mutation (apply DOM changes) │
│ 3. Layout (useLayoutEffect) │
│ → then passive effects (useEffect) run async │
└─────────────────────────────────────────────────────────┘Concurrent features enabled by Fiber
- Time slicing — split work across frames
useTransition— mark updates as low prioritySuspense— pause rendering while data loads- Offscreen / Activity — hide UI without unmounting
Interview answer
66. Explain React's rendering lifecycle from state update to DOM update
Full flow
1. Event handler calls setState / dispatch
↓
2. Update enqueued on Fiber (priority lane assigned)
↓
3. RENDER PHASE begins
- React calls component function
- Hooks run (useState returns new value)
- Children reconciled (diff algorithm)
- Effect flags marked (Placement, Update, Deletion)
↓
4. COMMIT PHASE
a. Before Mutation: getSnapshotBeforeUpdate (class)
b. Mutation: apply DOM insert/update/delete
c. Layout: flushSync useLayoutEffect callbacks
↓
5. Browser paints screen
↓
6. Passive effects: useEffect callbacks run (async, after paint)Practical Example — Effect order
function Demo() {
const [count, setCount] = useState(0)
useLayoutEffect(() => {
console.log('1: layout effect — runs after DOM update, before paint')
}, [count])
useEffect(() => {
console.log('2: passive effect — runs after paint')
}, [count])
console.log('3: render')
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// Click output:
// 3: render
// 1: layout effect
// (browser paints)
// 2: passive effectClass component lifecycle mapping
| Class | Hooks equivalent |
|---|---|
render | Function body |
componentDidMount | useEffect(fn, []) |
componentDidUpdate | useEffect(fn, [deps]) |
componentWillUnmount | useEffect cleanup |
getSnapshotBeforeUpdate | useLayoutEffect |
67. Explain Virtual DOM and its comparison mechanism
Theory
The Virtual DOM is a lightweight JavaScript representation of the real DOM tree. React creates a new VDOM tree on each render, compares it with the previous tree (diffing), and computes the minimum set of DOM mutations needed.
Diffing rules:
- Different element types → tear down old subtree, build new one
- Same type → update props only, recurse into children
- Keys identify list items across renders for efficient reordering
Pros & Cons
| Pros | Cons |
|---|---|
| Declarative UI — you describe state, React patches DOM | Abstraction overhead vs direct DOM manipulation |
| Batched updates — fewer DOM operations | Not always faster than hand-optimized DOM |
| Cross-browser consistency | Diffing large trees has CPU cost |
Real-Life Example
// Before render: <ul><li key="a">Apple</li><li key="b">Banana</li></ul>
// After render: <ul><li key="b">Banana</li><li key="a">Apple</li></ul>
// React sees same types (ul, li), same keys (a, b)
// → Only reorders DOM nodes, does NOT destroy/recreate <li> elements
// → Component state inside each <li> is preserved
// Without keys (or index keys), inserting at top causes:
// → React thinks every item changed → full re-render of all children68. Flexbox vs Grid — key differences
Theory
Both are CSS layout systems, but they solve different problems:
- Flexbox — one-dimensional layout (row or column). Content-driven sizing.
- Grid — two-dimensional layout (rows and columns). Layout-driven sizing.
Comparison
| Feature | Flexbox | Grid |
|---|---|---|
| Dimensions | 1D (row or column) | 2D (rows + columns) |
| Control | Content dictates space | Template dictates space |
| Alignment | Excellent for distributing items | Excellent for page layouts |
| Gap support | gap | gap |
| Best for | Navbars, card rows, centering | Page layouts, dashboards, galleries |
| Item stretching | flex: 1 | 1fr |
Pros & Cons
| Flexbox | Grid |
|---|---|
| ✅ Simple for component-level layout | ✅ Powerful for full page structure |
| ✅ Great for unknown item count | ✅ Precise column/row control |
| ❌ Awkward for complex 2D layouts | ❌ Overkill for a single row of buttons |
| ❌ Steeper learning curve |
Real-Life Example
/* Flexbox — navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0 1.5rem;
}
.nav-links {
display: flex;
gap: 1.5rem;
list-style: none;
}
/* Flexbox — center a modal */
.modal-overlay {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* Flexbox — card with footer pushed to bottom */
.product-card {
display: flex;
flex-direction: column;
height: 100%;
}
.product-card .footer {
margin-top: auto; /* pushes footer down */
}/* Grid — dashboard layout */
.dashboard {
display: grid;
grid-template-rows: 64px 1fr;
grid-template-columns: 240px 1fr;
grid-template-areas:
'sidebar header'
'sidebar main';
min-height: 100vh;
}
.sidebar {
grid-area: sidebar;
}
.header {
grid-area: header;
}
.main {
grid-area: main;
}
/* Grid — responsive product gallery */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
padding: 1.5rem;
}
/* Grid — form with aligned labels */
.form-row {
display: grid;
grid-template-columns: 120px 1fr;
align-items: center;
gap: 0.5rem 1rem;
}// When to use which — decision guide
// Flexbox: toolbar, button group, horizontal list, vertical card stack
// Grid: page shell, image gallery, data table layout, complex forms
// Both together: Grid for page, Flexbox inside each grid cellInterview answer (concise)
69. How does React reconciliation work?
Theory
Reconciliation is React's process of comparing the new virtual DOM tree with the previous one and determining the minimum set of DOM operations needed to update the UI.
Key concepts
- Virtual DOM — Lightweight JS representation of the UI tree.
- Diffing algorithm — Heuristic O(n) comparison (not full tree diff).
- Fiber architecture — Each unit of work is a Fiber node; enables incremental rendering, pausing, and prioritization.
- Render phase vs Commit phase
- Render: Build/update Fiber tree, mark side effects (can be interrupted).
- Commit: Apply DOM mutations, run layout effects, then passive effects.
Diffing heuristics
| Rule | Implication |
|---|---|
| Different element types → tear down subtree, rebuild | <div> → <span> replaces entire subtree |
| Same type → update props, recurse children | <User name="A" /> → <User name="B" /> |
| Keys identify stable identity across renders | Enables efficient list reordering |
flowchart LR
A[State/Props Change] --> B[Render Phase]
B --> C[Build New Fiber Tree]
C --> D[Diff with Previous Tree]
D --> E[Mark DOM Mutations]
E --> F[Commit Phase]
F --> G[Apply DOM Updates]
F --> H[Run useLayoutEffect]
F --> I[Run useEffect]Practical Example — Why type matters
// React destroys the entire subtree when element type changes
function App({ showList }) {
return showList ? (
<ul>
<li>A</li>
</ul>
) : (
<div>No items</div>
)
// Switching between <ul> and <div> = full unmount/remount
}Concurrent features (4+ YOE depth)
- Time slicing: Break work into chunks so the main thread stays responsive.
- Priority lanes: Urgent updates (typing) beat non-urgent (data fetch results).
useTransition/useDeferredValue: Mark updates as low priority.
70. Keys in React Lists
Theory
Keys give list items a stable identity across re-renders. React uses keys to match, move, insert, or delete efficiently. Use unique IDs from data — not array index for mutable lists.
Pros & Cons
| Stable ID keys | Index keys |
|---|---|
| ✅ Preserves component state | ❌ State slides on delete/reorder |
| ✅ Efficient DOM moves | ❌ Unnecessary re-renders |
Real Example
// ✅ Good
{
todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)
}
// ❌ Bad — index keys on mutable list
{
todos.map((todo, i) => <TodoItem key={i} todo={todo} />)
}71. Next.js SSR vs CSR vs ISR
Theory — real trade-offs, not definitions
| CSR | SSR | ISR | |
|---|---|---|---|
| First byte | Fast (empty shell) | Slower (server work) | Fast (CDN cached) |
| SEO | Poor without extra work | Excellent | Excellent |
| Data freshness | Client fetch | Always current | TTL-based stale |
| Server cost | Low | High at scale | Low |
| TTFB | Low | Higher | Low (edge) |
| Personalization | Easy (client) | Per-request | Hard (cached) |
| a fintech app use case | Dashboard post-login | Account settings | Credit score landing |
Pros & Cons — when a fintech app would choose each
| CSR | SSR | ISR |
|---|---|---|
| ✅ Post-auth app screens | ✅ User-specific bill pay page | ✅ Marketing / SEO pages |
| ✅ No server render cost | ✅ Always fresh balance | ✅ 1M users, same HTML |
| ❌ Blank until JS loads | ❌ Server spike at peak | ❌ Stale for TTL window |
| ❌ Bad for crawlers | ❌ TTFB dependency | ❌ Invalidation complexity |
Real-Life Example
// CSR — a fintech app app dashboard (authenticated, no SEO need)
'use client'
export default function Dashboard() {
const { data } = useQuery({ queryKey: ['rewards'], queryFn: fetchRewards })
return <RewardsCard data={data} />
}
// SSR — personalized credit card bill (must be fresh, user-specific)
export async function getServerSideProps({ req }) {
const bill = await fetchBill(req.cookies.session)
return { props: { bill } }
}
// ISR — credit score explainer page (SEO, same for all, refresh hourly)
export async function getStaticProps() {
const content = await fetchCMSContent('credit-score-guide')
return { props: { content }, revalidate: 3600 }
}Interview answer (concise)
72. Optimize 10+ fields — re-renders
Theory
With one state object, every keystroke in any field re-renders the entire form and all 10+ field components.
Fields that cause unnecessary re-renders:
- All fields when using single
useState({ ...10 fields }) - Parent re-render → all unmemoized children re-render
- Context provider wrapping form with changing value
- Inline object/function props to child fields
Pros & Cons
| React Hook Form (uncontrolled) | Single useState object |
|---|---|
| ✅ Field change doesn't re-render form | ❌ Every keystroke re-renders all fields |
| ✅ Scales to 50+ fields | ✅ Simple mental model |
| Industry standard for large forms | OK for <5 fields |
Real-Life Example
// ❌ BAD — typing in field 1 re-renders fields 1-10
function BigForm() {
const [form, setForm] = useState({ f1: "", f2: "", ... f10: "" });
return (
<>
{FIELDS.map((f) => (
<input
key={f}
value={form[f]}
onChange={(e) => setForm({ ...form, [f]: e.target.value })}
/>
))}
</>
);
}
// ✅ GOOD — memoized field components
const FormField = React.memo(function FormField({
label, value, error, onChange, onBlur,
}: FieldProps) {
return (
<div>
<label>{label}</label>
<input value={value} onChange={(e) => onChange(e.target.value)} onBlur={onBlur} />
{error && <span role="alert">{error}</span>}
</div>
);
});
// ✅ BETTER — React Hook Form (a fintech app production pattern)
import { useForm } from "react-hook-form";
function OptimizedKYCForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("pan", { validate: validatePAN })} />
{errors.pan && <span>{errors.pan.message}</span>}
{/* Only error display re-renders — not entire form */}
</form>
);
}What to say in interview: "I'd start with isolated field components + memo. For 10+ fields I'd switch to React Hook Form because it uses refs — field keystrokes don't re-render siblings."
73. React Fiber
Theory
Fiber is React's reconciliation engine (React 16+). It enables:
- Incremental rendering — split work into units, pause/resume
- Priority scheduling — user input before background work
- Concurrent features — Suspense, transitions, useDeferredValue
Each fiber node represents a component instance with work-in-progress state.
Real Example
// Concurrent features built on Fiber
const [isPending, startTransition] = useTransition()
function handleSearch(query) {
setInput(query) // urgent — input stays responsive
startTransition(() => setResults(search(query))) // low priority — can be interrupted
}74. React Internals & Rendering Lifecycle
Theory
When state changes, React runs a render pass (pure, can be interrupted) and a commit pass (DOM updates, synchronous).
Trigger (setState, dispatch, parent re-render)
↓
Render phase (Fiber work loop — interruptible)
→ Call component functions / hooks
→ Build new element tree
→ Diff with previous tree (reconciliation)
↓
Commit phase (synchronous)
→ Apply DOM mutations
→ useLayoutEffect
↓
Browser paint
↓
useEffect (passive, after paint)| Phase | Methods |
|---|---|
| Mount | constructor → render → componentDidMount |
| Update | render → componentDidUpdate |
| Unmount | componentWillUnmount |
| Class | Hooks |
|---|---|
componentDidMount | useEffect(() => {}, []) |
componentDidUpdate | useEffect(() => {}, [deps]) |
componentWillUnmount | useEffect(() => () => cleanup, []) |
shouldComponentUpdate | React.memo, useMemo |
Real Example — Lifecycle in hooks
function LiveOrderMap({ orderId }: { orderId: string }) {
const [location, setLocation] = useState<Coords | null>(null)
// Mount + update when orderId changes
useEffect(() => {
const ws = new WebSocket(`${WS_URL}/orders/${orderId}/location`)
ws.onmessage = (e) => setLocation(JSON.parse(e.data))
// Unmount cleanup
return () => ws.close()
}, [orderId])
// Runs after DOM update, before paint — measure DOM
useLayoutEffect(() => {
if (!location) return
centerMapOn(location)
}, [location])
return <MapMarker coords={location} />
}75. React reconciliation — what to update?
Theory
React reconciliation compares the new element tree with the previous Fiber tree and decides the minimum DOM operations.
Update rules:
- Different type (
div→span) → destroy entire subtree, rebuild - Same type → update props on DOM node, recurse children
- Same type + same key → update in place, preserve state
- Keys differ in lists → unmount old, mount new (or move if key matches)
React bails out if Object.is(oldState, newState) for state, or if React.memo shallow-compares equal props.
Pros & Cons
| Understanding reconciliation | Ignorance |
|---|---|
| ✅ Know why keys matter | ❌ Index keys cause state bugs |
| ✅ Targeted memoization | ❌ Random React.memo everywhere |
| ✅ Predict render behavior | ❌ Unnecessary DOM work |
Real-Life Example
// Case 1: Same type — UPDATE props only
// Before: <div className="card"><h2>₹500</h2></div>
// After: <div className="card active"><h2>₹500</h2></div>
// → Only className updated on div
// Case 2: Different type — REPLACE subtree
// Before: <div><TransactionList /></div>
// After: <section><TransactionList /></section>
// → TransactionList unmounts and remounts — state lost
// Case 3: Keys in list
// Before: [<Row key="a" />, <Row key="b" />]
// After: [<Row key="b" />, <Row key="a" />]
// → React MOVES nodes — state preserved
// Case 4: Bailout
function Counter() {
const [n, setN] = useState(0)
return <button onClick={() => setN(0)}>{n}</button> // click at 0 → no re-render
}Interview answer (concise)
76. SSR vs CSR with examples and use-cases
Theory
- CSR (Client-Side Rendering): Server sends empty HTML + JS bundle. Browser downloads JS, executes it, fetches data, renders UI.
- SSR (Server-Side Rendering): Server runs React, generates full HTML with data, sends to browser. Browser hydrates interactivity on top.
Pros & Cons
| CSR | SSR | |
|---|---|---|
| Pros | Rich interactivity, simpler hosting, SPA navigation | Fast FCP, SEO-friendly, works without JS |
| Cons | Slow initial load, poor SEO without extra work | Server cost, hydration complexity, TTFB dependency |
| Best for | Dashboards, admin panels, authenticated apps | Marketing pages, e-commerce listings, blogs |
Real-Life Example
CSR — a food-delivery platform order tracking (authenticated, post-login):
Browser → GET /app → empty <div id="root"> → download 500KB JS
→ fetch /api/orders → render tracking UI
→ User sees spinner for 2-3 seconds
SSR — a food-delivery platform restaurant listing (SEO-critical):
Browser → GET /restaurants/delhi
→ Server runs React → fetches restaurant data
→ Returns full HTML with restaurant cards
→ User sees content in <1s → JS hydrates for filters/sort// Next.js SSR example
export async function getServerSideProps() {
const restaurants = await fetchRestaurants('delhi')
return { props: { restaurants } }
}
export default function RestaurantPage({ restaurants }) {
return <RestaurantList data={restaurants} />
}77. Tell me about your key skills
Theory
This is an opening behavioral question. For a Senior React role at Engineo, structure your answer around technical depth, leadership, and business impact — not a laundry list of buzzwords.
Use the T-shaped model: deep in React/JS frontend, broad in related areas (APIs, testing, CI/CD).
What to include
| Category | Examples to mention |
|---|---|
| Core | React, TypeScript, JavaScript (ES6+) |
| State | Redux Toolkit, TanStack Query, Context |
| Styling | CSS Modules, Tailwind, responsive design |
| Testing | Jest, React Testing Library, Playwright |
| Tools | Git, Webpack/Vite, CI/CD |
| Soft | Code review, mentoring, agile, cross-team collaboration |
Real Example — Sample answer (60 seconds)
Tip: Tailor to the job description. Mention skills Engineo likely needs (enterprise apps, Redux, auth).
78. this Keyword
Theory
this refers to the execution context — who called the function. Its value depends on how the function is invoked, not where it's defined.
| Invocation | this value |
|---|---|
| Global / standalone | undefined (strict) or window |
| Object method | The object |
call / apply / bind | Explicitly set |
new keyword | Newly created instance |
| Arrow function | Inherited from enclosing lexical scope |
| Event handler | The element (usually) |
Pros & Cons
| Explicit binding (bind/call) | Arrow functions (lexical this) |
|---|---|
✅ Full control over this | ✅ No this confusion in callbacks |
| ❌ Verbose, easy to forget | ❌ Can't use as constructors |
❌ Wrong this if you need dynamic binding |
Real-Life Example
const checkout = {
items: ['Biryani', 'Naan'],
total: 348,
printReceipt() {
console.log(this.total) // 348 — method call
},
processItems: function () {
this.items.forEach(function (item) {
// console.log(this.total); // undefined — `this` is lost in callback
})
this.items.forEach((item) => {
console.log(this.total) // 348 — arrow inherits `this` from processItems
})
},
}
checkout.printReceipt()
// Explicit binding
function formatPrice(amount) {
return `${this.currency}${amount}`
}
formatPrice.call({ currency: '₹' }, 299) // "₹299"
formatPrice.apply({ currency: '$' }, [49]) // "$49"
const formatINR = formatPrice.bind({ currency: '₹' })
formatINR(199) // "₹199"
// React class component (legacy)
class Counter extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({ count: this.state.count + 1 })
}
}79. To-Do List (Vanilla JS / React) — Optimize Re-renders
Theory
Todo list tests: CRUD operations, state management, and re-render optimization in React. Avoid re-rendering entire list when one item changes — use stable keys, memoized item components, and immutable updates.
Vanilla JS Implementation
<div id="app">
<input id="input" placeholder="Add todo..." />
<button id="add">Add</button>
<ul id="list"></ul>
</div>class TodoApp {
constructor() {
this.todos = []
this.listEl = document.getElementById('list')
this.inputEl = document.getElementById('input')
document.getElementById('add').onclick = () => this.add()
this.inputEl.onkeydown = (e) => e.key === 'Enter' && this.add()
}
add() {
const text = this.inputEl.value.trim()
if (!text) return
this.todos.push({ id: crypto.randomUUID(), text, done: false })
this.inputEl.value = ''
this.render()
}
toggle(id) {
this.todos = this.todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
this.render()
}
remove(id) {
this.todos = this.todos.filter((t) => t.id !== id)
this.render()
}
render() {
this.listEl.innerHTML = this.todos
.map(
(t) => `
<li data-id="${t.id}">
<input type="checkbox" ${t.done ? 'checked' : ''} />
<span style="${t.done ? 'text-decoration:line-through' : ''}">${t.text}</span>
<button class="delete">×</button>
</li>`,
)
.join('')
this.listEl.querySelectorAll('li').forEach((li) => {
const id = li.dataset.id
li.querySelector('input').onchange = () => this.toggle(id)
li.querySelector('.delete').onclick = () => this.remove(id)
})
}
}
new TodoApp()React — Optimized Re-renders
type Todo = { id: string; text: string; done: boolean }
// Memoized item — only re-renders when its props change
const TodoItem = React.memo(function TodoItem({
todo,
onToggle,
onRemove,
}: {
todo: Todo
onToggle: (id: string) => void
onRemove: (id: string) => void
}) {
console.log('render', todo.id) // verify only changed item renders
return (
<li>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>{todo.text}</span>
<button onClick={() => onRemove(todo.id)}>×</button>
</li>
)
})
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([])
const [input, setInput] = useState('')
const addTodo = useCallback(() => {
const text = input.trim()
if (!text) return
setTodos((prev) => [...prev, { id: crypto.randomUUID(), text, done: false }])
setInput('')
}, [input])
const toggleTodo = useCallback((id: string) => {
setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)))
}, [])
const removeTodo = useCallback((id: string) => {
setTodos((prev) => prev.filter((t) => t.id !== id))
}, [])
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} onRemove={removeTodo} />
))}
</ul>
</div>
)
}What to say: "React.memo on TodoItem + useCallback on handlers + stable UUID keys = only the toggled item re-renders."
80. Use unique keys while rendering lists
Theory
Keys give React a stable identity for list items across re-renders. They help React match, move, insert, or delete elements efficiently without destroying component state.
Use stable, unique IDs from your data (database ID, UUID). Avoid array index as key when the list can change order, insert, or delete items.
Pros & Cons
| Stable unique keys | Index keys |
|---|---|
| ✅ Preserves component state on reorder | ❌ State "slides" to wrong item on delete |
| ✅ Efficient DOM moves | ❌ Unnecessary re-renders |
| ✅ Correct animations | ❌ Broken enter/exit transitions |
Real-Life Example
// ❌ Index keys on mutable list
function TodoList({ todos, onDelete }) {
return todos.map((todo, index) => <TodoItem key={index} todo={todo} onDelete={() => onDelete(index)} />)
// Delete first item → all keys shift → every item re-mounts with wrong state
}
// ✅ Stable ID keys
function TodoList({ todos, onDelete }) {
return todos.map((todo) => <TodoItem key={todo.id} todo={todo} onDelete={() => onDelete(todo.id)} />)
}
// When index keys ARE acceptable:
// - Static list that never reorders
// - No local state inside list items
// - No animations81. Virtual DOM
Theory
The Virtual DOM is a lightweight JavaScript copy of the real DOM. On each state change, React builds a new VDOM tree, compares it with the previous one (diffing), and updates only the changed parts of the real DOM.
This is faster than rewriting the entire page.
Pros & Cons
| Pros | Cons |
|---|---|
| Efficient batched updates | CPU cost for diffing |
| Declarative — you describe state, not DOM ops | Not always faster than hand-tuned DOM |
Real Example
// You write:
<p className={isActive ? 'active' : ''}>{count}</p>
// React diffs: only className and text node update — rest of page untouched82. Virtual DOM, Hydration, Reconciliation & Scheduling
Theory
| Concept | What it is |
|---|---|
| Virtual DOM | Lightweight JS representation of UI (React elements / Fiber nodes) |
| Reconciliation | Diffing old vs new tree; keys identify stable identity in lists |
| Hydration | Attaching event listeners to server-rendered HTML (SSR) |
| Scheduling | Priority lanes — user input > transitions > idle work |
- Same component type → update props, recurse children
- Different type → unmount old, mount new
- Lists → use stable
key(not index if list reorders)
Hydration mismatch — server HTML ≠ client first render → React warns and may re-render client-side.
Real Example — Keys and reconciliation
// ❌ Index keys — reordering breaks state
{
items.map((item, i) => <Row key={i} item={item} />)
}
// ✅ Stable id keys
{
items.map((item) => <Row key={item.id} item={item} />)
}startTransition
import { useState, useTransition } from 'react'
function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Item[]>([])
const [isPending, startTransition] = useTransition()
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value
setQuery(value) // urgent — input stays responsive
startTransition(() => {
setResults(filterHeavyList(value)) // low priority
})
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <span>Updating…</span>}
<ResultList items={results} />
</>
)
}// ❌ Mismatch — Date.now() differs server vs client
function Bad() {
return <p>{Date.now()}</p>
}
// ✅ Client-only value after mount
function Good() {
const [now, setNow] = useState<number | null>(null)
useEffect(() => setNow(Date.now()), [])
return <p>{now ?? '—'}</p>
}83. What is the Virtual DOM?
Theory
The Virtual DOM (VDOM) is a lightweight JavaScript object tree that mirrors the real DOM structure. React keeps a VDOM representation in memory and, on each state change:
- Creates a new VDOM tree
- Diffs it against the previous tree (reconciliation)
- Applies only the minimum DOM mutations needed
This is faster than rewriting the entire DOM because DOM operations are expensive; JS object comparison is cheap.
Pros & Cons
| Pros | Cons |
|---|---|
| Declarative UI — describe state, React patches DOM | CPU overhead for diffing large trees |
| Batched updates — fewer DOM operations | Not always faster than hand-optimized DOM |
| Cross-browser consistency | Abstraction can hide performance issues |
| Enables React Native (same model, native renderer) | Learning curve for reconciliation rules |
Real-Life Example
// You write declarative UI
function OrderStatus({ order }) {
return (
<div className="status">
<h2>Order #{order.id}</h2>
<p className={order.status}>{order.statusLabel}</p>
</div>
)
}
// When order.status changes "pending" → "delivered":
// VDOM diff sees: same <div>, same <h2>, <p> className changed
// Real DOM: only updates className on <p> — nothing else touchedDiffing rules (mention in interview):
- Different element type → destroy old subtree, build new
- Same type → update props, recurse children
- Keys help match list items efficiently across re-renders
84. Why are keys important in React lists?
Theory
Keys give React a stable identity for elements in a list across re-renders. Without keys (or with bad keys like array index), React may:
- Reuse wrong component instances
- Lose internal state
- Trigger unnecessary re-renders
- Cause subtle UI bugs
Bad vs Good keys
// ❌ Index as key — breaks on insert/delete/reorder
{
items.map((item, index) => <TodoItem key={index} item={item} />)
}
// ✅ Stable unique ID from data
{
items.map((item) => <TodoItem key={item.id} item={item} />)
}Practical Example — State loss with index keys
function TodoItem({ item }) {
const [editing, setEditing] = useState(false)
return (
<div>
{editing ? <input defaultValue={item.text} /> : <span>{item.text}</span>}
<button onClick={() => setEditing(!editing)}>Edit</button>
</div>
)
}
// If you delete the first item and keys are index-based,
// the second item "inherits" the first item's editing state.When index keys are acceptable
- Static lists that never reorder
- No component state inside list items
- No animations depending on identity
85. Why do keys matter in React and how do they improve performance?
Theory
Keys are stable identifiers that tell React which items in a list correspond between renders. They enable React to match, move, insert, or delete elements efficiently instead of re-creating the entire list.
Without stable keys, React falls back to index-based matching, which breaks when items are inserted, deleted, or reordered.
Pros & Cons
| Good keys (unique IDs) | Bad keys (array index) |
|---|---|
| Preserve component state on reorder | State "slides" to wrong item on delete |
| Minimal DOM operations | Unnecessary unmount/remount |
| Correct animations | Broken enter/exit transitions |
Real-Life Example
// ❌ Bad — index keys on a mutable todo list
{
todos.map((todo, index) => <TodoItem key={index} todo={todo} />)
}
// Delete first todo → every item below gets new key → all re-render, state bleeds
// ✅ Good — stable ID from database
{
todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)
}
// Delete first todo → only that item unmounts, rest stay intact86. You need to display frequently changing stock market data on a page. Would you use SSR, SSG, or CSR? Why?
Description
Stock prices change every second (or faster). Pre-rendered HTML from build time (SSG) is stale immediately. The choice depends on freshness vs SEO vs interactivity.
Theory
| Strategy | Freshness | Best for stock data? |
|---|---|---|
| SSG | Stale until rebuild | ❌ Wrong for live prices |
| SSR | Fresh per request | ⚠️ OK for initial snapshot, expensive at scale |
| CSR | Fresh via client fetch/WebSocket | ✅ Best for live ticks |
| Hybrid | SSR shell + CSR/WebSocket updates | ✅ Recommended |
Recommended pattern: SSR (or static shell) for layout + SEO metadata, then client-side polling or WebSocket for live quotes.
Pros & Cons
| Hybrid (SSR shell + client stream) | Pure SSR every tick |
|---|---|
| ✅ Fast first paint, live updates | ❌ Server hammered on every price change |
| ✅ Scales via CDN for static parts | ❌ High TTFB under load |
| ❌ Client JS required for numbers | ❌ No benefit over WebSocket |
Real Example — NSE / trading dashboard
// app/markets/page.tsx — Server Component: static shell + initial snapshot
export const dynamic = 'force-dynamic' // or short revalidate
async function getMarketSnapshot() {
const res = await fetch(`${process.env.API_URL}/quotes/snapshot`, {
cache: 'no-store', // always fresh on each page load
})
return res.json()
}
export default async function MarketsPage() {
const snapshot = await getMarketSnapshot()
return (
<main>
<h1>Live Markets</h1>
{/* Client island — WebSocket updates after hydration */}
<LiveTicker initialQuotes={snapshot.quotes} />
</main>
)
}// components/live-ticker.tsx — Client Component
'use client'
import { useEffect, useState } from 'react'
export function LiveTicker({ initialQuotes }) {
const [quotes, setQuotes] = useState(initialQuotes)
useEffect(() => {
const ws = new WebSocket(process.env.NEXT_PUBLIC_WS_URL!)
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
setQuotes((prev) => mergeQuote(prev, update))
}
return () => ws.close()
}, [])
return (
<table>
<tbody>
{quotes.map((q) => (
<tr key={q.symbol}>
<td>{q.symbol}</td>
<td>{q.price}</td>
<td className={q.change >= 0 ? 'text-green' : 'text-red'}>{q.changePercent}%</td>
</tr>
))}
</tbody>
</table>
)
}Why not SSG? Build-time prices are wrong the moment the user opens the page.
Why not pure CSR? You lose fast meaningful HTML and SEO for /markets landing.
State Management
87. Avoid unnecessary prop drilling
Theory
Prop drilling is passing data through intermediate components that don't need it. One-to-two levels is fine and explicit. Beyond that, use Context API, component composition, or a state library (Zustand, Redux).
Choose the simplest solution that fits:
- Theme/locale → Context
- Auth session → Context or Zustand
- Complex global state → Redux Toolkit
- Layout slots → Component composition
Pros & Cons
| Context / Zustand | Deep prop drilling |
|---|---|
| ✅ Any depth access | ❌ Fragile — rename breaks chain |
| ✅ Cleaner intermediate components | ❌ Hard to trace data flow |
| ❌ All consumers re-render on change* | ✅ Explicit and easy to follow (shallow) |
*Mitigate with split contexts or Zustand selectors.
Real-Life Example
// ✅ Composition — pass JSX directly, skip intermediates
function App() {
const [user, setUser] = useState(null)
return <Layout header={<Header user={user} />} sidebar={<Sidebar />} content={<Dashboard user={user} />} />
}
// ✅ Context — theme used deep in tree
const ThemeContext = createContext<'light' | 'dark'>('light')
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
)
}
function DeepNestedButton() {
const theme = useContext(ThemeContext)
return <button className={theme === 'dark' ? 'btn-dark' : 'btn-light'}>Click</button>
}88. Can you use RTK Query without Redux Toolkit?
Answer
No, RTK Query is tightly coupled with Redux Toolkit and requires it to work properly.
89. Context API
Theory
Context API shares data across the component tree without passing props at every level. Three steps:
createContext(defaultValue)<Provider value={...}>wraps subtreeuseContext(MyContext)consumes value
Best for low-frequency global data: theme, locale, authenticated user, feature flags.
Pros & Cons
| Pros | Cons |
|---|---|
| Eliminates prop drilling | All consumers re-render on value change |
| Built into React | Easy to overuse as global store |
| Simple for theme/auth | No DevTools like Redux |
Real Example
import { createContext, useContext, useState, useMemo } from 'react'
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const value = useMemo(
() => ({
user,
login: (userData) => setUser(userData),
logout: () => setUser(null),
isAuthenticated: !!user,
}),
[user],
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
return ctx
}
// App setup
function App() {
return (
<AuthProvider>
<Navbar />
<Routes />
</AuthProvider>
)
}
// Any deep child — no prop drilling
function ProfileMenu() {
const { user, logout } = useAuth()
return (
<div>
<span>{user?.name}</span>
<button onClick={logout}>Logout</button>
</div>
)
}Performance Tip
Split contexts by update frequency — ThemeContext separate from UserContext so theme toggle doesn't re-render auth consumers.
90. Explain the Redux Workflow
Theory
Redux is a predictable state container with a strict unidirectional data flow:
UI dispatches Action → Reducer processes → Store updates → UI re-rendersCore pieces:
- Store — single source of truth holding application state
- Action — plain object
{ type: "ADD_ITEM", payload: {...} }describing what happened - Reducer — pure function
(state, action) => newState - Dispatch — sends action to store
- Selector — reads specific slice of state
With Redux Toolkit (RTK), slices, Immer, and createAsyncThunk simplify the boilerplate.
Pros & Cons
| Redux workflow | Ad-hoc useState everywhere |
|---|---|
| ✅ Predictable, traceable updates | ❌ State scattered across components |
| ✅ Time-travel debugging | ❌ Hard to sync sibling components |
| ✅ Middleware for async/logging | ❌ Prop drilling for shared state |
| ❌ Boilerplate without RTK |
Real-Life Example
// 1. Slice — state + reducers
// store/employeeSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const fetchEmployees = createAsyncThunk('employees/fetchAll', async (deptId: string) => {
const res = await fetch(`/api/departments/${deptId}/employees`)
return res.json()
})
const employeeSlice = createSlice({
name: 'employees',
initialState: { list: [], loading: false, error: null },
reducers: {
clearEmployees: (state) => {
state.list = []
},
},
extraReducers: (builder) => {
builder
.addCase(fetchEmployees.pending, (state) => {
state.loading = true
})
.addCase(fetchEmployees.fulfilled, (state, action) => {
state.loading = false
state.list = action.payload
})
.addCase(fetchEmployees.rejected, (state, action) => {
state.loading = false
state.error = action.error.message
})
},
})
// 2. Store
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: { employees: employeeSlice.reducer },
})
// 3. Provider
;<Provider store={store}>
<App />
</Provider>
// 4. Component — dispatch + select
function EmployeeList() {
const dispatch = useAppDispatch()
const { list, loading, error } = useAppSelector((s) => s.employees)
useEffect(() => {
dispatch(fetchEmployees('D101'))
}, [dispatch])
if (loading) return <Spinner />
if (error) return <ErrorBanner message={error} />
return list.map((e) => <EmployeeCard key={e.id} employee={e} />)
}Workflow diagram
User clicks "Load Employees"
↓
dispatch(fetchEmployees("D101"))
↓
createAsyncThunk → API call
↓
pending → loading: true
↓
fulfilled → state.list = data, loading: false
↓
useSelector reads new state → component re-renders91. Have you used Redux Toolkit (RTK) or TanStack Query?
Theory
These solve different problems:
| Redux Toolkit (RTK) | TanStack Query | |
|---|---|---|
| Purpose | Client-side global state | Server state (API data) |
| Manages | UI state, auth, cart, preferences | Fetched/cached remote data |
| Caching | Manual | Automatic (stale-while-revalidate) |
| Async | createAsyncThunk, RTK Query | Built-in useQuery / useMutation |
| DevTools | Redux DevTools | React Query DevTools |
They are complementary, not competitors. Use RTK for client state, TanStack Query for server state.
Pros & Cons
| RTK | TanStack Query |
|---|---|
| ✅ Great for complex client workflows | ✅ Eliminates boilerplate for API calls |
| ✅ Predictable global state | ✅ Auto refetch, cache invalidation, dedup |
| ❌ Overkill for API-only state | ❌ Not for UI-only state (sidebar open) |
| ❌ Boilerplate even with RTK | ❌ Learning curve for cache concepts |
Real-Life Example — Using both together
// TanStack Query — server state (restaurant data from API)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function RestaurantList({ city }) {
const { data, isLoading, error } = useQuery({
queryKey: ['restaurants', city],
queryFn: () => fetch(`/api/restaurants?city=${city}`).then((r) => r.json()),
staleTime: 5 * 60 * 1000, // fresh for 5 minutes
})
if (isLoading) return <Spinner />
if (error) return <ErrorBanner />
return data.map((r) => <RestaurantCard key={r.id} restaurant={r} />)
}
// RTK — client state (shopping cart, not from API cache)
function CartDrawer() {
const items = useAppSelector((state) => state.cart.items)
const dispatch = useAppDispatch()
return (
<div>
{items.map((item) => (
<CartItem key={item.id} item={item} onRemove={() => dispatch(removeItem(item.id))} />
))}
</div>
)
}
// TanStack Query mutation — place order, then invalidate cache
function PlaceOrderButton({ cartItems }) {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (order) => fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] })
dispatch(clearCart()) // RTK action
},
})
return <button onClick={() => mutation.mutate(cartItems)}>Place Order</button>
}Interview answer (concise)
92. How do you handle loading and error states in RTK Query?
Answer
All query and mutation hooks return isLoading, isError, data, and error states, which you can use to show loaders or error messages.
const { data, isLoading, isError } = useGetUsersQuery()93. How do you use Context API?
Theory
Context API shares data across the component tree without prop drilling. Pattern: createContext
→ Provider wraps tree → consumers use useContext.
Best for: theme, locale, auth session — data that many components need but changes infrequently.
Avoid for: fast-changing or fine-grained state (causes mass re-renders).
Pros & Cons
| Pros | Cons |
|---|---|
| No prop drilling | All consumers re-render on value change |
| Built into React | Provider hell if overused |
| Simple for theme/auth | Not a full state manager |
Real Example
// Split contexts — Engineo senior pattern
const ThemeContext = createContext<'light' | 'dark'>('light')
const AuthContext = createContext<AuthState | null>(null)
function AppProviders({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const auth = useAuthState() // custom hook with login/logout
const themeValue = useMemo(() => ({ theme, setTheme }), [theme])
return (
<AuthContext.Provider value={auth}>
<ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
</AuthContext.Provider>
)
}
// Consumer — only re-renders when its context changes
function Header() {
const { theme } = useContext(ThemeContext)
const { user } = useContext(AuthContext)
return <header className={`header-${theme}`}>Hello, {user?.name}</header>
}// When NOT to use Context — use TanStack Query instead
// ❌ Context for API data
const DataContext = createContext(fetchedUsers)
// ✅ TanStack Query
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })94. How does caching work in RTK Query?
Answer
RTK Query automatically caches fetched data and reuses it on subsequent requests, reducing network calls. You can customize when it should refetch.
95. How does Redux work, from installation to usage in a project?
Theory
Redux is a predictable state container. It holds application state in a single store, updated only through actions dispatched to a reducer (pure function). The flow is unidirectional:
UI → dispatch(action) → reducer(state, action) → new state → UI re-rendersPros & Cons
| Pros | Cons |
|---|---|
| Predictable state updates | Boilerplate without RTK |
| Time-travel debugging (Redux DevTools) | Overkill for simple apps |
| Centralized state — easy to trace | Learning curve for new developers |
| Middleware for async (thunks, sagas) | Can cause unnecessary re-renders without selectors |
Real-Life Example — Full flow
Step 1: Installation
npm install @reduxjs/toolkit react-reduxStep 2: Create a slice (RTK — modern approach)
// store/slices/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
total: number
}
const initialState: CartState = { items: [], total: 0 }
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action: PayloadAction<CartItem>) => {
const existing = state.items.find((i) => i.id === action.payload.id)
if (existing) {
existing.quantity += 1
} else {
state.items.push({ ...action.payload, quantity: 1 })
}
state.total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
},
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter((i) => i.id !== action.payload)
state.total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
},
clearCart: () => initialState,
},
})
export const { addItem, removeItem, clearCart } = cartSlice.actions
export default cartSlice.reducerStep 3: Configure store
// store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './slices/cartSlice'
import authReducer from './slices/authSlice'
export const store = configureStore({
reducer: {
cart: cartReducer,
auth: authReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatchStep 4: Provide store to React
// main.tsx
import { Provider } from 'react-redux'
import { store } from './store'
ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<App />
</Provider>,
)Step 5: Use in components
// hooks.ts — typed hooks
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
// CartButton.tsx
import { useAppSelector, useAppDispatch } from '../hooks'
import { addItem } from '../store/slices/cartSlice'
function AddToCartButton({ product }) {
const dispatch = useAppDispatch()
const cartCount = useAppSelector((state) => state.cart.items.length)
return <button onClick={() => dispatch(addItem(product))}>Add to Cart ({cartCount})</button>
}Step 6: Async with createAsyncThunk
// store/slices/ordersSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
export const fetchOrders = createAsyncThunk('orders/fetchAll', async (userId: string) => {
const res = await fetch(`/api/orders?userId=${userId}`)
return res.json()
})
const ordersSlice = createSlice({
name: 'orders',
initialState: { list: [], loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchOrders.pending, (state) => {
state.loading = true
})
.addCase(fetchOrders.fulfilled, (state, action) => {
state.loading = false
state.list = action.payload
})
.addCase(fetchOrders.rejected, (state, action) => {
state.loading = false
state.error = action.error.message
})
},
})Classic Redux flow (without RTK — know for interviews)
// Action
const ADD_ITEM = 'cart/ADD_ITEM'
const addItem = (item) => ({ type: ADD_ITEM, payload: item })
// Reducer (pure function)
function cartReducer(state = [], action) {
switch (action.type) {
case ADD_ITEM:
return [...state, action.payload]
default:
return state
}
}
// Store
const store = createStore(cartReducer)
store.dispatch(addItem({ id: 1, name: 'Biryani' }))96. Lifting State Up
Theory
When two sibling components need to share state, lift it to their closest common parent. Parent holds state and passes it down as props + callback to update.
This is React's primary pattern for shared local state before reaching for Context or Redux.
Real Example
function App() {
const [selectedId, setSelectedId] = useState(null)
return (
<>
<ProductList selectedId={selectedId} onSelect={setSelectedId} />
<ProductDetail id={selectedId} />
</>
)
}97. Optimize Redux application performance
Theory
Redux performance issues come from: unnecessary re-renders, large state trees, unmemoized selectors, and storing server data in Redux.
Optimization strategies
| Strategy | How |
|---|---|
| Reselect / createSelector | Memoized derived data |
| Slice selectors | Select only needed slice |
| Normalized state | Flat entities by ID — avoid nested updates |
| RTK Query for API | Don't duplicate server state in Redux |
| React.memo on list items | Prevent child re-renders |
| Avoid storing UI state globally | Keep modal open state local |
| useAppSelector with equality | shallowEqual for object selections |
| Code split reducers | injectReducer for lazy routes |
Pros & Cons
| Optimized Redux | Unoptimized Redux |
|---|---|
| ✅ Minimal re-renders | ❌ Entire tree re-renders on any change |
| ✅ DevTools still useful | ❌ Slow lists, janky UI |
| More setup upfront | Simple but doesn't scale |
Real Example
// ❌ Bad — new object every render, all subscribers re-render
const user = useAppSelector((state) => ({
name: state.auth.user.name,
role: state.auth.user.role,
}))
// ✅ createSelector — memoized
const selectUserProfile = createSelector(
(state: RootState) => state.auth.user,
(user) => (user ? { name: user.name, role: user.role } : null),
)
const profile = useAppSelector(selectUserProfile)
// Normalized state
// ❌ { orders: [{ id, items: [...] }, ...] }
// ✅ { orders: { byId: { "1": {...} }, allIds: ["1", "2"] } }
// RTK Query — server state NOT in Redux slice
const { data: orders } = useGetOrdersQuery(userId)
// Redux only holds: auth, UI preferences, cart// injectReducer — code split Redux per route
import { injectReducer } from './store'
injectReducer('reports', reportsReducer)98. Prop Drilling
Theory
Prop drilling is passing props through many intermediate components that don't use them — just to reach a deep child.
Solutions: Context API, component composition (children), state management (Redux/Zustand).
Real Example
// ❌ Drilling user through 4 levels
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<Avatar user={user} />
// ✅ Context
<UserProvider value={user}>
<Layout><Sidebar><Avatar /></Sidebar></Layout>
</UserProvider>
// ✅ Composition — skip middle layers
<Layout sidebar={<Sidebar avatar={<Avatar user={user} />} />} />99. Redux
Theory
Redux is a predictable state container for JavaScript apps. State lives in a single store, updated only through actions processed by reducers (pure functions).
Redux Toolkit (RTK) is the modern standard — slices, Immer, createAsyncThunk.
Use Redux for complex client-side global state. Use TanStack Query for server state.
Pros & Cons
| Pros | Cons |
|---|---|
| Single source of truth | Boilerplate (reduced with RTK) |
| Predictable updates | Overkill for simple apps |
| Redux DevTools time-travel | Learning curve |
| Middleware (logging, async) | Can cause unnecessary re-renders |
Real-Life Example
// store/cartSlice.ts
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] as CartItem[] },
reducers: {
addItem: (state, action: PayloadAction<CartItem>) => {
const existing = state.items.find((i) => i.id === action.payload.id)
if (existing) existing.qty += 1
else state.items.push({ ...action.payload, qty: 1 })
},
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter((i) => i.id !== action.payload)
},
},
})
// Component
function CartBadge() {
const count = useAppSelector((s) => s.cart.items.length)
const dispatch = useAppDispatch()
return <button onClick={() => dispatch(addItem(product))}>Cart ({count})</button>
}Flow: dispatch(action) → reducer(state, action) → new state → useSelector → re-render
100. Redux Toolkit (RTK)
Theory
RTK is the official Redux approach — reduces boilerplate with:
createSlice— actions + reducer togetherconfigureStore— store with DevToolscreateAsyncThunk— async logic- Immer built-in — "mutate" draft safely
Real Example
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], status: 'idle' },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload)
},
clearCart: (state) => {
state.items = []
},
},
extraReducers: (builder) => {
builder.addCase(fetchCart.fulfilled, (state, action) => {
state.items = action.payload
})
},
})
const store = configureStore({ reducer: { cart: cartSlice.reducer } })101. Redux vs Context API
Theory
| Redux / RTK | Context | |
|---|---|---|
| Use case | Complex global state, middleware | Theme, auth, locale |
| DevTools | Yes | No |
| Performance | Selective subscriptions (useSelector) | All consumers re-render |
| Async | createAsyncThunk, middleware | Manual in useEffect |
| Boilerplate | Higher (RTK reduces it) | Lower |
Real Example
// Context — theme (changes rarely)
<ThemeProvider value={theme}>
// Redux — cart (many actions, DevTools, persistence)
const items = useSelector((s) => s.cart.items);
dispatch(addItem(product));102. Redux Workflow
Theory
Redux flow: UI → dispatch(action) → reducer(state, action) → new state → UI re-renders
Components never mutate state directly. Reducers are pure functions.
Real Example
// 1. Slice
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload)
},
removeItem: (state, action) => {
state.items = state.items.filter((i) => i.id !== action.payload)
},
},
})
// 2. Store
const store = configureStore({ reducer: { cart: cartSlice.reducer } })
// 3. Provider
;<Provider store={store}>
<App />
</Provider>
// 4. Component
function Cart() {
const items = useSelector((s) => s.cart.items)
const dispatch = useDispatch()
return <button onClick={() => dispatch(addItem(product))}>Cart ({items.length})</button>
}Workflow:
Click "Add" → dispatch(addItem(product))
→ reducer runs → state.cart.items updated
→ useSelector detects change → Cart re-renders103. State Management at Scale
Theory
At scale, state splits into categories — pick the right tool per category:
| State type | Examples | Tool |
|---|---|---|
| Server state | API data, cache | TanStack Query |
| URL state | Filters, pagination | React Router searchParams |
| Global client state | Auth, cart, theme | Redux / Zustand / Jotai |
| Local UI state | Modal open, input | useState |
| Form state | Validation, dirty | React Hook Form |
Library Comparison
| Redux (RTK) | Zustand | Jotai | |
|---|---|---|---|
| Boilerplate | Medium (RTK helps) | Minimal | Minimal |
| DevTools | Excellent | Good plugin | Limited |
| Middleware | Thunks, listeners | Custom | Atom effects |
| Selectors | useSelector | subscribe with selector | useAtom |
| Best for | Complex apps, teams | Simple global state | Fine-grained atomic state |
| Learning curve | Higher | Low | Low |
Redux at Scale (RTK)
// features/cart/cartSlice.ts
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], status: 'idle' },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload)
},
},
extraReducers: (builder) => {
builder.addCase(syncCart.fulfilled, (state, action) => {
state.items = action.payload
})
},
})
// Selectors — prevent unnecessary re-renders
const selectCartItems = (state: RootState) => state.cart.items
const selectCartTotal = createSelector([selectCartItems], (items) => items.reduce((sum, i) => sum + i.price * i.qty, 0))Zustand — Lightweight Global State
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
}
const useCartStore = create<CartStore>()(
devtools(
persist(
(set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
}),
{ name: 'cart-storage' },
),
),
)
// Component — subscribe to slice only
const items = useCartStore((s) => s.items)
const addItem = useCartStore((s) => s.addItem)Jotai — Atomic Fine-Grained State
import { atom, useAtom, useAtomValue } from 'jotai'
const cartItemsAtom = atom<CartItem[]>([])
const cartTotalAtom = atom((get) => get(cartItemsAtom).reduce((sum, i) => sum + i.price, 0))
function CartBadge() {
const total = useAtomValue(cartTotalAtom) // only re-renders when total changes
return <span>₹{total}</span>
}Anti-Patterns at Scale
// ❌ Everything in Context — all consumers re-render
<AppContext.Provider value={{ user, cart, theme, notifications, ... }}>
// ❌ Duplicating server data in Redux
dispatch(setUsers(apiResponse)); // TanStack Query already caches this
// ✅ Colocate state as low as possible
function Modal() {
const [open, setOpen] = useState(false); // only Modal cares
}104. What are the advantages of Redux?
Theory
Redux centralizes application state and enforces predictable updates through pure reducers. It's most valuable when multiple components share complex state or when you need strong debugging and traceability.
Advantages
| Advantage | Explanation |
|---|---|
| Single source of truth | All state in one store — easy to inspect |
| Predictable updates | Pure reducers — same input always same output |
| Time-travel debugging | Redux DevTools replay every action |
| Middleware ecosystem | Logging, async (thunk/saga), analytics |
| Testability | Reducers are pure functions — easy unit tests |
| Scalability | Large teams — clear state ownership per slice |
| Decoupled components | Any component can read/dispatch without prop drilling |
Pros & Cons
| When Redux helps | When Redux is overkill |
|---|---|
| Complex shared client state (auth, cart, multi-step forms) | Simple local UI state |
| Multiple features need same data | Server state (use TanStack Query) |
| Need action history / debugging | Small apps with few components |
| Enterprise apps with many developers | Prototyping / MVPs |
Real-Life Example
// Without Redux — prop drilling + duplicate fetches
;<App user={user}>
<Header user={user} />
<Sidebar user={user} />
<Dashboard user={user} />
</App>
// With Redux — any component accesses state directly
function Header() {
const user = useAppSelector((s) => s.auth.user)
return <span>{user.name}</span>
}
function Sidebar() {
const user = useAppSelector((s) => s.auth.user)
return <nav>{user.role === 'admin' && <AdminLink />}</nav>
}
// Redux DevTools — replay bug report
// "User clicked Add → ADD_ITEM → state.cart.items.length went 2→3"
// Reproduce exact sequence of actionsModern note for interview: Mention Redux Toolkit + TanStack Query together — Redux for client state, Query for server state.
105. What are the benefits of using Redux Toolkit over traditional Redux?
Answer
- Less boilerplate
- Simplified setup
- Built-in Redux DevTools support
- Encourages best practices
- Includes RTK Query for efficient data fetching
106. What does createApi do in RTK Query?
Answer
createApi sets up an API slice where you define endpoints using builder.query and builder.mutation. It auto-generates hooks for each endpoint.
107. What is tagTypes in RTK Query?
Answer
tagTypes define the types of data used for cache invalidation. Tags help manage refetching when data changes.
108. What is a slice in Redux Toolkit?
Answer
A slice is a portion of the Redux state and the logic associated with it. You create a slice using createSlice(), which automatically generates action creators and reducers.
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
})109. What is RTK Query?
Answer
RTK Query is a powerful data fetching and caching tool built into Redux Toolkit. It manages API calls, caching, loading states, and errors efficiently.
110. What is the difference between query and mutation in RTK Query?
Answer
query: For fetching data (GET)mutation: For modifying data (POST, PUT, DELETE)
111. When should you avoid Context API?
Avoid Context when:
| Situation | Why | Alternative |
|---|---|---|
| Frequently changing values | All consumers re-render | Zustand, Jotai, external store |
| Large app-wide state | Provider hell, hard to trace | Redux Toolkit, Zustand |
| Performance-critical lists | Context in parent re-renders all children | Colocate state, composition |
| Server data | No caching, dedup, stale handling | TanStack Query / SWR |
| Form state | Too granular for context | React Hook Form |
| URL-shareable state | Not in URL | Router search params |
The re-render problem
const AppContext = createContext()
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
// ❌ New object every render → ALL consumers re-render
const value = { user, setUser, theme, setTheme }
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}Fix — split contexts or memoize
const UserContext = createContext()
const ThemeContext = createContext()
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const userValue = useMemo(() => ({ user, setUser }), [user])
const themeValue = useMemo(() => ({ theme, setTheme }), [theme])
return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
</UserContext.Provider>
)
}When Context IS appropriate
- Theme (changes rarely)
- Locale / i18n
- Auth session (read-heavy, changes infrequently)
- Design system tokens
Interview answer
Routing & Navigation
112. Dynamic Routes — useParams
Theory
Dynamic segments use :param in the path. useParams() reads them from the URL.
Nested routes use <Outlet /> in the parent layout.
Real Example
// Route: /products/:productId
function ProductDetail() {
const { productId } = useParams()
const { data, isLoading } = useProduct(productId)
if (isLoading) return <Spinner />
return <h1>{data.name}</h1>
}
;<Route path="/products/:productId" element={<ProductDetail />} />113. How do you connect routes in React?
Theory
React Router is the standard library for client-side routing in React SPAs. It maps URLs to components without full page reloads, enabling navigation, nested routes, protected routes, and URL parameters.
Core components (v6+):
BrowserRouter— wraps app, uses HTML5 history APIRoutes/Route— define URL-to-component mappingLink/NavLink— navigation without page reloaduseNavigate,useParams,useSearchParams— programmatic navigation and URL data
Pros & Cons
| Client-side routing (SPA) | Server-side routing |
|---|---|
| ✅ Instant navigation, no full reload | ✅ Better SEO out of the box |
| ✅ App-like experience | ✅ Works without JavaScript |
| ❌ Needs SSR/SSG for SEO | ❌ Full page reload on navigation |
| ❌ Initial bundle includes all routes (without lazy loading) | ❌ Slower perceived navigation |
Real-Life Example
Installation
npm install react-router-domBasic routing setup
// App.tsx
import { BrowserRouter, Routes, Route, Link, Navigate } from 'react-router-dom'
import { lazy, Suspense } from 'react'
const Home = lazy(() => import('./pages/Home'))
const Restaurants = lazy(() => import('./pages/Restaurants'))
const RestaurantDetail = lazy(() => import('./pages/RestaurantDetail'))
const Cart = lazy(() => import('./pages/Cart'))
const Login = lazy(() => import('./pages/Login'))
const NotFound = lazy(() => import('./pages/NotFound'))
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/restaurants">Restaurants</Link>
<Link to="/cart">Cart</Link>
</nav>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/restaurants" element={<Restaurants />} />
<Route path="/restaurants/:id" element={<RestaurantDetail />} />
<Route
path="/cart"
element={
<ProtectedRoute>
<Cart />
</ProtectedRoute>
}
/>
<Route path="/login" element={<Login />} />
<Route path="/404" element={<NotFound />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}Protected route
import { Navigate, useLocation } from 'react-router-dom'
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth()
const location = useLocation()
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return children
}Dynamic params and navigation
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
function RestaurantDetail() {
const { id } = useParams() // /restaurants/:id
const [searchParams] = useSearchParams() // ?sort=rating&filter=veg
const navigate = useNavigate()
const sort = searchParams.get('sort') ?? 'rating'
const goToCart = () => navigate('/cart', { state: { from: id } })
return (
<div>
<h1>Restaurant #{id}</h1>
<p>Sorted by: {sort}</p>
<button onClick={goToCart}>Go to Cart</button>
</div>
)
}Nested routes with layout
// Dashboard layout with sidebar
;<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="orders" element={<Orders />} />
<Route path="settings" element={<Settings />} />
</Route>
// DashboardLayout.tsx
import { Outlet } from 'react-router-dom'
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<main>
<Outlet />
</main>{' '}
{/* child routes render here */}
</div>
)
}114. Navigation — useNavigate & useLocation
Theory
useNavigate()— programmatic navigation (navigate("/dashboard"),navigate(-1))useLocation()— current location object (pathname, search, state)useSearchParams()— read/write URL query strings
Real Example
function LoginForm() {
const navigate = useNavigate()
const location = useLocation()
const from = location.state?.from?.pathname || '/dashboard'
const handleLogin = async () => {
await login(credentials)
navigate(from, { replace: true })
}
}
function ProductFilters() {
const [searchParams, setSearchParams] = useSearchParams()
const category = searchParams.get('category') ?? 'all'
const setCategory = (cat) => setSearchParams({ category: cat })
}115. Protected Routes
Theory
Protected routes redirect unauthenticated users to login. Wrap route element in a guard component that checks auth state.
Real Example
function ProtectedRoute({ children }) {
const { user, isLoading } = useAuth()
const location = useLocation()
if (isLoading) return <Spinner />
if (!user) return <Navigate to="/login" state={{ from: location }} replace />
return children
}
;<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>116. React Router
Theory
React Router maps URLs to components in a Single Page Application — no full page reload. Core
pieces (v6): BrowserRouter, Routes, Route, Link, useNavigate, useParams.
Pros & Cons
| SPA routing | Full page reload |
|---|---|
| ✅ Instant navigation | ❌ Slower perceived navigation |
| ✅ App-like UX | ❌ Needs SSR for SEO |
Real Example
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/products">Products</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
</Routes>
</BrowserRouter>
)
}117. React Router Basics
Theory
React Router enables client-side routing in SPAs. Core pieces:
BrowserRouter— HTML5 history APIRoutes/Route— path-to-component mappingLink/NavLink— navigation without full reloadOutlet— nested route rendering
Real Example
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
)
}118. Your API routes are receiving a high volume of requests. How would you improve scalability and performance?
Description
Route Handlers (app/api/...) become bottlenecks under spike traffic without caching, rate limiting, horizontal scale, and offloading work to queues.
Theory
| Layer | Tactic |
|---|---|
| Edge | Move read-heavy, low-compute routes to Edge Runtime |
| Cache | CDN cache GET responses; Redis for computed results |
| Rate limiting | Token bucket per IP/user (Upstash, middleware) |
| Queue | POST returns 202; worker processes async |
| DB | Connection pooling (PgBouncer), read replicas |
| Stateless | Scale serverless instances horizontally |
Pros & Cons
| Rate limit + cache + queue | Raw handlers hitting DB every time |
|---|---|
| ✅ Survives viral traffic | ❌ DB connection exhaustion |
| ✅ Predictable cost | ❌ Cascading 503 errors |
| ❌ More infrastructure | ❌ Hard to scale past single region |
Real Example — High-traffic search API
// middleware.ts — rate limit at edge
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 req/min per IP
})
export async function middleware(req: NextRequest) {
if (!req.nextUrl.pathname.startsWith('/api/')) return NextResponse.next()
const ip = req.ip ?? '127.0.0.1'
const { success, reset } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429, headers: { 'Retry-After': String(reset) } })
}
return NextResponse.next()
}
export const config = { matcher: '/api/:path*' }// app/api/search/route.ts
export const runtime = 'edge'
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const q = searchParams.get('q') ?? ''
const res = await fetch(`${process.env.SEARCH_SERVICE}/q=${encodeURIComponent(q)}`, {
next: { revalidate: 30 },
})
const data = await res.json()
return Response.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=30, stale-while-revalidate=60',
},
})
}// Heavy writes — queue instead of blocking
// app/api/orders/route.ts
export async function POST(req: Request) {
const body = await req.json()
await queue.publish('order.created', body) // SQS / Kafka / BullMQ
return Response.json({ status: 'accepted' }, { status: 202 })
}API & Data Fetching
119. A Next.js page is loading slowly because it fetches data from multiple APIs. How would you optimize the page performance?
Description
The page blocks on sequential or waterfall API calls — each request waits for the previous one. Time to first byte (TTFB) and Largest Contentful Paint (LCP) suffer when the server or client chains 4–5 fetches.
Theory
In the App Router, Server Components fetch on the server by default. The main levers are:
| Technique | What it does |
|---|---|
| Parallel fetching | Promise.all — independent APIs run at the same time |
| Streaming + Suspense | Ship shell HTML first; slow sections stream in |
| Colocate data | Fetch in the component that needs it (automatic dedup via fetch cache) |
| Move to edge | export const runtime = 'edge' for low-latency reads |
| Reduce payload | Return only fields the page needs |
| Cache stable data | fetch(url, { next: { revalidate: 60 } }) |
User → Page → API A (400ms) → API B (300ms) → API C (350ms) = 1050msUser → Page → API A ─┐
API B ─┼→ merge → 400ms (longest call)
API C ─┘Pros & Cons
| Parallel + streaming | One giant blocking fetch |
|---|---|
| ✅ Faster perceived load | ❌ More complex loading UI |
| ✅ Better Core Web Vitals | ❌ Harder to debug race conditions |
| ✅ Partial content visible early | ❌ Needs Suspense boundaries |
Real Example — Dashboard with user, orders, and notifications
// app/dashboard/page.tsx — Server Component
import { Suspense } from 'react'
import { UserHeader } from './user-header'
import { OrdersPanel } from './orders-panel'
import { NotificationsPanel } from './notifications-panel'
import { PanelSkeleton } from '@/components/panel-skeleton'
// Independent parallel fetches inside each child — no waterfall
export default function DashboardPage() {
return (
<main>
<Suspense fallback={<PanelSkeleton />}>
<UserHeader />
</Suspense>
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<PanelSkeleton />}>
<OrdersPanel />
</Suspense>
<Suspense fallback={<PanelSkeleton />}>
<NotificationsPanel />
</Suspense>
</div>
</main>
)
}// app/dashboard/orders-panel.tsx
async function getOrders(userId: string) {
const res = await fetch(`${process.env.API_URL}/orders?userId=${userId}`, {
next: { revalidate: 30, tags: ['orders'] },
})
return res.json()
}
export async function OrdersPanel() {
const session = await getSession() // deduped if same fetch elsewhere
const orders = await getOrders(session.userId)
return <OrderList orders={orders} />
}// When you MUST combine in one file — parallel, not sequential
export async function AnalyticsPage() {
const [revenue, users, churn] = await Promise.all([fetchRevenue(), fetchUsers(), fetchChurn()])
return <Analytics revenue={revenue} users={users} churn={churn} />
}loading.tsxfor route-level skeleton- React Query / SWR on client islands only for interactive refetch
- Measure with Lighthouse + Next.js Speed Insights
120. Async Data Handling — React Query & Caching
Theory
TanStack Query manages server state — fetching, caching, background sync, deduplication, retries, stale-while-revalidate.
Key concepts:
- queryKey — unique cache identifier
- staleTime — how long data is fresh (no refetch)
- gcTime (cacheTime) — how long unused data stays in cache
- invalidation — mark cache stale after mutations
Pros & Cons
| Pros | Cons |
|---|---|
| Eliminates 80% of useEffect fetch boilerplate | Learning curve for cache keys |
| Background refetch, dedup | Over-fetching if keys wrong |
| Optimistic updates built-in | SSR needs hydration setup |
Core Patterns
// Hierarchical query keys — invalidate at any level
const productKeys = {
all: ['products'] as const,
lists: () => [...productKeys.all, 'list'] as const,
list: (filters: Filters) => [...productKeys.lists(), filters] as const,
details: () => [...productKeys.all, 'detail'] as const,
detail: (id: string) => [...productKeys.details(), id] as const,
}
function useProducts(filters: Filters) {
return useQuery({
queryKey: productKeys.list(filters),
queryFn: () => api.getProducts(filters),
staleTime: 5 * 60 * 1000, // 5 min — product list doesn't change often
gcTime: 30 * 60 * 1000,
})
}
function useProduct(id: string) {
return useQuery({
queryKey: productKeys.detail(id),
queryFn: () => api.getProduct(id),
staleTime: 60 * 1000,
enabled: !!id,
})
}Mutation + Invalidation
function useAddToCart() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (item: CartItem) => api.addToCart(item),
onMutate: async (newItem) => {
await queryClient.cancelQueries({ queryKey: ['cart'] })
const previous = queryClient.getQueryData<CartItem[]>(['cart'])
queryClient.setQueryData(['cart'], (old = []) => [...old, newItem])
return { previous }
},
onError: (_err, _item, context) => {
queryClient.setQueryData(['cart'], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['cart'] })
},
})
}Parallel + Dependent Queries
// Parallel
const user = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const orders = useQuery({
queryKey: ['orders', id],
queryFn: () => fetchOrders(id),
})
// Dependent — orders waits for user
const user = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const orders = useQuery({
queryKey: ['orders', user.data?.id],
queryFn: () => fetchOrders(user.data!.id),
enabled: !!user.data?.id,
})
// Parallel with useQueries
const results = useQueries({
queries: ids.map((id) => ({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
})),
})Caching Strategy Table
| Data type | staleTime | Refetch on focus? |
|---|---|---|
| User profile | 5–10 min | Yes |
| Product catalog | 5 min | Yes |
| Stock/inventory | 30 sec | Yes |
| Static CMS content | 1 hour | No |
| Real-time order status | 0 (always stale) | WebSocket + invalidate |
121. Axios vs fetch in React
Theory
| fetch | Axios | |
|---|---|---|
| Built-in | Yes | No — npm package |
| JSON | Manual .json() | Auto-transform |
| Interceptors | Manual | Built-in |
| Timeout | Manual AbortController | Built-in config |
| Error on 4xx | No — check response.ok | Throws by default |
Real Example
const api = axios.create({ baseURL: '/api/v1', timeout: 10000 })
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) logout()
return Promise.reject(err)
},
)122. Build a custom React hook for data fetching
Theory
A custom hook extracts reusable stateful logic. For data fetching, handle:
- Loading, error, and data states
- Race conditions (stale responses)
- Abort/cleanup on unmount or dependency change
- Optional: caching, retry, refetch
Practical Example — Production-grade useFetch
import { useEffect, useReducer, useRef } from 'react'
type FetchState<T> = {
data: T | null
error: Error | null
isLoading: boolean
}
type Action<T> = { type: 'loading' } | { type: 'success'; payload: T } | { type: 'error'; payload: Error }
function fetchReducer<T>(state: FetchState<T>, action: Action<T>): FetchState<T> {
switch (action.type) {
case 'loading':
return { ...state, isLoading: true, error: null }
case 'success':
return { data: action.payload, error: null, isLoading: false }
case 'error':
return { ...state, error: action.payload, isLoading: false }
default:
return state
}
}
export function useFetch<T>(url: string | null, options?: RequestInit) {
const [state, dispatch] = useReducer(fetchReducer<T>, {
data: null,
error: null,
isLoading: false,
})
const optionsRef = useRef(options)
optionsRef.current = options
useEffect(() => {
if (!url) return
const controller = new AbortController()
let cancelled = false
async function load() {
dispatch({ type: 'loading' })
try {
const res = await fetch(url!, {
...optionsRef.current,
signal: controller.signal,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = (await res.json()) as T
if (!cancelled) dispatch({ type: 'success', payload: json })
} catch (err) {
if (!cancelled && (err as Error).name !== 'AbortError') {
dispatch({ type: 'error', payload: err as Error })
}
}
}
load()
return () => {
cancelled = true
controller.abort()
}
}, [url])
return state
}Usage
function RestaurantList() {
const { data, error, isLoading } = useFetch<Restaurant[]>('https://api.example.com/restaurants')
if (isLoading) return <Spinner />
if (error) return <ErrorBanner message={error.message} />
return (
<ul>
{data?.map((r) => (
<li key={r.id}>{r.name}</li>
))}
</ul>
)
}What interviewers look for
- AbortController for cleanup
- Guard against setting state after unmount
- Typed generics
- Separation of concerns (hook vs UI)
- Mention React Query / SWR for production apps
123. CORS
Theory
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks JavaScript from reading responses from a different origin unless the server explicitly allows it.
Origin = protocol + domain + port (e.g. https://a quick-commerce app.com:443)
The browser sends a preflight OPTIONS request for non-simple requests (custom headers, PUT/DELETE, JSON content-type). Server responds with:
Access-Control-Allow-Origin: https://a quick-commerce app.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: truePros & Cons
| Pros | Cons |
|---|---|
| Protects users from malicious sites | Confusing for beginners |
| Standard, well-supported | Misconfigured servers break apps |
| Granular control per origin | Can't fix purely on frontend |
Real Example
// Frontend — a quick-commerce app.com calling api.a quick-commerce app.com
fetch('https://api.a quick-commerce app.com/v1/products', {
method: 'GET',
credentials: 'include', // sends cookies — server must allow credentials
headers: { 'Content-Type': 'application/json' },
})
.then((r) => r.json())
.then(setProducts)
// ❌ CORS error in console:
// "Access to fetch at 'https://api.a quick-commerce app.com' from origin 'https://a quick-commerce app.com'
// has been blocked by CORS policy"// Backend — Express CORS setup
import cors from 'cors'
app.use(
cors({
origin: ['https://a quick-commerce app.com', 'http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}),
)Common Interview Follow-ups
| Question | Answer |
|---|---|
| Can frontend bypass CORS? | No — it's enforced by the browser |
| Simple vs preflight request? | Simple: GET/POST with basic headers — no OPTIONS. Preflight: custom headers, PUT/DELETE |
| Why credentials need special header? | Access-Control-Allow-Credentials: true + specific origin (not *) |
124. Custom useFetch Hook
Theory
A custom useFetch hook encapsulates API call logic — loading, data, error — reusable across any endpoint. Senior expectations:
- AbortController cleanup on unmount / URL change
- Re-fetch when URL or deps change
- Handle HTTP errors (non-2xx)
- Optional: refetch function, enabled flag
Pros & Cons
| Pros | Cons |
|---|---|
| DRY — one fetch pattern everywhere | For production, TanStack Query is better |
| Consistent loading/error handling | Manual cache invalidation |
| Easy to test in isolation | Can grow complex with auth/retry |
Full Implementation
import { useState, useEffect, useCallback, useRef } from 'react'
function useFetch(url, options = {}) {
const { enabled = true, deps = [] } = options
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const abortRef = useRef(null)
const fetchData = useCallback(async () => {
if (!url || !enabled) return
abortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
setLoading(true)
setError(null)
try {
const response = await fetch(url, {
signal: controller.signal,
headers: { 'Content-Type': 'application/json', ...options.headers },
...options.fetchOptions,
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const json = await response.json()
setData(json)
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message ?? 'Something went wrong')
setData(null)
}
} finally {
if (!controller.signal.aborted) {
setLoading(false)
}
}
}, [url, enabled, ...deps])
useEffect(() => {
fetchData()
return () => abortRef.current?.abort()
}, [fetchData])
const refetch = useCallback(() => fetchData(), [fetchData])
return { data, loading, error, refetch }
}
export default useFetchUsage Examples
function UserList() {
const { data, loading, error, refetch } = useFetch('https://jsonplaceholder.typicode.com/users')
if (loading) return <p>Loading...</p>
if (error)
return (
<p>
Error: {error} <button onClick={refetch}>Retry</button>
</p>
)
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
function UserDetail({ userId }) {
const { data, loading, error } = useFetch(userId ? `https://jsonplaceholder.typicode.com/users/${userId}` : null, {
enabled: !!userId,
deps: [userId],
})
// ...
}Generic Version (any HTTP method + body)
function useFetch(url, options = {}) {
const { method = 'GET', body, enabled = true } = options
const [state, setState] = useState({ data: null, loading: false, error: null })
useEffect(() => {
if (!enabled || !url) return
const controller = new AbortController()
setState((s) => ({ ...s, loading: true, error: null }))
fetch(url, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
})
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(r.statusText))))
.then((data) => setState({ data, loading: false, error: null }))
.catch((err) => {
if (err.name !== 'AbortError') {
setState({ data: null, loading: false, error: err.message })
}
})
return () => controller.abort()
}, [url, method, enabled, JSON.stringify(body)])
return state
}Interview Tip
Mention that in production you'd use TanStack Query for caching, deduplication, and stale-while-revalidate — but useFetch demonstrates hook composition and effect cleanup.
125. How do you handle race conditions in API calls?
The problem
User navigates quickly → Request A starts → Request B starts → B finishes first → A finishes last → stale data overwrites fresh data.
Solutions
| Approach | How |
|---|---|
| AbortController | Cancel previous request |
| Ignore flag | Track latest request ID |
| React Query / SWR | Built-in stale request handling |
| Axios cancel token | Legacy cancel pattern |
Practical Example — AbortController
function useSearch(query) {
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!query) return
const controller = new AbortController()
let cancelled = false
async function search() {
setLoading(true)
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
})
const data = await res.json()
if (!cancelled) setResults(data)
} catch (err) {
if (err.name !== 'AbortError' && !cancelled) {
console.error(err)
}
} finally {
if (!cancelled) setLoading(false)
}
}
search()
return () => {
cancelled = true
controller.abort()
}
}, [query])
return { results, loading }
}Practical Example — Request ID pattern
function useLatestFetch(url) {
const [data, setData] = useState(null)
const requestIdRef = useRef(0)
useEffect(() => {
const id = ++requestIdRef.current
fetch(url)
.then((r) => r.json())
.then((json) => {
if (id === requestIdRef.current) setData(json)
})
}, [url])
return data
}Interview answer
126. How does SSO work?
Theory
Single Sign-On (SSO) lets a user authenticate once and access multiple applications without logging in again. It is typically built on top of OAuth 2.0 + OpenID Connect (OIDC) or SAML.
The Identity Provider (IdP) handles authentication. Service Providers (SPs) trust the IdP's tokens.
Pros & Cons
| Pros | Cons |
|---|---|
| One login for all company apps | Single point of failure (IdP down = all apps down) |
| Centralized user management | Complex setup for small teams |
| Better security (MFA at IdP level) | Session management across domains is tricky |
Real-Life Example
Employee logs into company SSO (Okta) once:
1. Employee visits internal-dashboard.company.com
2. No session → redirect to okta.company.com/login
3. Employee enters credentials + MFA on Okta
4. Okta issues SAML assertion / OIDC id_token
5. Dashboard app validates token → creates local session
6. Employee visits hr-portal.company.com
→ Already has Okta session cookie → auto-authenticated
→ No second login required
Frontend's role:
- Redirect to IdP login page
- Handle callback with authorization code
- Store session token (httpOnly cookie, not localStorage)
- Check token expiry and refresh silently127. Long Polling vs WebSockets
Theory
| Long Polling | WebSockets | |
|---|---|---|
| Connection | New HTTP request each cycle | Single persistent connection |
| Direction | Client pulls (server holds request) | Bidirectional push/pull |
| Latency | Higher — wait + new request | Low — instant push |
| Overhead | HTTP headers every request | Minimal after handshake |
| Scaling | Easier — stateless HTTP | Harder — sticky sessions |
| Use case | Fallback, low-frequency updates | High-frequency real-time |
Long polling: Client sends request → server holds it open until data arrives or timeout → client immediately sends new request.
Short polling: Client requests every N seconds regardless — wasteful.
Real Example
// Long polling — order status
async function pollOrderStatus(orderId) {
while (true) {
try {
const res = await fetch(`/api/orders/${orderId}/status?longPoll=true`, {
signal: AbortSignal.timeout(30000), // 30s server hold
})
const data = await res.json()
updateUI(data)
if (data.status === 'delivered') break
} catch (err) {
await new Promise((r) => setTimeout(r, 2000)) // backoff on error
}
}
}
// WebSocket — same use case, better UX
const ws = new WebSocket(`wss://api.a quick-commerce app.com/orders/${orderId}`)
ws.onmessage = (e) => {
const data = JSON.parse(e.data)
updateUI(data)
if (data.status === 'delivered') ws.close()
}When a quick-commerce app Might Use Each
| Feature | Likely approach |
|---|---|
| Live delivery map / ETA | WebSocket |
| Cart inventory sync | WebSocket or SSE |
| Notification badge (low freq) | Long polling or SSE |
| Legacy API / CDN compatibility | Long polling fallback |
128. Optimize API calls and handle loading/error states properly
Theory
Every API call in a UI should handle three states: loading, success, and error. Optimize by: deduplicating requests, caching responses, aborting stale requests, and using libraries like TanStack Query or SWR.
Never leave users staring at a blank screen or show stale data from a previous query.
Pros & Cons
| TanStack Query / SWR | Manual fetch in useEffect |
|---|---|
| ✅ Auto caching, dedup, refetch | ❌ Boilerplate for every endpoint |
| ✅ Stale-while-revalidate | ❌ Race conditions without AbortController |
| ✅ Loading/error built-in | ❌ No background refetch |
| ✅ DevTools | ❌ Cache invalidation is manual |
Real-Life Example
// ✅ With TanStack Query — production pattern
function RestaurantList({ city }) {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['restaurants', city],
queryFn: () => api.get(`/restaurants?city=${city}`).then((r) => r.data),
staleTime: 5 * 60 * 1000,
retry: 2,
})
if (isLoading) return <RestaurantListSkeleton count={6} />
if (isError) return <ErrorBanner message={error.message} onRetry={refetch} />
if (!data?.length) return <EmptyState message="No restaurants found" />
return data.map((r) => <RestaurantCard key={r.id} restaurant={r} />)
}
// Manual pattern — when you can't use a library
function useFetch<T>(url: string) {
const [state, setState] = useState<{
data: T | null
loading: boolean
error: string | null
}>({ data: null, loading: true, error: null })
useEffect(() => {
const controller = new AbortController()
setState({ data: null, loading: true, error: null })
fetch(url, { signal: controller.signal })
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
})
.then((data) => setState({ data, loading: false, error: null }))
.catch((err) => {
if (err.name !== 'AbortError') {
setState({ data: null, loading: false, error: err.message })
}
})
return () => controller.abort()
}, [url])
return state
}129. REST vs GraphQL APIs
Theory
REST exposes multiple endpoints, each returning fixed data shapes. GraphQL exposes a single endpoint where the client specifies exactly what fields it needs.
| REST | GraphQL | |
|---|---|---|
| Endpoints | Multiple (/users, /orders) | Single (/graphql) |
| Data fetching | Server defines response shape | Client defines query shape |
| Over-fetching | Common | Avoided |
| Under-fetching | Needs multiple requests | Single request |
| Caching | HTTP caching (simple) | Complex (needs Apollo/Relay) |
| Tooling | Universal | GraphQL schema, Playground |
Pros & Cons
| REST | GraphQL |
|---|---|
| ✅ Simple, well-understood | ✅ Fetch exactly what you need |
| ✅ HTTP cache friendly | ✅ Single request for related data |
| ✅ File upload straightforward | ✅ Strong typing via schema |
| ❌ Over/under-fetching | ❌ Caching harder |
| ❌ Multiple round trips | ❌ N+1 query problem on server |
| ❌ Learning curve |
Real-Life Example
// REST — 3 requests for dashboard
const user = await fetch('/api/user/42').then((r) => r.json())
const orders = await fetch('/api/users/42/orders').then((r) => r.json())
const notifications = await fetch('/api/users/42/notifications').then((r) => r.json())
// GraphQL — 1 request, exact fields
const { data } = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query Dashboard($userId: ID!) {
user(id: $userId) { name email avatar }
orders(userId: $userId, limit: 5) { id total status }
notifications(userId: $userId, unread: true) { id message createdAt }
}
`,
variables: { userId: '42' },
}),
}).then((r) => r.json())When to choose:
- REST: Simple CRUD, public APIs, CDN caching critical, small teams
- GraphQL: Mobile apps (bandwidth), complex dashboards, many clients with different data needs
130. Scenario-Based: 3 API calls, fast load, partial errors
Theory
Requirements from the company interview:
- Three independent API calls populate different page sections
- Page must load fast
- If one API fails, show error only in that block — other sections keep working
Solution architecture:
- Parallel fetching —
Promise.allSettled()or three independent TanStack Query hooks - Per-section error boundaries — catch render errors
- Per-section error/loading state — independent UI per block
- Streaming / progressive rendering — show each section as data arrives (not wait for all)
- Suspense boundaries (optional) — per-section loading fallbacks
Pros & Cons
| Independent sections + allSettled | Promise.all |
|---|---|
| ✅ Partial failure tolerated | ❌ One failure breaks entire page |
| ✅ Fast perceived load — sections appear as ready | ❌ Waits for slowest API |
| ✅ Matches real enterprise dashboards | More complex state per section |
| More code per section |
Real-Life Example — Full implementation
// hooks/useSectionData.ts
import { useQuery } from '@tanstack/react-query'
function useSectionData<T>(key: string, fetcher: () => Promise<T>) {
return useQuery({
queryKey: [key],
queryFn: fetcher,
retry: 2,
staleTime: 5 * 60 * 1000,
// Each query is independent — one failure doesn't affect others
})
}// components/SectionBlock.tsx — reusable section wrapper
function SectionBlock({ title, isLoading, isError, error, onRetry, children }) {
return (
<section className="dashboard-section" aria-labelledby={`${title}-heading`}>
<h2 id={`${title}-heading`}>{title}</h2>
{isLoading && <SectionSkeleton />}
{isError && (
<div role="alert" className="section-error">
<p>
Failed to load {title}. {error?.message}
</p>
<button onClick={onRetry}>Retry</button>
</div>
)}
{!isLoading && !isError && children}
</section>
)
}// pages/DashboardPage.tsx — the company scenario solution
import ErrorBoundary from '../ErrorBoundary'
function AnnouncementsSection() {
const { data, isLoading, isError, error, refetch } = useSectionData('announcements', () =>
fetch('/api/announcements').then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}),
)
return (
<SectionBlock title="Announcements" isLoading={isLoading} isError={isError} error={error} onRetry={refetch}>
<AnnouncementList items={data} />
</SectionBlock>
)
}
function TeamSection() {
const { data, isLoading, isError, error, refetch } = useSectionData('team', () =>
fetch('/api/team').then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}),
)
return (
<SectionBlock title="Team" isLoading={isLoading} isError={isError} error={error} onRetry={refetch}>
<TeamGrid members={data} />
</SectionBlock>
)
}
function ProjectsSection() {
const { data, isLoading, isError, error, refetch } = useSectionData('projects', () =>
fetch('/api/projects').then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}),
)
return (
<SectionBlock title="Projects" isLoading={isLoading} isError={isError} error={error} onRetry={refetch}>
<ProjectList projects={data} />
</SectionBlock>
)
}
function DashboardPage() {
return (
<main className="dashboard-grid">
{/* Each section: independent fetch, loading, error, and error boundary */}
<ErrorBoundary fallback={<SectionError name="Announcements" />}>
<AnnouncementsSection />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError name="Team" />}>
<TeamSection />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError name="Projects" />}>
<ProjectsSection />
</ErrorBoundary>
</main>
)
}Alternative — Promise.allSettled without React Query
function DashboardPageManual() {
const [sections, setSections] = useState({
announcements: { data: null, loading: true, error: null },
team: { data: null, loading: true, error: null },
projects: { data: null, loading: true, error: null },
})
useEffect(() => {
const controllers = [new AbortController(), new AbortController(), new AbortController()]
const configs = [
{ key: 'announcements', url: '/api/announcements' },
{ key: 'team', url: '/api/team' },
{ key: 'projects', url: '/api/projects' },
]
// Fire all 3 in parallel — don't wait for each other
configs.forEach(({ key, url }, i) => {
fetch(url, { signal: controllers[i].signal })
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
})
.then((data) => {
setSections((prev) => ({
...prev,
[key]: { data, loading: false, error: null },
}))
})
.catch((err) => {
if (err.name === 'AbortError') return
setSections((prev) => ({
...prev,
[key]: { data: null, loading: false, error: err.message },
}))
})
})
return () => controllers.forEach((c) => c.abort())
}, [])
return (
<main className="dashboard-grid">
<SectionBlock title="Announcements" {...sections.announcements} />
<SectionBlock title="Team" {...sections.team} />
<SectionBlock title="Projects" {...sections.projects} />
</main>
)
}Architecture diagram
Dashboard Page
├── Announcements Section
│ ├── fetch /api/announcements (parallel)
│ ├── own loading skeleton
│ ├── own error block (if fails)
│ └── ErrorBoundary (render errors)
├── Team Section
│ ├── fetch /api/team (parallel)
│ ├── own loading / error
│ └── ErrorBoundary
└── Projects Section
├── fetch /api/projects (parallel)
├── own loading / error
└── ErrorBoundary
✅ Fast: all 3 fire simultaneously, sections render as each resolves
✅ Resilient: team API 503 → only team block shows errorPerformance optimizations to mention
| Technique | Benefit |
|---|---|
| Parallel fetch (not sequential) | Total time = slowest API, not sum of all |
| Independent loading states | Perceived speed — fast sections show first |
staleTime caching (React Query) | Instant on revisit |
| Prefetch on navigation hover | Data ready before page mount |
| Skeleton loaders per section | Better CLS than full-page spinner |
AbortController cleanup | No stale updates on navigate away |
Interview answer (concise)
| # | Topic | One-liner |
|---|---|---|
| 1 | Hoisting | Declarations registered before execution; var=undefined, let=TDZ |
| 2 | Event Loop | Sync → microtasks → macrotask |
| 3 | Arrow vs normal | Arrow = lexical this; normal = dynamic this |
| 4 | Promise | pending/fulfilled/rejected async result |
| 5 | all vs allSettled | all = fail fast; allSettled = all outcomes, partial success |
| 6 | Virtual DOM | JS tree + diff → minimal real DOM updates |
| 7 | React performance | Split, memo, virtualize, cache, profile, Web Vitals |
| 8 | Redux workflow | dispatch → reducer → store → UI |
| 9 | Redux advantages | Single truth, predictable, DevTools, testable |
| 10 | Snapshot testing | Save render output, compare on future runs |
| 11 | First non-repeated | Frequency map + second left-to-right scan → "I" |
| 12 | Error Boundary | Catch render errors, show fallback per section |
| 13 | 3 API scenario | Parallel fetch, per-section state/error, Error Boundaries |
131. Spread & Rest Operators
Theory
Both use ... syntax but serve opposite purposes:
| Operator | Context | Action |
|---|---|---|
| Spread | Arrays, objects, function calls | Expands iterable into individual elements |
| Rest | Function parameters, destructuring | Collects remaining elements into array/object |
Pros & Cons
| Pros | Cons |
|---|---|
| Immutable updates (copy + modify) | Shallow copy only — nested objects shared |
| Clean function argument handling | Can be confusing spread vs rest |
| Merge objects/arrays easily | Performance cost on very large arrays |
Real-Life Example
// SPREAD — arrays
const cart = [item1, item2]
const updatedCart = [...cart, item3] // add
const withoutFirst = [...cart.slice(1)] // or cart.filter
const merged = [...cartA, ...cartB]
// SPREAD — objects (immutable state update)
const user = { name: 'Amit', role: 'user' }
const updated = { ...user, role: 'admin' } // override one field
const withDefaults = { theme: 'light', ...user } // user overrides defaults
// SPREAD — function calls
Math.max(...prices)
fetchOrder(...orderIds)
// REST — function parameters
function createOrder(customerId, ...items) {
console.log(items) // array of all remaining args
return { customerId, items }
}
createOrder('c_1', { id: 1 }, { id: 2 })
// REST — destructuring
const [head, ...tail] = [1, 2, 3, 4] // head=1, tail=[2,3,4]
const { name, ...rest } = user // rest = all other properties
// React state update
setFilters((prev) => ({ ...prev, city: 'Delhi', page: 1 }))
setItems((prev) => prev.filter((i) => i.id !== removedId))// ⚠️ Shallow copy trap
const state = { user: { name: 'Amit' }, cart: [] }
const copy = { ...state }
copy.user.name = 'Rahul'
console.log(state.user.name) // "Rahul" — nested object is shared!
// Fix: structuredClone(state) or deep merge132. Spread vs Rest Operator
Theory
Both use ... syntax — opposite purposes:
| Spread | Rest | |
|---|---|---|
| Action | Expands | Collects |
| Context | Arrays, objects, function calls | Function params, destructuring |
Real Example
// Spread
const merged = { ...defaults, ...userConfig }
const allItems = [...cartA, ...cartB]
Math.max(...numbers)
// Rest
function sum(...args) {
return args.reduce((a, b) => a + b, 0)
}
const [first, ...rest] = [1, 2, 3, 4] // first=1, rest=[2,3,4]
// React immutable update
setUser((prev) => ({ ...prev, name: 'New Name' }))133. TanStack Query (React Query)
Theory
TanStack Query manages server state — caching, background refetch, deduplication, stale-while-revalidate. Replaces most manual useEffect fetch logic.
Core: useQuery (read), useMutation (write), QueryClient.
Pros & Cons
| Pros | Cons |
|---|---|
| Caching, dedup, retry built-in | Learning curve |
| Less boilerplate than useEffect | Another dependency |
Real Example
function ProductList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['products', category],
queryFn: () => api.get(`/products?cat=${category}`).then((r) => r.data),
staleTime: 5 * 60 * 1000,
})
const mutation = useMutation({
mutationFn: (product) => api.post('/products', product),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['products'] }),
})
}134. WebSocket
Theory
WebSocket provides a persistent, full-duplex connection between client and server over a single TCP connection. Unlike HTTP polling, the server can push data to the client instantly.
Use for: live chat, notifications, order tracking, stock tickers, collaborative editing.
Protocol: starts as HTTP upgrade request (ws:// or wss://).
Pros & Cons
| WebSocket | HTTP Polling |
|---|---|
| ✅ Real-time, low latency | ✅ Simple, works everywhere |
| ✅ Single connection — efficient | ❌ Wasted requests when no updates |
| ✅ Bidirectional | ❌ Higher latency |
| ❌ Connection management complexity | ❌ Server load at scale |
| ❌ Doesn't work through some proxies |
Real-Life Example
function LiveOrderTracker({ orderId }: { orderId: string }) {
const [status, setStatus] = useState('preparing')
const [driverLocation, setDriverLocation] = useState<{
lat: number
lng: number
} | null>(null)
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/orders/${orderId}/live`)
ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe' }))
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
if (msg.type === 'status_update') setStatus(msg.status)
if (msg.type === 'location_update') setDriverLocation(msg.location)
}
ws.onerror = () => setStatus('connection_error')
ws.onclose = () => console.log('WebSocket closed')
// Reconnect with exponential backoff in production
return () => ws.close()
}, [orderId])
return (
<div>
<OrderStatusBadge status={status} />
{driverLocation && <LiveMap location={driverLocation} />}
</div>
)
}// Reconnection pattern
function connectWebSocket(url, { maxRetries = 5 } = {}) {
let retries = 0
let ws
function connect() {
ws = new WebSocket(url)
ws.onclose = () => {
if (retries < maxRetries) {
setTimeout(connect, Math.min(1000 * 2 ** retries++, 30000))
}
}
}
connect()
return () => ws?.close()
}135. WebSockets
Theory
WebSocket provides a persistent, full-duplex TCP connection between client and server over a single handshake. After upgrade from HTTP, both sides can send messages anytime without re-establishing connection.
Protocol: ws:// (dev) / wss:// (prod — always use in production)
Ideal for: live order tracking, chat, stock updates, delivery ETA.
Pros & Cons
| Pros | Cons |
|---|---|
| Real-time, low latency | Stateful — harder to scale |
| Bidirectional | Connection drops need reconnect logic |
| Less overhead than polling | Not ideal for request/response CRUD |
| Server can push instantly | Proxies/firewalls may block |
Real Example
function useOrderTracking(orderId) {
const [status, setStatus] = useState(null)
const wsRef = useRef(null)
useEffect(() => {
if (!orderId) return
const ws = new WebSocket(`wss://api.a quick-commerce app.com/orders/${orderId}/live`)
wsRef.current = ws
ws.onopen = () => console.log('Connected to order tracking')
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
setStatus(update) // { status: "out_for_delivery", eta: "8 min" }
}
ws.onerror = (err) => console.error('WebSocket error', err)
ws.onclose = () => console.log('Connection closed')
return () => ws.close()
}, [orderId])
return status
}Reconnection Pattern
function connectWithRetry(url, { maxRetries = 5, baseDelay = 1000 } = {}) {
let retries = 0
let ws
function connect() {
ws = new WebSocket(url)
ws.onopen = () => {
retries = 0
}
ws.onclose = () => {
if (retries < maxRetries) {
const delay = baseDelay * Math.pow(2, retries)
retries++
setTimeout(connect, delay)
}
}
}
connect()
return () => ws?.close()
}136. What are REST API methods and their differences?
Theory
REST (Representational State Transfer) uses HTTP methods to perform CRUD operations on resources identified by URLs. Each method has defined semantics for safety and idempotency.
Pros & Cons
| Method | Safe | Idempotent | Body | Use Case |
|---|---|---|---|---|
| GET | ✅ | ✅ | No | Read resource |
| POST | ❌ | ❌ | Yes | Create resource |
| PUT | ❌ | ✅ | Yes | Replace resource |
| PATCH | ❌ | ❌* | Yes | Partial update |
| DELETE | ❌ | ✅ | Optional | Remove resource |
*PATCH idempotency depends on implementation
Real-Life Example
// Restaurant API for a food delivery app
// GET — fetch restaurants (safe, cacheable)
const restaurants = await fetch('/api/restaurants?city=delhi').then((r) => r.json())
// POST — create a new order (not idempotent — duplicate = 2 orders)
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ restaurantId: 42, items: [{ id: 1, qty: 2 }] }),
})
// PUT — replace entire cart
await fetch('/api/cart', {
method: 'PUT',
body: JSON.stringify({ items: [{ id: 1, qty: 3 }] }),
})
// PATCH — update order status
await fetch('/api/orders/99', {
method: 'PATCH',
body: JSON.stringify({ status: 'delivered' }),
})
// DELETE — remove item from cart
await fetch('/api/cart/items/5', { method: 'DELETE' })137. What is fetchBaseQuery?
Answer
It is a default base query function built on fetch() for making HTTP requests. It simplifies making REST API calls.
138. What is reducerPath in createApi?
Answer
reducerPath is the key in the Redux state where the API slice reducer is mounted. Example: reducerPath: 'userApi'
139. What is Axios? How do you use it?
Theory
Axios is a promise-based HTTP client for browsers and Node.js. It simplifies API calls with features like automatic JSON transformation, request/response interceptors, timeout support, and cancellation.
Unlike raw fetch, Axios provides:
- Automatic JSON parsing
- Request/response interceptors (auth tokens, error handling)
- Built-in timeout and cancel token support
- Better error handling (non-2xx throws by default with config)
Pros & Cons
| Pros | Cons |
|---|---|
| Clean API, less boilerplate than fetch | Extra dependency (~13KB gzipped) |
| Interceptors for auth and global errors | fetch is native — no install needed |
| Automatic JSON transform | TanStack Query often preferred for data fetching layer |
| Request cancellation built-in | Can hide fetch details from juniors |
Real-Life Example
Installation
npm install axiosBasic usage
import axios from 'axios'
// GET
const { data: restaurants } = await axios.get('/api/restaurants', {
params: { city: 'bangalore', cuisine: 'north-indian' },
})
// POST
const { data: order } = await axios.post('/api/orders', {
restaurantId: 42,
items: [{ id: 1, quantity: 2 }],
})
// PUT / PATCH / DELETE
await axios.put(`/api/cart/${userId}`, { items: updatedCart })
await axios.patch(`/api/orders/${orderId}`, { status: 'cancelled' })
await axios.delete(`/api/cart/items/${itemId}`)Axios instance with interceptors (production pattern)
// api/client.js
import axios from 'axios'
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
})
// Request interceptor — attach auth token
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => Promise.reject(error),
)
// Response interceptor — handle 401 globally
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Redirect to login or refresh token
window.location.href = '/login'
}
if (error.response?.status === 503) {
toast.error('Service temporarily unavailable')
}
return Promise.reject(error)
},
)
export default apiClientUsage in React component
import { useEffect, useState } from 'react'
import apiClient from '../api/client'
function OrderHistory({ userId }) {
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const controller = new AbortController()
apiClient
.get(`/users/${userId}/orders`, { signal: controller.signal })
.then((res) => setOrders(res.data))
.catch((err) => {
if (!axios.isCancel(err)) setError(err.message)
})
.finally(() => setLoading(false))
return () => controller.abort()
}, [userId])
if (loading) return <Spinner />
if (error) return <ErrorBanner message={error} />
return <OrderList orders={orders} />
}140. What is OAuth and how does it work?
Theory
OAuth 2.0 is an authorization framework that lets a user grant a third-party application limited access to their resources without sharing their password. It defines roles: Resource Owner (user), Client (your app), Authorization Server (Google, GitHub), Resource Server (API).
Common flows:
- Authorization Code (most secure, used by web apps)
- PKCE (extension for SPAs and mobile)
- Client Credentials (machine-to-machine)
Pros & Cons
| Pros | Cons |
|---|---|
| No password sharing with third parties | Complex to implement correctly |
| Granular scopes (read email, not password) | Token management overhead |
| Industry standard | Misconfiguration leads to security holes |
Real-Life Example
"Sign in with Google" on a food delivery app:
1. User clicks "Sign in with Google"
2. App redirects to Google Authorization Server:
GET https://accounts.google.com/o/oauth2/v2/auth
?client_id=YOUR_APP_ID
&redirect_uri=https://app.com/callback
&response_type=code
&scope=email profile
&state=random_csrf_token
3. User logs in and grants permission on Google's page
4. Google redirects back:
https://app.com/callback?code=AUTH_CODE&state=random_csrf_token
5. Backend exchanges code for tokens (server-side, never in browser):
POST https://oauth2.googleapis.com/token
code=AUTH_CODE&client_secret=SECRET&grant_type=authorization_code
6. Backend receives: { access_token, refresh_token, id_token }
7. Backend creates session / JWT for the user in your app141. You need to implement server-side caching to reduce API calls. How would you approach it?
Description
Repeated identical upstream API calls waste latency and cost. Next.js offers multiple cache layers — choosing the right one depends on how often data changes and whether it's per-user or global.
Theory
| Layer | Mechanism | Scope |
|---|---|---|
| Request memoization | Same fetch in one render tree | Single request |
| Data Cache | fetch with cache / revalidate | Cross-request, persistent |
| Full Route Cache | Static pages at build time | Static routes |
| Router Cache | Client-side RSC payload cache | Navigation |
| Redis / CDN | External cache in Route Handler or BFF | Production scale |
revalidate options
fetch(url, { cache: 'force-cache' }) // default — cache until invalidated
fetch(url, { next: { revalidate: 60 } }) // ISR — refresh every 60s
fetch(url, { cache: 'no-store' }) // never cache
fetch(url, { next: { tags: ['products'] } }) // on-demand invalidationPros & Cons
| Cached reads | Always fresh (no-store) |
|---|---|
| ✅ Lower latency, fewer API bills | ✅ Simplest mental model |
| ✅ Survives traffic spikes | ❌ Upstream overload at scale |
| ❌ Stale data risk | ❌ Slower every request |
Real Example — Product catalog BFF
// lib/products.ts
export async function getProducts() {
const res = await fetch(`${process.env.CATALOG_API}/products`, {
next: { revalidate: 300, tags: ['products'] }, // 5 min ISR
})
if (!res.ok) throw new Error('Catalog unavailable')
return res.json()
}// app/api/revalidate/route.ts — on-demand after CMS publish
import { revalidateTag } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(req: NextRequest) {
const secret = req.nextUrl.searchParams.get('secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 })
}
revalidateTag('products')
return Response.json({ revalidated: true })
}// Redis layer for hot per-user data (Route Handler)
// app/api/dashboard/route.ts
import { Redis } from '@upstash/redis'
const redis = Redis.fromEnv()
export async function GET(req: Request) {
const userId = await getUserId(req)
const cacheKey = `dashboard:${userId}`
const cached = await redis.get(cacheKey)
if (cached) return Response.json(cached)
const data = await fetchDashboardFromUpstream(userId)
await redis.setex(cacheKey, 60, data) // 60s TTL
return Response.json(data)
}Performance
142. A page contains a heavy chart library that is only needed after user interaction. How would you optimize the loading strategy?
Description
Chart libraries (Recharts, Chart.js, D3) can add 100–300+ KB to the client bundle. If the chart appears only after "View analytics" or tab click, it should not load on initial page visit.
Theory
| Strategy | Use case |
|---|---|
next/dynamic + ssr: false | Chart needs window; load on mount |
| Dynamic import on click | User must click before download starts |
| Server Component for data | Fetch aggregates on server; pass props to chart island |
| Suspense fallback | Skeleton while chunk loads |
Pros & Cons
| Load on interaction | Bundle in main page |
|---|---|
| ✅ Smaller initial JS | ❌ Slower first load |
| ✅ Better INP / LCP | ❌ Wasted bytes if user never opens chart |
| ❌ Brief delay on first open | ❌ Mobile users suffer |
Real Example — Analytics tab with Recharts
// app/sales/page.tsx — Server Component
import { SalesSummary } from './sales-summary'
import { SalesChartLoader } from './sales-chart-loader'
export default async function SalesPage() {
const summary = await getSalesSummary() // server — no chart lib
return (
<main>
<SalesSummary data={summary} />
<SalesChartLoader chartData={summary.byMonth} />
</main>
)
}// app/sales/sales-chart-loader.tsx
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
const SalesChart = dynamic(() => import('./sales-chart'), {
ssr: false,
loading: () => <div className="h-64 animate-pulse bg-gray-100" />,
})
export function SalesChartLoader({ chartData }) {
const [showChart, setShowChart] = useState(false)
if (!showChart) {
return (
<button type="button" onClick={() => setShowChart(true)} className="btn-primary">
View chart
</button>
)
}
return <SalesChart data={chartData} />
}// app/sales/sales-chart.tsx — only loaded after button click
'use client'
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer } from 'recharts'
export default function SalesChart({ data }) {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<XAxis dataKey="month" />
<YAxis />
<Line type="monotone" dataKey="revenue" stroke="#2563eb" />
</LineChart>
</ResponsiveContainer>
)
}143. Bundle Size Optimization
Theory
Smaller bundles = faster download, parse, and execute. Target < 200 KB gzipped for initial JS.
Strategies: code splitting, tree shaking, analyze bundle, replace heavy libraries, dynamic imports, remove unused code.
Pros & Cons
| Optimized bundle | Large bundle |
|---|---|
| ✅ Faster TTI and LCP | ❌ Slow on 3G/4G |
| ✅ Better mobile experience | ❌ Higher bounce rate |
| ✅ Lower CDN costs | ❌ Poor Lighthouse scores |
Real-Life Example
npx vite-bundle-visualizer
npx source-map-explorer dist/assets/*.js
# Find heavy imports
npm install -D webpack-bundle-analyzer// ❌ Imports entire library (70KB)
import _ from 'lodash'
_.debounce(fn, 300)
// ✅ Import only what you need (2KB)
import debounce from 'lodash/debounce'
// ❌ Import all icons
import * as Icons from 'lucide-react'
// ✅ Import specific icon
import { Search, Cart } from 'lucide-react'
// Replace moment.js (300KB) with date-fns (tree-shakeable)
import { format } from 'date-fns'| Technique | Savings |
|---|---|
| Route code splitting | 40–70% initial bundle reduction |
| lodash → lodash-es + per-method import | ~65 KB |
| moment → date-fns | ~280 KB |
| Remove unused dependencies | Varies |
144. Closure memory leak and fix
Theory
A memory leak occurs when JavaScript retains references to objects that are no longer needed. Closures are a common cause — an inner function holds references to outer variables, preventing garbage collection of large objects even after the outer function returns.
Pros & Cons
| Awareness of closure leaks | Ignorance |
|---|---|
| ✅ Clean up timers, listeners, refs | ❌ App slows over time |
| ✅ Pass only needed data into closures | ❌ Detached DOM nodes stay in memory |
| ✅ Use WeakMap for DOM metadata |
Real-Life Example
// ❌ LEAK — closure holds entire dataset forever
function createTransactionHandler(transactions) {
// transactions = 50MB array
return function handleClick(id) {
const txn = transactions.find((t) => t.id === id)
console.log(txn.amount)
}
}
const handler = createTransactionHandler(hugeTransactionList)
button.addEventListener('click', handler)
// Even if button removed, handler + hugeTransactionList stay in memory
// ✅ FIX 1 — pass only what's needed
function createTransactionHandler(transactionMap) {
return function handleClick(id) {
console.log(transactionMap.get(id)?.amount)
}
}
// ✅ FIX 2 — remove listener on cleanup
function setupHandler(button, transactions) {
const handler = (e) => {
const id = e.target.dataset.id
console.log(transactions.find((t) => t.id === id)?.amount)
}
button.addEventListener('click', handler)
return () => button.removeEventListener('click', handler)
}
// ✅ FIX 3 — WeakMap for DOM-associated data (GC when element removed)
const txnMetadata = new WeakMap()
function attachTransaction(el, data) {
txnMetadata.set(el, { id: data.id, amount: data.amount })
// When el is GC'd, WeakMap entry is GC'd too
}// ❌ LEAK — interval in closure
function startPolling(userId) {
const cache = loadMassiveUserCache(userId)
setInterval(() => {
updateUI(cache.lastBalance)
}, 1000)
// Never cleared — cache lives forever
}
// ✅ FIX
function startPolling(userId) {
let cache = loadMassiveUserCache(userId)
const intervalId = setInterval(() => updateUI(cache.lastBalance), 1000)
return () => {
clearInterval(intervalId)
cache = null // release reference
}
}145. Code Splitting
Theory
Code splitting breaks your JavaScript bundle into smaller chunks loaded on demand. The bundler (Webpack, Vite) creates separate files per dynamic import or entry point.
Split strategies:
- Route-based — each page is a chunk (biggest win)
- Component-based — heavy widgets loaded when needed
- Vendor splitting — separate chunk for
node_modules
Pros & Cons
| Pros | Cons |
|---|---|
| Smaller initial bundle | More HTTP requests (mitigated by HTTP/2) |
| Parallel chunk loading | Requires Suspense/error boundaries |
| Cache-friendly (vendor chunk stable) | Complexity in configuration |
Real-Life Example
// Route-based splitting (automatic with lazy)
const routes = [
{ path: '/', component: lazy(() => import('./Home')) },
{ path: '/dashboard', component: lazy(() => import('./Dashboard')) },
]
// Webpack magic comment — named chunk
const HeavyChart = lazy(() => import(/* webpackChunkName: "charts" */ './HeavyChart'))
// Vite — automatic code splitting on dynamic import
const module = await import('./heavy-module.js')// webpack.config.js — split vendor
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
},
},
},
},146. Code Splitting & Lazy Loading
Theory
Code splitting breaks the bundle into chunks loaded on demand. React.lazy(() => import("./Page")) dynamically imports a component.
Always pair with <Suspense fallback={...}>.
Real Example
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}147. Core Web Vitals (LCP, INP, CLS)
Theory
Google's Core Web Vitals measure real-user experience:
| Metric | Measures | Good |
|---|---|---|
| LCP (Largest Contentful Paint) | Loading — largest visible element | ≤ 2.5s |
| INP (Interaction to Next Paint) | Responsiveness (replaced FID) | ≤ 200ms |
| CLS (Cumulative Layout Shift) | Visual stability | ≤ 0.1 |
Pros & Cons
| Monitoring Web Vitals | Ignoring them |
|---|---|
| ✅ SEO ranking factor | ❌ Higher bounce rates |
| ✅ Data-driven optimization | ❌ Poor mobile UX |
| ✅ RUM catches real user issues | ❌ Lighthouse lab scores ≠ field data |
Real-Life Example
// Measure in production
import { onLCP, onINP, onCLS } from 'web-vitals'
function sendToAnalytics({ name, value, id }) {
gtag('event', name, { value: Math.round(value), event_id: id })
}
onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)// LCP — preload hero image
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<img src="/hero.webp" width={1200} height={600} alt="Hero" fetchpriority="high" />
// CLS — reserve space
.product-card img { aspect-ratio: 4/3; width: 100%; }
// INP — defer heavy work
import { useTransition } from "react";
const [isPending, startTransition] = useTransition();
startTransition(() => setFilteredProducts(heavyFilter(allProducts)));148. Debounce and Throttle implementation
Debounce — execute after pause in events
Waits until the user stops triggering for delay ms, then runs once.
function debounce(fn, delay) {
let timerId
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Usage: search input — API call only after user stops typing
const search = debounce((query) => {
console.log('Searching:', query)
}, 300)Throttle — execute at most once per interval
Runs immediately, then ignores subsequent calls for limit ms.
function throttle(fn, limit) {
let inThrottle = false
return function (...args) {
if (!inThrottle) {
fn.apply(this, args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
// Usage: scroll handler — fire at most every 100ms
const onScroll = throttle(() => {
console.log('Scroll position:', window.scrollY)
}, 100)Leading + trailing debounce (advanced)
function debounce(fn, delay, { leading = false, trailing = true } = {}) {
let timerId
let lastArgs
let lastThis
return function (...args) {
lastArgs = args
lastThis = this
const callNow = leading && !timerId
clearTimeout(timerId)
timerId = setTimeout(() => {
timerId = null
if (trailing && !callNow) fn.apply(lastThis, lastArgs)
}, delay)
if (callNow) fn.apply(this, args)
}
}When to use which
| Pattern | Use case |
|---|---|
| Debounce | Search input, resize end, form validation |
| Throttle | Scroll, mouse move, button spam prevention |
149. Debounce vs Throttle
Theory
Both control execution frequency of functions during rapid events:
- Debounce — wait until activity stops, then execute once
- Throttle — execute at most once per time window during continuous activity
Pros & Cons
| Debounce | Throttle |
|---|---|
| ✅ Minimizes executions (API cost) | ✅ Regular updates during scroll |
| ✅ Great for search, auto-save | ✅ Immediate first response |
| ❌ Delays until pause | ❌ May miss final event |
| ❌ More calls than debounce |
Real-Life Example
function debounce(fn, delay) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
function throttle(fn, limit) {
let inThrottle = false
return (...args) => {
if (!inThrottle) {
fn(...args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
// Debounce — restaurant search
const search = debounce((q) => {
fetch(`/api/restaurants?q=${q}`).then(renderResults)
}, 300)
// Throttle — infinite scroll position tracker
const onScroll = throttle(() => {
if (nearBottom()) loadMoreItems()
}, 200)
// Throttle — prevent double-submit on payment button
const handlePay = throttle(() => processPayment(), 2000)| Scenario | Use |
|---|---|
| Search input | Debounce |
| Window resize (layout recalc) | Debounce |
| Scroll handler | Throttle |
| Mouse move / drag | Throttle |
| Auto-save form | Debounce |
| API rate limiting | Throttle |
150. Debouncing
Theory
Debouncing delays execution until the user stops an action for a specified time. Only the last call in a burst runs.
Common use: search input — wait until user stops typing before API call.
Pros & Cons
| Pros | Cons |
|---|---|
| Reduces API calls / CPU work | Slight delay in response |
| Better UX for search | Wrong delay feels laggy |
| Prevents rate-limit errors | Needs cleanup on unmount |
Debounce vs Throttle
| Debounce | Throttle | |
|---|---|---|
| Runs | After pause | Once per interval |
| Use for | Search, auto-save | Scroll, resize |
Debounce Utility
function debounce(fn, delay = 300) {
let timer
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}Custom useDebounce Hook
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}Search with Debounce + useFetch
function ProductSearch() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 400)
const { data, loading } = useFetch(
debouncedQuery ? `https://api.example.com/products?q=${encodeURIComponent(debouncedQuery)}` : null,
{ enabled: debouncedQuery.length >= 2 },
)
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
aria-label="Search products"
/>
{loading && <span>Searching...</span>}
<ProductList products={data ?? []} />
</>
)
}Debounced Callback Hook
function useDebouncedCallback(callback, delay = 300) {
const callbackRef = useRef(callback)
const timerRef = useRef(null)
useEffect(() => {
callbackRef.current = callback
}, [callback])
useEffect(() => () => clearTimeout(timerRef.current), [])
return useCallback(
(...args) => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => callbackRef.current(...args), delay)
},
[delay],
)
}
// Usage
const debouncedSave = useDebouncedCallback((text) => saveToApi(text), 500)
;<input onChange={(e) => debouncedSave(e.target.value)} />| # | Topic | One-liner |
|---|---|---|
| 1 | Counter | useState + functional updaters, optional min/max |
| 2 | useFetch | useState + useEffect + AbortController cleanup |
| 3 | Merge objects | reduce + spread (shallow) or recursive deepMerge |
| 4 | useDeferredValue | Defer heavy re-renders; input stays responsive |
| 5 | Context API | createContext → Provider → useContext |
| 6 | Custom hooks | Reuse stateful logic; name with use |
| 7 | Debouncing | Wait for pause before executing — search inputs |
the company Interview Strategy
Coding round
- Counter — start simple, add bounds/accessibility if time allows
- useFetch — always mention AbortController cleanup
- Merge objects — clarify shallow vs deep with interviewer before coding
Fundamentals round
- Connect useDeferredValue with debouncing — both optimize search UX differently
- Explain Context vs custom hooks — Context shares data; hooks share logic
- Mention TanStack Query as production upgrade over raw useFetch
the company's 1st round balances hands-on React coding with modern hook knowledge — practice building counters and hooks from scratch without copy-paste templates.
151. Debouncing implementation
Theory
Debouncing delays execution of a function until after a specified pause in triggering events. If the event fires again before the delay expires, the timer resets. The function runs only once the user stops the action.
Classic use: search-as-you-type, resize-end handlers, auto-save on form changes.
Pros & Cons
| Pros | Cons |
|---|---|
| Reduces API calls dramatically | Adds perceived latency (waits for pause) |
| Prevents expensive work on every keystroke | Can feel unresponsive if delay is too long |
| Simple to implement | May skip intermediate states user expected to see |
Real-Life Example
function debounce(fn, delay) {
let timerId
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Real-life: Restaurant search on a food-delivery platform-style app
const searchRestaurants = debounce(async (query) => {
const res = await fetch(`/api/restaurants?q=${encodeURIComponent(query)}`)
const data = await res.json()
renderResults(data)
}, 300)
// User types "pizza" → only 1 API call after they stop typing for 300ms
document.getElementById('search').addEventListener('input', (e) => {
searchRestaurants(e.target.value)
})152. Design & Implement a Virtualized Infinite Feed
Description
the organization machine coding ask: Build a feed (like Twitter/LinkedIn) that loads more items on scroll without rendering 10,000 DOM nodes.
Theory
| Approach | Idea |
|---|---|
| Windowing / virtualization | Only render visible rows + overscan buffer |
| Infinite scroll | Fetch next page when sentinel enters viewport |
| Intersection Observer | Detect when sentinel is visible (no scroll event spam) |
Libraries: @tanstack/react-virtual, react-window, react-virtuoso.
Pros & Cons
| Virtualized list | Render all items |
|---|---|
| ✅ Constant DOM size | ✅ Simpler code |
| ✅ Smooth scroll on 100K items | ❌ Memory + paint cost explodes |
| ❌ Variable row height is harder | ❌ Mobile browsers crash |
Real Example — Virtualized infinite feed
'use client'
import { useRef, useCallback, useEffect } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import { useInfiniteQuery } from '@tanstack/react-query'
type FeedItem = { id: string; author: string; text: string }
async function fetchFeedPage({ pageParam = 0 }) {
const res = await fetch(`/api/feed?cursor=${pageParam}`)
return res.json() as Promise<{
items: FeedItem[]
nextCursor: number | null
}>
}
export function VirtualizedFeed() {
const parentRef = useRef<HTMLDivElement>(null)
const sentinelRef = useRef<HTMLDivElement>(null)
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeedPage,
getNextPageParam: (last) => last.nextCursor ?? undefined,
})
const items = data?.pages.flatMap((p) => p.items) ?? []
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 120,
overscan: 5,
})
// Infinite load when sentinel visible
const onIntersect = useCallback(
(entries: IntersectionObserverEntry[]) => {
if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
},
[fetchNextPage, hasNextPage, isFetchingNextPage],
)
useEffect(() => {
const el = sentinelRef.current
if (!el) return
const observer = new IntersectionObserver(onIntersect, {
rootMargin: '200px',
})
observer.observe(el)
return () => observer.disconnect()
}, [onIntersect])
if (status === 'pending') return <FeedSkeleton />
return (
<div ref={parentRef} className="h-[80vh] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}>
{virtualizer.getVirtualItems().map((row) => {
const item = items[row.index]
return (
<article
key={item.id}
className="feed-card"
style={{
position: 'absolute',
top: 0,
transform: `translateY(${row.start}px)`,
height: `${row.size}px`,
}}>
<strong>{item.author}</strong>
<p>{item.text}</p>
</article>
)
})}
</div>
<div ref={sentinelRef} aria-hidden className="h-1" />
{isFetchingNextPage && <p>Loading more…</p>}
</div>
)
}- Virtualizer computes visible range from scroll offset
- Only ~15–20 DOM nodes exist regardless of total items
- Sentinel + Intersection Observer triggers
fetchNextPage key={item.id}for stable reconciliationoverscanreduces blank flashes on fast scroll
153. Explain code splitting and lazy loading
Theory
Code splitting breaks your bundle into smaller chunks loaded on demand. Lazy loading defers loading a chunk until it's needed (route, modal, heavy component).
Route-based splitting
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}Component-level splitting
const HeavyChart = lazy(() => import('./HeavyChart'))
function Analytics() {
const [show, setShow] = useState(false)
return (
<>
<button onClick={() => setShow(true)}>Show Chart</button>
{show && (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)}
</>
)
}Webpack / Vite magic comments
const Admin = lazy(() => import(/* webpackChunkName: "admin" */ './Admin'))Best practices
- Split by route first (biggest wins)
- Prefetch on hover:
import("./Dashboard")inonMouseEnter - Always wrap lazy components in
<Suspense> - Monitor bundle size with
webpack-bundle-analyzerorrollup-plugin-visualizer
154. How does memoization work in React?
Theory
Memoization caches the result of a computation or component render and returns the cached version when inputs haven't changed. React provides three levels:
| Tool | Caches | Skips |
|---|---|---|
useMemo | Computed value | Re-computation |
useCallback | Function reference | New function creation |
React.memo | Component render | Re-render if props unchanged (shallow compare) |
React compares previous and next props/state using Object.is (shallow equality).
Pros & Cons
| Pros | Cons |
|---|---|
| Avoids expensive recalculations | Memory overhead for cached values |
| Prevents child re-renders | Shallow compare misses deep object changes |
| Profile-guided optimization | Over-memoization adds complexity for no gain |
Real-Life Example
function OrderSummary({ orders, taxRate }) {
// useMemo — expensive aggregation
const total = useMemo(() => {
return orders.reduce((sum, o) => sum + o.amount, 0)
}, [orders])
const tax = useMemo(() => total * taxRate, [total, taxRate])
return <div>Total: ₹{total + tax}</div>
}
const OrderRow = React.memo(function OrderRow({ order, onCancel }) {
return (
<div>
{order.name} — ₹{order.amount}
<button onClick={() => onCancel(order.id)}>Cancel</button>
</div>
)
})
function OrderList({ orders }) {
const handleCancel = useCallback((id) => {
cancelOrder(id)
}, []) // stable reference → OrderRow won't re-render unnecessarily
return orders.map((o) => <OrderRow key={o.id} order={o} onCancel={handleCancel} />)
}155. How to optimize performance?
Answer
- Use
React.memo,useMemo, anduseCallback - Avoid unnecessary re-renders
- Use lazy loading and code splitting
- Optimize rendering of large lists with virtualization
156. How would you implement infinite scrolling efficiently?
Theory — Core techniques
| Technique | Purpose |
|---|---|
| Intersection Observer | Detect when sentinel enters viewport (no scroll event spam) |
| Virtualization | Render only visible rows (@tanstack/react-virtual) |
| Cursor-based pagination | Stable pagination for large datasets (?cursor=abc&limit=20) |
| Request deduplication | Prevent duplicate fetches on fast scroll |
| Prefetch next page | Load before user reaches bottom |
| Skeleton / placeholder | Perceived performance |
Practical Example — Intersection Observer hook
import { useCallback, useEffect, useRef } from 'react'
function useInfiniteScroll(onLoadMore: () => void, hasMore: boolean) {
const observerRef = useRef<IntersectionObserver | null>(null)
const sentinelRef = useCallback(
(node: HTMLDivElement | null) => {
if (observerRef.current) observerRef.current.disconnect()
if (!node || !hasMore) return
observerRef.current = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) onLoadMore()
},
{ rootMargin: '200px' }, // prefetch before visible
)
observerRef.current.observe(node)
},
[onLoadMore, hasMore],
)
useEffect(() => () => observerRef.current?.disconnect(), [])
return sentinelRef
}Practical Example — With TanStack Query
import { useInfiniteQuery } from '@tanstack/react-query'
function useRestaurants() {
return useInfiniteQuery({
queryKey: ['restaurants'],
queryFn: ({ pageParam }) => fetch(`/api/restaurants?cursor=${pageParam ?? ''}`).then((r) => r.json()),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: undefined as string | undefined,
})
}
function RestaurantFeed() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useRestaurants()
const sentinelRef = useInfiniteScroll(fetchNextPage, !!hasNextPage)
const items = data?.pages.flatMap((p) => p.items) ?? []
return (
<div>
{items.map((r) => (
<RestaurantCard key={r.id} restaurant={r} />
))}
<div ref={sentinelRef} />
{isFetchingNextPage && <Spinner />}
</div>
)
}Virtualization for 10k+ items
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualList({ items }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
overscan: 5,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((row) => (
<div
key={row.key}
style={{
position: 'absolute',
top: row.start,
height: row.size,
width: '100%',
}}>
<RestaurantCard restaurant={items[row.index]} />
</div>
))}
</div>
</div>
)
}Interview answer highlights
157. How would you optimize a React application with thousands of rows?
Strategy checklist
| Technique | Purpose |
|---|---|
| Virtualization | Render only visible rows (~20 instead of 10,000) |
| Stable keys | Prevent unnecessary unmount/remount |
| Memoization | React.memo on row components |
| Pagination / infinite scroll | Load data in chunks |
| Web Workers | Offload sorting/filtering from main thread |
| Avoid inline objects/functions | Prevent memo breakage |
Practical Example — Virtual list with TanStack Virtual
import { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualTable({ rows }) {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
overscan: 10,
})
return (
<div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<div
key={row.id}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
width: '100%',
}}>
<Row item={row} />
</div>
)
})}
</div>
</div>
)
}
const Row = React.memo(function Row({ item }) {
return <div className="row">{item.name}</div>
})Additional optimizations
// Debounce search filter
const debouncedFilter = useDeferredValue(searchQuery)
// Sort in worker or server, not on every keystroke in main thread
const filtered = useMemo(() => rows.filter((r) => r.name.includes(debouncedFilter)), [rows, debouncedFilter])Interview answer
158. Image Optimization (WebP, AVIF)
Theory
Images are often the largest page weight. Modern formats compress better than JPEG/PNG:
| Format | vs JPEG | Browser support |
|---|---|---|
| WebP | ~25–35% smaller | Universal (2024+) |
| AVIF | ~50% smaller | Chrome, Firefox, Safari 16+ |
| JPEG | Baseline | Universal |
Also use: responsive srcset, lazy loading, explicit dimensions (CLS), CDN image optimization.
Pros & Cons
| Modern formats (WebP/AVIF) | Legacy JPEG/PNG only |
|---|---|
| ✅ 25–50% smaller files | ❌ Slower LCP |
| ✅ Better quality at same size | ❌ Higher bandwidth costs |
| ❌ Need fallback for old browsers |
Real-Life Example
<!-- Picture element — AVIF → WebP → JPEG fallback -->
<picture>
<source srcset="/images/hero.avif" type="image/avif" />
<source srcset="/images/hero.webp" type="image/webp" />
<img src="/images/hero.jpg" alt="Team collaboration" width="1200" height="600" loading="lazy" decoding="async" />
</picture>
<!-- Responsive srcset -->
<img
src="/product-400.webp"
srcset="/product-400.webp 400w, /product-800.webp 800w, /product-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 400px"
alt="Product"
width="400"
height="300" />// Next.js Image — automatic optimization
import Image from 'next/image'
;<Image
src="/product.jpg"
alt="Product"
width={400}
height={300}
placeholder="blur"
blurDataURL={blurDataUrl}
priority={isAboveFold}
/>159. Infinite Scroll
Theory
Infinite scroll automatically loads the next batch of data when the user scrolls near the bottom of the page. Typically uses Intersection Observer to detect when a sentinel element enters the viewport.
Combine with virtualization when lists exceed a few hundred DOM nodes.
Pros & Cons
| Pros | Cons |
|---|---|
| Seamless UX for feeds | Hard to access footer |
| No pagination clicks | DOM grows without virtualization |
| Great for mobile | Back-button scroll position issues |
| Prefetch before bottom reached | Accessibility concerns (no clear end) |
Real-Life Example
import { useInfiniteQuery } from '@tanstack/react-query'
import { useRef, useCallback } from 'react'
function useInfiniteScroll(onLoadMore: () => void, hasMore: boolean) {
const observer = useRef<IntersectionObserver | null>(null)
const sentinelRef = useCallback(
(node: HTMLDivElement | null) => {
if (observer.current) observer.current.disconnect()
if (!node || !hasMore) return
observer.current = new IntersectionObserver(
([entry]) => entry.isIntersecting && onLoadMore(),
{ rootMargin: '200px' }, // prefetch before visible
)
observer.current.observe(node)
},
[onLoadMore, hasMore],
)
return sentinelRef
}
function Feed() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam }) => fetch(`/api/feed?cursor=${pageParam ?? ''}`).then((r) => r.json()),
getNextPageParam: (last) => last.nextCursor ?? undefined,
initialPageParam: undefined as string | undefined,
})
const items = data?.pages.flatMap((p) => p.items) ?? []
const sentinelRef = useInfiniteScroll(fetchNextPage, !!hasNextPage)
return (
<div>
{items.map((item) => (
<FeedCard key={item.id} item={item} />
))}
<div ref={sentinelRef} aria-hidden="true" />
{isFetchingNextPage && <Spinner />}
</div>
)
}160. Intersection Observer & Rendering Optimizations
Theory
Intersection Observer asynchronously reports when an element enters/leaves a viewport (or scroll
container). Better than scroll listeners — no main-thread firing on every pixel.
| Technique | When |
|---|---|
React.memo | Pure child re-renders from parent |
useMemo / useCallback | Expensive compute / stable callbacks to memoized children |
| Code splitting | React.lazy + Suspense |
| Virtualization | Long lists |
| Intersection Observer | Lazy images, infinite scroll, analytics impressions |
content-visibility: auto | CSS native offscreen skip (modern browsers) |
| Debounce resize handlers | Layout-heavy components |
Real Example — Lazy image + impression tracking
function LazyFeedImage({ src, alt }: { src: string; alt: string }) {
const imgRef = useRef<HTMLImageElement>(null)
const [loaded, setLoaded] = useState(false)
useEffect(() => {
const el = imgRef.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.src = src
setLoaded(true)
observer.disconnect()
}
},
{ rootMargin: '100px' },
)
observer.observe(el)
return () => observer.disconnect()
}, [src])
return <img ref={imgRef} alt={alt} className={loaded ? 'opacity-100' : 'opacity-0'} width={400} height={225} />
}// Memoize heavy child
const ChartPanel = React.memo(function ChartPanel({ data }: { data: Point[] }) {
return <ExpensiveChart data={data} />
})
function Dashboard({ raw }: { raw: RawRow[] }) {
const chartData = useMemo(() => aggregate(raw), [raw])
return <ChartPanel data={chartData} />
}Behavioral round — use STAR (Situation, Task, Action, Result). Keep answers 2–3 minutes.
161. Lazy Loading
Theory
Lazy loading defers loading resources until needed. In React: React.lazy() + <Suspense> for components. For images: loading="lazy".
Reduces initial bundle size — users download only what they visit.
Real Example
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Reports = lazy(() => import("./pages/Reports"));
const Admin = lazy(() => import("./pages/Admin"));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
<Route path="/admin" element={<Admin />} />
</Routes>
</Suspense>
);
}
// Prefetch on hover
<Link to="/reports" onMouseEnter={() => import("./pages/Reports")}>
Reports
</Link>
// Image lazy load
<img src="/chart.png" alt="Analytics" loading="lazy" width={800} height={400} />| # | Topic | One-liner |
|---|---|---|
| 1 | Key skills | React, TS, Redux, auth, testing, mentoring |
| 2 | Why React | Components, ecosystem, hooks, job market |
| 3 | Last project role | STAR — ownership, impact, metrics |
| 4 | Auth in React | httpOnly cookies, AuthProvider, protected routes |
| 5 | session vs local | Tab-scoped vs persistent; never for tokens |
| 6 | Context API | Split by concern, memoize value |
| 7 | Auth deep dive | Login → cookie → refresh → RBAC |
| 8 | React 19 features | Actions, useOptimistic, use hook, no forwardRef |
| 9 | addEventListener | Register, capture/bubble, cleanup in useEffect |
| 10 | useLayoutEffect | Before paint — DOM measure, no flicker |
| 11 | Debounce/throttle | Pause vs rate-limit |
| 12 | Event loop | Sync → micro → macro |
| 13 | useState | Local state + setter, functional updates |
| 14 | useEffect | Side effects + cleanup + deps |
| 15 | Redux | dispatch → reducer → store |
| 16 | Redux perf | createSelector, normalize, RTK Query |
| 17 | Lazy loading | React.lazy + Suspense |
Engineo tests senior breadth — auth, Redux, hooks, and JS fundamentals in one interview. Prepare short confident answers with one example each.
162. Memory Management
Theory
JavaScript uses automatic garbage collection — memory for unreachable objects is reclaimed. The primary algorithm is mark-and-sweep: GC marks all objects reachable from roots (global, call stack, closures), then frees unmarked objects.
V8 uses generational GC: young generation (short-lived objects, frequent Scavenge) and old generation (long-lived, Mark-Sweep-Compact).
Common memory leak causes
- Global variables holding references
- Forgotten timers and intervals
- Event listeners not removed
- Closures holding large objects
- Detached DOM nodes still referenced in JS
- Unbounded caches
Pros & Cons
| Automatic GC | Manual memory management (C/C++) |
|---|---|
| ✅ Developer doesn't free memory | ✅ Predictable memory usage |
| ❌ GC pauses can affect performance | ❌ Manual errors (leaks, dangling pointers) |
| ❌ Leaks still possible via references |
Real-Life Example
// ❌ Memory leak — global cache grows forever
const cache = {}
function fetchUser(id) {
if (!cache[id]) cache[id] = expensiveFetch(id)
return cache[id]
}
// ✅ Bounded cache with LRU or WeakMap
const cache = new Map()
const MAX_SIZE = 100
function fetchUserCached(id) {
if (cache.has(id)) return cache.get(id)
const result = expensiveFetch(id)
if (cache.size >= MAX_SIZE) cache.delete(cache.keys().next().value)
cache.set(id, result)
return result
}
// ❌ Leak — closure holds large data
function setupHandler() {
const hugeData = new Array(1e6).fill('x')
return () => console.log(hugeData.length) // hugeData never freed
}
// ✅ Cleanup pattern
function setupPolling(callback) {
const intervalId = setInterval(callback, 5000)
return () => clearInterval(intervalId) // caller cleans up
}
// WeakMap — doesn't prevent GC of keys
const domMetadata = new WeakMap()
function attachMetadata(element, data) {
domMetadata.set(element, data)
// When element is removed from DOM and unreferenced, entry is GC'd
}163. Pagination
Theory
Pagination divides large datasets into discrete pages, fetching and displaying one chunk at a time. Users navigate with page numbers, next/previous buttons, or "load more."
Two common API patterns:
- Offset-based:
?page=2&limit=20— simple but slow on deep pages - Cursor-based:
?cursor=abc123&limit=20— stable for large, changing datasets
Pros & Cons
| Pagination | Infinite scroll |
|---|---|
| ✅ Predictable navigation, shareable URLs | ✅ Seamless browsing experience |
| ✅ Better for SEO (page URLs) | ✅ Better for mobile feeds |
| ✅ Lower memory — only one page in DOM | ❌ Hard to reach footer |
| ❌ Extra clicks for users | ❌ DOM grows unless virtualized |
Real-Life Example
function ProductList() {
const [page, setPage] = useState(1)
const limit = 20
const { data, isLoading } = useQuery({
queryKey: ['products', page],
queryFn: () => fetch(`/api/products?page=${page}&limit=${limit}`).then((r) => r.json()),
keepPreviousData: true, // show old page while loading next
})
return (
<>
<Grid products={data?.items ?? []} />
<nav aria-label="Pagination">
<button disabled={page === 1} onClick={() => setPage((p) => p - 1)}>
Previous
</button>
<span>
Page {page} of {data?.totalPages}
</span>
<button disabled={page >= data?.totalPages} onClick={() => setPage((p) => p + 1)}>
Next
</button>
</nav>
</>
)
}// Cursor-based — better for social feeds / orders
GET /api/orders?cursor=eyJpZCI6MTAwfQ&limit=20
// Response: { items: [...], nextCursor: "eyJpZCI6MTIwfQ", hasMore: true }164. React.memo
Theory
React.memo wraps a component and skips re-render if props haven't changed (shallow compare). Use when a component re-renders often with the same props — especially expensive child components.
Pros & Cons
| Pros | Cons |
|---|---|
| Skips unnecessary renders | Shallow compare misses deep object changes |
| Pairs with useCallback | Overuse adds comparison overhead |
Real Example
const ProductRow = React.memo(function ProductRow({ product, onSelect }) {
return (
<tr onClick={() => onSelect(product.id)}>
<td>{product.name}</td>
<td>₹{product.price}</td>
</tr>
)
})165. Suspense (React)
Theory
Suspense lets components wait for something (lazy-loaded code, async data)
before rendering, showing a fallback UI in the meantime. It enables declarative loading states
instead of manual isLoading flags.
Current uses:
React.lazy— code splitting fallback- React 19+ / frameworks — async data fetching with Suspense
- Streaming SSR — send HTML progressively
Pros & Cons
| Pros | Cons |
|---|---|
| Declarative loading states | Data fetching Suspense needs framework support |
| Enables streaming SSR | Error handling needs Error Boundaries alongside |
| Composable fallbacks at any level | Learning curve |
Real-Life Example
import { Suspense, lazy } from 'react'
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
function App() {
return (
<Suspense fallback={<FullPageSpinner />}>
<Routes>
<Route
path="/dashboard"
element={
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
}
/>
<Route
path="/settings"
element={
<Suspense fallback={<SettingsSkeleton />}>
<Settings />
</Suspense>
}
/>
</Routes>
</Suspense>
)
}
// Nested Suspense — granular loading
function Page() {
return (
<div>
<Header /> {/* renders immediately */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar /> {/* lazy */}
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent /> {/* lazy + async data */}
</Suspense>
</div>
)
}166. Throttle promises by batching
Theory
Batching promises groups multiple concurrent requests into controlled batches to avoid overwhelming the server or hitting rate limits. Instead of firing 1000 API calls at once, you process them in chunks of N with optional delay between batches.
This is essential for bulk imports, mass notifications, or syncing large datasets.
Pros & Cons
| Pros | Cons |
|---|---|
| Prevents server overload and 429 errors | Slower total completion time |
| Predictable resource usage | More complex code than fire-and-forget |
| Respects API rate limits | Batch size tuning required per API |
Real-Life Example
async function batchPromises(tasks, batchSize = 5, delayMs = 200) {
const results = []
for (let i = 0; i < tasks.length; i += batchSize) {
const batch = tasks.slice(i, i + batchSize)
const batchResults = await Promise.all(batch.map((task) => task()))
results.push(...batchResults)
if (i + batchSize < tasks.length) {
await new Promise((r) => setTimeout(r, delayMs))
}
}
return results
}
// Real-life: Send push notifications to 10,000 users in batches
async function notifyUsers(userIds, message) {
const tasks = userIds.map(
(id) => () =>
fetch('/api/notify', {
method: 'POST',
body: JSON.stringify({ userId: id, message }),
}),
)
return batchPromises(tasks, 50, 100) // 50 at a time, 100ms between batches
}167. Throttling implementation
Theory
Throttling ensures a function executes at most once per time window, regardless of how many times the event fires. The first call runs immediately; subsequent calls within the window are ignored until the window expires.
Classic use: scroll handlers, mouse-move tracking, rate-limiting button clicks.
Pros & Cons
| Pros | Cons |
|---|---|
| Guarantees regular execution during continuous events | May miss the final event (unless trailing edge added) |
| Protects performance on high-frequency events | Less precise than debounce for "wait until done" |
| Immediate first response | Can drop important intermediate values |
Real-Life Example
function throttle(fn, limit) {
let inThrottle = false
return function (...args) {
if (!inThrottle) {
fn.apply(this, args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
// Real-life: Track scroll depth for analytics (max once per 200ms)
const trackScrollDepth = throttle(() => {
const depth = Math.round((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100)
analytics.track('scroll_depth', { depth })
}, 200)
window.addEventListener('scroll', trackScrollDepth)168. Use React.lazy() and code splitting for better performance
Theory
Code splitting breaks your JavaScript bundle into smaller chunks loaded on demand. React.lazy() dynamically imports a component, and <Suspense> shows a fallback while the chunk loads.
Split by route first (biggest impact), then by heavy components (charts, editors, maps).
Pros & Cons
| Code splitting | Single bundle |
|---|---|
| ✅ Faster initial page load | ❌ User downloads code for pages they never visit |
| ✅ Better Core Web Vitals (LCP, TTI) | ❌ Longer time-to-interactive on slow networks |
| ✅ Parallel chunk loading | — |
| ❌ Brief loading flash without good fallback | — |
Real-Life Example
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
// Route-level splitting — each page is a separate chunk
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Analytics = lazy(() => import('./pages/Analytics')) // heavy chart library
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
// Component-level splitting — load heavy widget only when needed
const RichTextEditor = lazy(() => import('./RichTextEditor'))
function BlogEditor() {
const [showEditor, setShowEditor] = useState(false)
return (
<>
<button onClick={() => setShowEditor(true)}>Write Post</button>
{showEditor && (
<Suspense fallback={<Spinner />}>
<RichTextEditor />
</Suspense>
)}
</>
)
}
// Prefetch on hover for instant navigation
;<Link to="/analytics" onMouseEnter={() => import('./pages/Analytics')}>
Analytics
</Link>169. Web Vitals & Production Monitoring
Theory
You can't optimize what you don't measure. Production monitoring connects real user metrics to code changes.
Tools:
- web-vitals library → send to analytics
- Sentry — errors + performance
- Datadog RUM — Real User Monitoring
- Lighthouse CI — PR-level regression checks
- Chrome UX Report (CrUX) — field data from Google
Instrument Web Vitals
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals'
function sendToAnalytics({ name, value, id, rating }) {
// Google Analytics 4
gtag('event', name, {
event_category: 'Web Vitals',
event_label: id,
value: Math.round(name === 'CLS' ? value * 1000 : value),
metric_rating: rating, // "good" | "needs-improvement" | "poor"
non_interaction: true,
})
// Or custom endpoint
navigator.sendBeacon('/api/vitals', JSON.stringify({ name, value, page: location.pathname }))
}
onCLS(sendToAnalytics)
onINP(sendToAnalytics)
onLCP(sendToAnalytics)
onFCP(sendToAnalytics)
onTTFB(sendToAnalytics)Sentry Performance
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
tracesSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
// Custom transaction for slow flows
function CheckoutPage() {
useEffect(() => {
const transaction = Sentry.startInactiveSpan({ name: 'checkout-load' })
loadCheckoutData().finally(() => transaction.end())
}, [])
}Lighthouse CI in PRs
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
urls: |
https://staging.example.com/
https://staging.example.com/products
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true// lighthouse-budget.json
[
{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 },
{ "metric": "interactive", "budget": 3500 }
]
}
]Alerting Strategy
| Metric | Alert when | Action |
|---|---|---|
| LCP p75 | > 3s for 1 hour | Check deploy, CDN, image regression |
| CLS p75 | > 0.15 | Find layout shift source in RUM |
| INP p75 | > 300ms | Profile long tasks, check new JS bundle |
| Error rate | > 1% spike | Rollback, check Sentry |
170. What are memory leaks in React and how to detect them?
Theory
A memory leak occurs when memory that is no longer needed is not released. In React, common causes are: uncleared timers/subscriptions, detached DOM nodes held in closures, growing caches, and event listeners not removed on unmount.
Pros & Cons of detection tools
| Tool | Pros | Cons |
|---|---|---|
| Chrome DevTools Memory tab | Heap snapshots, comparison | Manual, requires reproduction |
| React DevTools Profiler | Component render counts | Doesn't show JS heap directly |
why-did-you-render | Catches unnecessary re-renders | Dev-only, noisy |
| Sentry / monitoring | Production leak patterns | Indirect detection |
Real-Life Example
// ❌ Memory leak — subscription not cleaned up
function LiveOrderTracker({ orderId }) {
const [status, setStatus] = useState('pending')
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/orders/${orderId}`)
ws.onmessage = (e) => setStatus(JSON.parse(e.data).status)
// Missing cleanup! WebSocket stays open after unmount
}, [orderId])
return <div>Status: {status}</div>
}
// ✅ Fixed — cleanup on unmount
function LiveOrderTracker({ orderId }) {
const [status, setStatus] = useState('pending')
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/orders/${orderId}`)
ws.onmessage = (e) => setStatus(JSON.parse(e.data).status)
return () => ws.close() // cleanup
}, [orderId])
return <div>Status: {status}</div>
}Detection steps:
- Chrome DevTools → Memory → Take heap snapshot
- Perform the action (navigate away from component)
- Force GC → Take another snapshot
- Compare — look for detached DOM trees or growing object counts
171. What is Debouncing and Throttling?
Theory
Both limit how often a function executes during high-frequency events, but with different strategies:
| Debounce | Throttle | |
|---|---|---|
| Behavior | Wait until activity stops, then run once | Run at most once per interval during activity |
| Analogy | Elevator waits for everyone to board | Train departs every 10 minutes regardless |
| Best for | Search input, resize-end, auto-save | Scroll, mousemove, button spam prevention |
Pros & Cons
| Debounce | Throttle |
|---|---|
| ✅ Minimizes total executions | ✅ Guarantees regular updates during continuous events |
| ✅ Great for API cost reduction | ✅ Better for scroll analytics |
| ❌ Delays until pause ends | ❌ May miss final state without trailing edge |
| ❌ Can feel sluggish if delay too long | ❌ More executions than debounce |
Real-Life Example
// Debounce — search restaurants after user stops typing
function debounce(fn, delay) {
let timerId
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
const searchRestaurants = debounce(async (query) => {
const { data } = await axios.get(`/api/restaurants?q=${query}`)
setResults(data)
}, 300)
// Throttle — track scroll position max once per 200ms
function throttle(fn, limit) {
let inThrottle = false
return function (...args) {
if (!inThrottle) {
fn.apply(this, args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
const trackScroll = throttle(() => {
analytics.track('scroll_depth', { y: window.scrollY })
}, 200)// React usage
function SearchBar() {
const [query, setQuery] = useState('')
const debouncedSearch = useMemo(() => debounce((q) => fetchResults(q), 300), [])
useEffect(() => {
if (query) debouncedSearch(query)
return () => debouncedSearch.cancel?.() // cleanup if using lodash
}, [query])
}172. What is the difference between debounce and throttle?
Theory
Both limit how often a function runs, but with opposite philosophies:
- Debounce: "Wait until they stop, then run once."
- Throttle: "Run at most once per interval, even if they keep going."
Pros & Cons
| Debounce | Throttle | |
|---|---|---|
| Best for | Search input, resize-end, auto-save | Scroll, mousemove, API rate limiting |
| Pros | Minimizes total executions | Guarantees regular updates during continuous events |
| Cons | Delays until pause ends | May miss final state without trailing option |
Real-Life Example
User typing "pizza" in search box:
Debounce (300ms): p-i-z-z-a → wait 300ms → 1 API call
Throttle (300ms): p → call, (300ms) z → call, (300ms) a → call
User scrolling a feed:
Debounce: No analytics until scroll STOPS (bad — miss scroll data)
Throttle (200ms): Analytics fires every 200ms during scroll (good)173. What techniques would you use to improve Core Web Vitals?
Theory — The three Core Web Vitals
| Metric | Measures | Good threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | Loading performance | ≤ 2.5s |
| INP (Interaction to Next Paint) | Responsiveness (replaced FID) | ≤ 200ms |
| CLS (Cumulative Layout Shift) | Visual stability | ≤ 0.1 |
LCP optimizations
<!-- Preload hero image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- Modern formats + responsive images -->
<img
src="/restaurant.webp"
srcset="/restaurant-400.webp 400w, /restaurant-800.webp 800w"
sizes="(max-width: 600px) 100vw, 400px"
width="400"
height="300"
alt="Restaurant"
loading="eager"
fetchpriority="high" />- Inline critical CSS; defer non-critical CSS
- Use CDN + HTTP/2 or HTTP/3
- Server-side render or stream above-the-fold content (RSC/SSR)
- Reduce TTFB (edge caching, efficient APIs)
INP optimizations
// Break long tasks
function processChunk(items, index = 0) {
const CHUNK = 50
const end = Math.min(index + CHUNK, items.length)
for (let i = index; i < end; i++) {
processItem(items[i])
}
if (end < items.length) {
requestIdleCallback(() => processChunk(items, end))
}
}- Minimize main-thread JS (code splitting, tree shaking)
- Use
useTransitionfor non-urgent UI updates - Debounce expensive handlers; use Web Workers for heavy computation
- Avoid layout thrashing (batch DOM reads/writes)
CLS optimizations
// Always reserve space for images/ads
<img width={400} height={300} src="..." alt="..." />
// Use aspect-ratio in CSS
.card-image {
aspect-ratio: 4 / 3;
width: 100%;
}
// Don't inject content above existing content
// Prefer transform animations over layout-changing properties- Set explicit dimensions on media and embeds
- Use
font-display: swapwith matched fallback metrics (size-adjust) - Skeleton loaders with fixed height
Measurement
import { onLCP, onINP, onCLS } from 'web-vitals'
onLCP(console.log)
onINP(console.log)
onCLS(console.log)Ship RUM data to Datadog, Sentry, or Google Analytics 4. Set performance budgets in CI (Lighthouse CI, bundle analyzer).
174. Write a debounce function (cancellable)
Theory
Debouncing delays execution until after a pause in calls. A cancellable
debounce also exposes .cancel() to abort pending execution — essential for cleanup on unmount, route change, or
explicit user action.
a fintech app asks you to write this from scratch — no lodash.
Pros & Cons
| Cancellable debounce | Basic debounce |
|---|---|
| ✅ Cleanup on component unmount | ❌ Pending call may fire after unmount |
| ✅ Cancel on route change | |
| ✅ Abort stale API responses |
Real-Life Example
function debounce(fn, delay) {
let timerId
function debounced(...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
debounced.cancel = function () {
clearTimeout(timerId)
timerId = null
}
debounced.flush = function (...args) {
clearTimeout(timerId)
fn.apply(this, args)
}
return debounced
}
// Usage
const search = debounce((query) => {
fetch(`/api/search?q=${query}`).then(renderResults)
}, 300)
searchInput.addEventListener('input', (e) => search(e.target.value))
// Cancel on navigate away
router.beforeEach(() => search.cancel())
// Flush on form submit (don't wait for debounce)
form.addEventListener('submit', () => search.flush(input.value))// TypeScript version — a fintech app-grade
function debounce<T extends (...args: Parameters<T>) => void>(
fn: T,
delay: number
): T & { cancel: () => void; flush: (...args: Parameters<T>) => void } {
let timerId: ReturnType<typeof setTimeout> | null = null;
const debounced = function (this: unknown, ...args: Parameters<T>) {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
timerId = null;
fn.apply(this, args);
}, delay);
} as T & { cancel: () => void; flush: (...args: Parameters<T>) => void };
debounced.cancel = () => {
if (timerId) clearTimeout(timerId);
timerId = null;
};
debounced.flush = function (this: unknown, ...args: Parameters<T>) {
debounced.cancel();
fn.apply(this, args);
};
return debounced;
}175. Your Next.js application has a large JavaScript bundle size affecting performance. How would you reduce it?
Description
Large bundles increase download, parse, and execute time — hurting LCP and Interaction to Next Paint (INP). Next.js helps by default with Server Components, but client islands and dependencies can still bloat the bundle.
Theory
| Technique | Impact |
|---|---|
| Server Components | Keep heavy logic/libraries on server — zero client JS |
next/dynamic | Lazy-load client components |
optimizePackageImports | Tree-shake barrel files (lodash, MUI, etc.) |
| Analyze bundle | @next/bundle-analyzer |
| Replace heavy libs | date-fns over moment, recharts only where needed |
next/image | Avoid shipping image processing to client |
| Route-based splitting | Automatic per-route chunks in App Router |
Pros & Cons
| Aggressive code splitting | Single large bundle |
|---|---|
| ✅ Faster initial load | ✅ Simpler imports |
| ✅ Better mobile performance | ❌ Poor Core Web Vitals |
| ❌ More loading states to design | ❌ Higher bounce rate |
Real Example
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
experimental: {
optimizePackageImports: ['lodash-es', '@mui/material', 'lucide-react'],
},
})// ❌ Heavy chart in main bundle
import { LineChart } from 'recharts'
// ✅ Lazy load only when tab opens
import dynamic from 'next/dynamic'
const LineChart = dynamic(() => import('recharts').then((mod) => mod.LineChart), {
ssr: false,
loading: () => <ChartSkeleton />,
})// Prefer Server Component for data — no client JS for fetch logic
// app/reports/page.tsx
import { HeavyDataTable } from './heavy-data-table' // Server Component
export default async function ReportsPage() {
const data = await getReportData() // runs on server only
return <HeavyDataTable rows={data} />
}ANALYZE=true npm run build- First-load JS shared chunk: < 100–150 KB gzipped where possible
- Audit
importfrom package roots — use direct paths oroptimizePackageImports
Architecture & Patterns
176. Error Boundaries & Design Patterns
Theory
Error Boundaries
Class components (or react-error-boundary library) that catch render errors in children via componentDidCatch / getDerivedStateFromError. They do not catch event handlers, async, or SSR errors.
Design Patterns
| Pattern | Idea | When |
|---|---|---|
| HOC | Function wrapping component to inject props | Cross-cutting: auth, logging |
| Render Props | Component accepts function as child | Shared behavior, flexible UI |
| Compound Components | Related components share implicit state | Tabs, Accordion, Select |
| Container/Presentational | Split logic vs UI | Testing, reuse |
Modern trend: custom hooks replace HOCs and render props for most cases.
Real Example
// Error Boundary (react-error-boundary)
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => queryClient.resetQueries()}>
<Dashboard />
</ErrorBoundary>
// HOC
function withAuth(WrappedComponent) {
return function AuthComponent(props) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
// Render Props
<DataFetcher url="/api/users">
{({ data, loading }) => loading ? <Spinner /> : <UserList users={data} />}
</DataFetcher>
// Compound Components
<Tabs defaultIndex={0}>
<Tabs.List>
<Tabs.Tab>Overview</Tabs.Tab>
<Tabs.Tab>Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel><Overview /></Tabs.Panel>
<Tabs.Panel><Settings /></Tabs.Panel>
</Tabs.Panels>
</Tabs>
// Custom Hook — modern replacement
function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be inside AuthProvider");
return ctx;
}| Pattern | Questions | Core idea |
|---|---|---|
| Fundamentals & JSX | 1–5 | Library, JSX rules, Fragments, events |
| Components & State | 6–10 | Props down, state local, one-way flow |
| Data Flow & Forms | 11–14 | Lift state, controlled inputs |
| Hooks | 15–23 | Stateful logic, rules, custom hooks |
| Virtual DOM & Rendering | 24–28 | Diff, keys, CSR/SSR, Fiber |
| Context | 29–31 | Share without drilling |
| Router | 32–35 | SPA navigation, guards |
| API Integration | 36–39 | fetch, Axios, TanStack Query |
| Performance | 40–44 | memo, lazy, Suspense, transitions |
| Lifecycle | 45–46 | Class methods ↔ useEffect |
| Redux | 47–49 | Global state, RTK, vs Context |
| Errors & Patterns | 50 | Boundaries, HOC, compound, hooks |
How to Study Pattern-Wise
- One pattern per session — finish Hooks (15–23) before jumping to Redux
- Connect patterns — after Context (29), revisit Prop Drilling (31) and Lifting State (11)
- Say the one-liner out loud — if you can say it, you can answer in an interview
- Code the example — type it, don't just read it
- Expect follow-ups — "When would you NOT use useMemo?" → when cost < overhead
50 questions. 12 patterns. Master the patterns and random interview questions become predictable.
177. Follow a proper folder structure
Theory
A good folder structure groups code by feature (domain) rather than by file type. Colocate components, hooks, API calls, types, and tests for each feature. Shared code lives in a shared/ or common/ directory.
Pros & Cons
| Feature-based structure | Type-based structure |
|---|---|
| ✅ Everything for a feature in one place | ❌ Jump between 5 folders to change one feature |
| ✅ Easy to delete/move features | ❌ Grows unwieldy at scale |
| ✅ Team ownership per feature | ❌ Merge conflicts across teams |
| ✅ Scales with micro-frontends | — |
Real-Life Example
src/
├── app/ # App shell
│ ├── App.tsx
│ ├── router.tsx
│ └── providers.tsx # QueryClient, Theme, Auth providers
│
├── features/ # Feature modules (preferred)
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ └── ProtectedRoute.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.ts
│ │ ├── api/
│ │ │ └── authService.ts
│ │ └── types.ts
│ │
│ ├── restaurants/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── types.ts
│ │
│ └── cart/
│ ├── components/
│ ├── hooks/
│ ├── store/ # cartSlice if using Redux
│ └── types.ts
│
├── shared/ # Cross-feature reusables
│ ├── components/ # Button, Input, Modal, Spinner
│ ├── hooks/ # useDebounce, useMediaQuery
│ ├── utils/ # formatPrice, dateHelpers
│ └── types/ # Global types
│
├── assets/ # Images, fonts, icons
└── styles/ # Global CSS, tokens, variablesRules:
- Import from features → shared ✅
- Import from feature A → feature B ❌ (creates coupling)
- Each feature exports a public API via
index.ts
178. How would you design a scalable frontend architecture for a large-scale application?
Theory — Layered architecture
flowchart TB
subgraph Presentation
Pages[Pages / Routes]
Components[UI Components]
end
subgraph Application
Hooks[Custom Hooks]
State[State Management]
Services[API Services]
end
subgraph Infrastructure
Router[Router]
Auth[Auth Layer]
Analytics[Observability]
Cache[Cache Layer]
end
Pages --> Components
Components --> Hooks
Hooks --> State
Hooks --> Services
Services --> Cache
Pages --> Router
Services --> AuthCore principles
| Principle | Implementation |
|---|---|
| Modular monorepo | apps/web, packages/ui, packages/api-client, packages/utils |
| Feature-based folders | features/orders/, features/restaurants/ — colocate components, hooks, API, tests |
| Separation of concerns | UI components don't call fetch directly; go through service layer |
| Design system | Shared tokens, primitives, accessibility baked in |
| Lazy loading | Route-level code splitting with React.lazy + Suspense |
| API contract | OpenAPI/GraphQL schema, generated TypeScript types |
| Error boundaries | Per-route or per-feature isolation |
| Observability | Sentry, Web Vitals RUM, structured logging |
Folder structure example
src/
├── app/ # App shell, providers, router
├── features/
│ ├── restaurants/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── types.ts
│ └── orders/
├── shared/
│ ├── components/ # Design system primitives
│ ├── hooks/
│ └── utils/
└── services/
├── http-client.ts
└── auth.tsInterview talking points
- Start with business domains, not technical layers only
- Enforce boundaries with ESLint module boundaries or Nx tags
- CI/CD: preview deploys per PR, visual regression, bundle size budgets
- Progressive rollout: feature flags (LaunchDarkly, Unleash)
179. Micro-frontend Architecture
Theory
Micro-frontends split a large frontend into independently deployable applications owned by different teams. Each handles a business domain (search, checkout, profile).
Integration: Module Federation, iframes, Web Components, single-spa.
Pros & Cons
| Pros | Cons |
|---|---|
| Team autonomy — independent deploys | Inconsistent UX across teams |
| Technology flexibility | Shared state / routing complexity |
| Incremental migration | Performance overhead (multiple bundles) |
| Fault isolation | Dependency duplication without federation |
Real-Life Example
Shell App (routing, auth, layout)
├── Search MFE (Team A — React)
├── Checkout MFE (Team B — React)
├── Profile MFE (Team C — Vue)
└── Analytics MFE (Team D — React)
Integration via Module Federation:// Remote — checkout team exposes widget
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: { './CheckoutWidget': './src/CheckoutWidget' },
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
// Host — shell app consumes
const CheckoutWidget = lazy(() => import('checkout/CheckoutWidget'))180. React Architecture
Theory
Production React architecture organizes code by feature, separates concerns, and scales with team size.
Layers
┌─────────────────────────────────────┐
│ Presentation — Components, Pages │
├─────────────────────────────────────┤
│ Application — Hooks, State, Router │
├─────────────────────────────────────┤
│ Domain — Services, API, Business Logic│
├─────────────────────────────────────┤
│ Infrastructure — HTTP, Auth, Config │
└─────────────────────────────────────┘Real Example
src/
├── app/ # Router, providers, shell
│ ├── App.tsx
│ ├── router.tsx
│ └── providers.tsx # QueryClient, Redux, Theme
├── features/
│ ├── auth/
│ │ ├── components/ # LoginForm, ProtectedRoute
│ │ ├── hooks/ # useAuth
│ │ ├── api/ # authService.ts
│ │ └── types.ts
│ ├── dashboard/
│ └── transactions/
├── shared/
│ ├── components/ # Button, Modal, StarRating
│ ├── hooks/
│ └── utils/
└── services/
└── httpClient.ts # Axios instance + interceptorsData flow: Component → custom hook → service/API → TanStack Query cache → UI
181. React vs Vue — Architecture & Performance Tradeoffs
Theory
| Area | React | Vue |
|---|---|---|
| UI model | JSX — JS everywhere | Templates + optional JSX |
| Reactivity | Immutable state + rerender | Proxy-based tracking (Vue 3) |
| Update granularity | Component-level rerender (memo to narrow) | Fine-grained by default |
| State | useState, Context, Redux, Zustand | ref/reactive, Pinia |
| SSR framework | Next.js | Nuxt |
| Learning curve | Hooks + ecosystem choices | Gentler defaults, more built-in |
Pros & Cons
| Choose React | Choose Vue |
|---|---|
| ✅ Largest ecosystem, hiring pool | ✅ Faster onboarding, less boilerplate |
| ✅ React Native for mobile | ✅ Built-in transitions, directives |
| ✅ Flexibility (library not framework) | ✅ SFC colocation (style/template/script) |
| ❌ More decisions (router, state, data) | ❌ Smaller enterprise footprint in some regions |
Performance tradeoffs
| React | Vue |
|---|---|
| Re-renders whole component on state change unless memoized | Compiler + proxies track deps — skip unaffected components |
| Virtual DOM diff every update | Patch only tracked dependencies |
useMemo / React.memo manual opt-in | computed auto-caches |
| Fiber scheduling for concurrent features | Vue 3 scheduler + async component boundaries |
Real Example — Same counter
// React — explicit state triggers component rerender
function ReactCounter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}<!-- Vue — ref tracked automatically -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>182. Scalable, Maintainable Architecture
Theory
Scalable frontend architecture optimizes for team velocity and change isolation — not premature abstraction.
Principles:
- Feature-based folders — colocate everything a feature needs
- Unidirectional data flow — predictable state
- API layer separation — components never call fetch directly
- Design system — shared UI primitives
- Barrel exports sparingly — avoid circular deps and bundle bloat
- Module boundaries — features don't import from sibling features
Folder Structure
src/
├── app/ # Shell, router, providers
│ ├── App.tsx
│ ├── router.tsx
│ └── providers.tsx
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ ├── types.ts
│ │ └── index.ts # public API only
│ ├── cart/
│ ├── catalog/
│ └── checkout/
├── shared/
│ ├── components/ # Button, Modal, DataTable
│ ├── hooks/
│ ├── utils/
│ └── types/
├── services/
│ └── httpClient.ts # Axios instance, interceptors
└── design-system/
├── tokens/
└── primitives/API Layer
// services/httpClient.ts
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
})
api.interceptors.request.use((config) => {
const token = getAccessToken()
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
api.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 401) await refreshTokenAndRetry(error.config)
return Promise.reject(error)
},
)
// features/catalog/api/products.ts
export const productApi = {
getAll: (filters: ProductFilters) => api.get<Product[]>('/products', { params: filters }).then((r) => r.data),
getById: (id: string) => api.get<Product>(`/products/${id}`).then((r) => r.data),
}Import Boundaries (ESLint)
// eslint.config.js
{
rules: {
"no-restricted-imports": ["error", {
patterns: [
{
group: ["../features/*/*"],
message: "Import from feature public API (index.ts), not internals",
},
{
group: ["features/cart/**"],
from: "features/checkout",
message: "Checkout cannot import cart internals directly",
},
],
}],
},
}Composition Over Configuration
// ❌ God component with 20 props
<DataTable sortable filterable exportable paginated virtualized ... />
// ✅ Composable
<DataTable data={products}>
<DataTable.Toolbar>
<SearchFilter />
<ExportButton />
</DataTable.Toolbar>
<DataTable.Columns columns={productColumns} />
<DataTable.Pagination pageSize={20} />
</DataTable>183. Use Error Boundaries to handle unexpected UI crashes
Theory
Error Boundaries are React components that catch JavaScript errors in their child tree during rendering and display a fallback UI instead of crashing the entire app. They do not catch errors in event handlers, async code, or SSR.
Use them at route level, around risky third-party widgets, and around data-heavy sections.
Pros & Cons
| Error Boundaries | No error handling |
|---|---|
| ✅ App stays usable when one section fails | ❌ White screen of death |
| ✅ Log errors to Sentry/Datadog | ❌ User has no recovery path |
| ✅ Per-feature isolation | ❌ One bug kills entire app |
| ❌ Class components only (for now) | — |
Real-Life Example
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
logToSentry(error, info.componentStack)
}
render() {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>Try again</button>
</div>
)
)
}
return this.props.children
}
}
// Wrap each route independently
function App() {
return (
<Routes>
<Route
path="/orders"
element={
<ErrorBoundary fallback={<OrdersErrorPage />}>
<OrdersPage />
</ErrorBoundary>
}
/>
<Route
path="/profile"
element={
<ErrorBoundary fallback={<ProfileErrorPage />}>
<ProfilePage />
</ErrorBoundary>
}
/>
</Routes>
)
}184. What are Error Boundaries in React?
Theory
Error Boundaries are React components that catch JavaScript errors in their child component tree during rendering, in lifecycle methods, and in constructors. They display a fallback UI instead of crashing the entire app.
They do NOT catch: event handler errors, async errors, SSR errors, or errors in the boundary itself.
Pros & Cons
| Pros | Cons |
|---|---|
| Isolates failures — app stays usable | Class components only (no hook equivalent yet) |
| Better UX with fallback UI | Doesn't catch event handler or async errors |
| Can log errors to monitoring services | Must be placed intentionally in tree |
Real-Life Example
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null }
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
// Send to Sentry / Datadog
logErrorToService(error, errorInfo.componentStack)
}
render() {
if (this.state.hasError) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>Try again</button>
</div>
)
}
return this.props.children
}
}
// Real-life: Wrap each route independently
function App() {
return (
<ErrorBoundary>
<Routes>
<Route
path="/orders"
element={
<ErrorBoundary>
<OrdersPage />
</ErrorBoundary>
}
/>
<Route
path="/profile"
element={
<ErrorBoundary>
<ProfilePage />
</ErrorBoundary>
}
/>
</Routes>
</ErrorBoundary>
)
}185. What are portals?
Answer
Portals allow rendering a child component into a different part of the DOM tree outside the parent component hierarchy.
ReactDOM.createPortal(child, document.getElementById('modal-root'))186. What are React Portals? How are modals mounted using them?
Theory
Portals let you render children into a
DOM node outside the parent component's DOM hierarchy while keeping the React tree (context,
events) connected. ReactDOM.createPortal(child, container) is the API.
Modals use portals to render at document.body level, escaping overflow: hidden, z-index stacking contexts, and parent CSS transforms.
Pros & Cons
| Pros | Cons |
|---|---|
| Escapes parent CSS constraints | Focus management must be handled manually |
| Proper z-index layering for overlays | Screen readers need aria-modal, focus trap |
| Event bubbling still works through React tree | Portal target must exist in DOM |
Real-Life Example
import { createPortal } from 'react-dom'
import { useEffect, useRef } from 'react'
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null)
useEffect(() => {
if (!isOpen) return
const handleEsc = (e) => e.key === 'Escape' && onClose()
document.addEventListener('keydown', handleEsc)
document.body.style.overflow = 'hidden'
modalRef.current?.focus()
return () => {
document.removeEventListener('keydown', handleEsc)
document.body.style.overflow = ''
}
}, [isOpen, onClose])
if (!isOpen) return null
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div
className="modal-content"
role="dialog"
aria-modal="true"
ref={modalRef}
tabIndex={-1}
onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.getElementById('modal-root'), // or document.body
)
}
// Usage in a deeply nested component
function ProductPage() {
const [showCart, setShowCart] = useState(false)
return (
<div style={{ overflow: 'hidden' }}>
<button onClick={() => setShowCart(true)}>View Cart</button>
<Modal isOpen={showCart} onClose={() => setShowCart(false)}>
<CartItems />
</Modal>
</div>
)
}187. What is an Error Boundary?
Theory
An Error Boundary is a React class component that catches JavaScript errors in its child component tree during rendering, in lifecycle methods, and in constructors. It displays a fallback UI instead of crashing the entire application.
Does NOT catch:
- Event handler errors (use try/catch)
- Async errors (use try/catch or
.catch()) - Errors in the boundary itself
- SSR errors
Pros & Cons
| Pros | Cons |
|---|---|
| ✅ Isolates failures — app stays usable | ❌ Class components only (no hook yet) |
| ✅ Better UX with fallback UI | ❌ Doesn't catch async/event errors |
| ✅ Integrates with error monitoring (Sentry) | Must be placed intentionally in tree |
Real-Life Example
class ErrorBoundary extends React.Component<
{
children: React.ReactNode
fallback?: React.ReactNode
onError?: (e: Error) => void
},
{ hasError: boolean }
> {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
this.props.onError?.(error)
logToSentry(error, info.componentStack)
}
render() {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div role="alert">
<p>Something went wrong in this section.</p>
<button onClick={() => this.setState({ hasError: false })}>Retry</button>
</div>
)
)
}
return this.props.children
}
}
// Wrap independent page sections
function DashboardPage() {
return (
<div className="dashboard">
<ErrorBoundary fallback={<SectionError name="Announcements" />}>
<AnnouncementsSection />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError name="Team" />}>
<TeamSection />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError name="Projects" />}>
<ProjectsSection />
</ErrorBoundary>
</div>
)
}188. What is Module Federation?
Theory
Module Federation (Webpack 5) allows multiple independently built and deployed applications to share code at runtime. One app can dynamically import components/modules from another without rebuilding.
A host app consumes remote modules. A remote app exposes modules. Shared dependencies (React) are deduplicated.
Pros & Cons
| Pros | Cons |
|---|---|
| Independent deploys per team | Runtime coupling — remote down = host breaks |
| Share components without npm packages | Complex webpack configuration |
| Runtime code sharing | Version mismatch risks for shared deps |
Real-Life Example
// Remote app (webpack.config.js) — "checkout" team
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: {
'./CheckoutWidget': './src/CheckoutWidget',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
// Host app (webpack.config.js) — "main" team
new ModuleFederationPlugin({
name: 'host',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
// Host consumes remote component
const CheckoutWidget = React.lazy(() => import('checkout/CheckoutWidget'))JavaScript Core
189. Async/Await
Theory
async/await is syntactic sugar over Promises. An async function always returns a Promise. await pauses execution until the Promise settles, then resumes with the resolved value.
Code before the first await runs synchronously. Code after await runs as a microtask.
Pros & Cons
| async/await | .then() chains |
|---|---|
| ✅ Reads like synchronous code | ✅ More explicit control flow |
| ✅ try/catch for errors | ✅ Easier to parallelize visually with Promise.all |
| ✅ Easier debugging (stack traces) | |
| ❌ Easy to accidentally run sequentially | |
| ❌ Sequential await is slower than parallel |
Real-Life Example
// Sequential — slow (each waits for previous)
async function loadCheckoutSlow(userId) {
const user = await fetchUser(userId)
const cart = await fetchCart(userId)
const addresses = await fetchAddresses(userId)
return { user, cart, addresses }
}
// Parallel — fast
async function loadCheckout(userId) {
const [user, cart, addresses] = await Promise.all([fetchUser(userId), fetchCart(userId), fetchAddresses(userId)])
return { user, cart, addresses }
}
// Error handling
async function placeOrder(orderData) {
try {
const response = await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(orderData),
})
if (!response.ok) throw new Error(`Order failed: ${response.status}`)
return await response.json()
} catch (error) {
logToSentry(error)
showToast('Order failed. Please try again.')
throw error
}
}
// Execution order demo
async function demo() {
console.log('A')
await Promise.resolve()
console.log('B') // microtask — after sync code
}
console.log('C')
demo()
console.log('D')
// C → A → D → B190. async/await and Promises
Theory
A Promise represents a future value: pending → fulfilled or rejected.
async/await is syntactic sugar — async functions return Promises; await pauses until Promise
settles.
Code before first await runs synchronously. After await resumes as microtask.
Real Example
// Promise chain
function fetchUser(id) {
return fetch(`/api/users/${id}`).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
})
}
// async/await — cleaner error handling
async function loadDashboard(userId) {
try {
const [user, orders, rewards] = await Promise.all([fetchUser(userId), fetchOrders(userId), fetchRewards(userId)])
return { user, orders, rewards }
} catch (err) {
logError(err)
throw err
}
}
// Execution order
async function demo() {
console.log('A')
await Promise.resolve()
console.log('B')
}
console.log('C')
demo()
console.log('D')
// C → A → D → B191. Call, Apply, Bind — Difference + Polyfill
Theory
All three control this context of a function:
| Method | Invokes? | Arguments | Returns |
|---|---|---|---|
call | Immediately | Comma-separated | Function result |
apply | Immediately | Array | Function result |
bind | No | Comma-separated (+ partial) | New function |
Pros & Cons
| call/apply | bind |
|---|---|
| One-time invocation | Reusable bound function |
| apply when args in array | Partial application |
| Cannot reuse easily | Slight memory per bound fn |
Polyfill Implementations
// Polyfill: Function.prototype.call
Function.prototype.myCall = function (thisArg, ...args) {
const fn = this
if (typeof fn !== 'function') throw new TypeError('Not a function')
const context = thisArg ?? globalThis
const uniqueKey = Symbol('fn')
context[uniqueKey] = fn
const result = context[uniqueKey](...args)
delete context[uniqueKey]
return result
}
// Polyfill: Function.prototype.apply
Function.prototype.myApply = function (thisArg, argsArray) {
const fn = this
if (typeof fn !== 'function') throw new TypeError('Not a function')
const context = thisArg ?? globalThis
const uniqueKey = Symbol('fn')
context[uniqueKey] = fn
const result = argsArray ? context[uniqueKey](...argsArray) : context[uniqueKey]()
delete context[uniqueKey]
return result
}
// Polyfill: Function.prototype.bind
Function.prototype.myBind = function (thisArg, ...boundArgs) {
const fn = this
if (typeof fn !== 'function') throw new TypeError('Not a function')
const boundFn = function (...callArgs) {
const isNew = this instanceof boundFn
return fn.apply(isNew ? this : thisArg, boundArgs.concat(callArgs))
}
if (fn.prototype) {
boundFn.prototype = Object.create(fn.prototype)
}
return boundFn
}Real Example
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const user = { name: 'Amit' }
greet.myCall(user, 'Hello', '!') // "Hello, Amit!"
greet.myApply(user, ['Hi', '.']) // "Hi, Amit."
const sayHello = greet.myBind(user, 'Hey')
sayHello('!!') // "Hey, Amit!!"192. Can call(), apply(), bind() be used with Arrow Functions?
Theory
No. Arrow functions have no own this — they inherit this lexically
from the enclosing scope. Since this is fixed at creation time, call, apply, and bind
cannot change it.
Calling them on arrow functions doesn't throw — but this stays lexical, arguments are still passed.
Real Example
const obj = {
name: 'a fintech app',
regular() {
return this.name
},
arrow: () => this?.name,
}
obj.regular() // "a fintech app"
obj.regular.call({ name: 'Other' }) // "Other"
obj.arrow() // undefined (lexical this)
obj.arrow.call({ name: 'Other' }) // still undefined — this NOT changed
// bind on arrow — creates bound function but this unchanged
const bound = obj.arrow.bind({ name: 'Other' })
bound() // undefined// Practical rule for React class components (legacy)
this.handleClick = this.handleClick.bind(this); // regular fn — works
this.handleClick = () => { ... }; // arrow — this already correct193. Closures
Theory
A closure is a function bundled together with its lexical environment — the scope in which it was created. The inner function retains access to outer variables even after the outer function has finished executing.
Closures enable: data privacy, factory functions, memoization, debouncing, and module patterns.
Pros & Cons
| Pros | Cons |
|---|---|
| Encapsulation / private variables | Memory — outer vars stay alive while closure exists |
| Reusable function factories | Accidental closures in loops (var bug) |
| Powerful callback patterns | Harder to debug deep closure chains |
Real-Life Example
// Private counter — module pattern
function createCart() {
let items = [] // private via closure
return {
add(item) {
items.push(item)
},
remove(id) {
items = items.filter((i) => i.id !== id)
},
getTotal() {
return items.reduce((sum, i) => sum + i.price * i.qty, 0)
},
getCount() {
return items.length
},
}
}
const cart = createCart()
cart.add({ id: 1, name: 'Biryani', price: 299, qty: 1 })
console.log(cart.getTotal()) // 299
// cart.items — undefined (private)
// Debounce — closure holds timer reference
function debounce(fn, delay) {
let timerId
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
const search = debounce((q) => fetch(`/api?q=${q}`), 300)// Memoization
function memoize(fn) {
const cache = new Map()
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)
const result = fn(...args)
cache.set(key, result)
return result
}
}
const expensiveFilter = memoize((products, category) => products.filter((p) => p.category === category))194. Currying — Implement sum(1)(2)(3)
Theory
Currying transforms a function taking multiple arguments into a chain of functions each taking one (or more) arguments.
sum(1)(2)(3) should return 6.
Real Example
// sum(1)(2)(3) → 6
function sum(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
console.log(sum(1)(2)(3)) // 6
// Generic curry — flexible arity
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return (...more) => curried(...args, ...more)
}
}
const add = (a, b, c) => a + b + c
const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6
// Infinite currying sum — sum(1)(2)(3)...(n)
function infiniteSum(a) {
const fn = (b) => infiniteSum(a + b)
fn.valueOf = () => a
fn.toString = () => String(a)
return fn
}
console.log(+infiniteSum(1)(2)(3)) // 6
console.log(+infiniteSum(5)(10)(15)) // 30// Real-life: partial application for API client
const apiCall = curry((method, baseUrl, endpoint) => fetch(`${baseUrl}${endpoint}`, { method }))
const credApi = apiCall('GET')('https://api.a fintech app.club')
credApi('/users/me').then((r) => r.json())195. Currying for Infinite Sum
Theory
Implement sum(10)(20)(30)() → 60 and sum(10)(20)(30)(40)(50)(60)() → 210.
Each call adds a number. Calling with empty parentheses () triggers the final result. This is infinite currying with an empty-call terminator.
Implementation
function sum(a) {
const fn = function (b) {
// Empty call () → return accumulated sum
if (arguments.length === 0) return a
return sum(a + b)
}
// Allow sum(10)(20)(30)() — empty parens call fn with no args
fn.valueOf = () => a
fn.toString = () => String(a)
return fn
}
// Tests
console.log(sum(10)(20)(30)()) // 60
console.log(sum(10)(20)(30)(40)(50)(60)()) // 210
console.log(+sum(5)(15)(25)()) // 45
console.log(sum(1)(2)(3)(4)(5)(6)(7)(8)(9)(10)()) // 55Alternative — explicit empty call detection
function sum(initial) {
let total = initial
function accumulator(value) {
if (value === undefined) {
// Called as () — return result
const result = total
total = 0 // reset for reuse (optional)
return result
}
total += value
return accumulator
}
// First call already set total = initial
return function next(value) {
if (value === undefined) return total
total += value
return next
}
}
// Cleaner version matching interview spec exactly
function sum(a) {
return function inner(b) {
if (b === undefined) return a
return sum(a + b)
}
}
// Usage: must pass undefined or use () which passes undefined
console.log(sum(10)(20)(30)(undefined)) // 60 — less elegant
// Best: empty call detection via argument length
function sum(a) {
const f = (b) => {
if (arguments.length === 0) return a
return sum(a + b)
}
return f
}
console.log(sum(10)(20)(30)()) // 60
console.log(sum(10)(20)(30)(40)(50)(60)()) // 210Walk-through: sum(10)(20)(30)()
sum(10) → f, a=10
(20) → sum(10+20=30) → f, a=30
(30) → sum(30+30=60) → f, a=60
() → arguments.length===0 → return 60 ✅| # | Topic | One-liner |
|---|---|---|
| 1 | call/apply/bind | Invoke now vs bind this; polyfill via Symbol |
| 2 | Flatten | Recursive reduce or stack; concat if array |
| 3 | 5 divs inline | display:inline-block; font-size:0 on parent |
| 4 | Sum no loop | arr.reduce((a,b) => a+b, 0) |
| 5 | Deep vs shallow | structuredClone vs spread; nested shared |
| 6 | Output puzzle | Sync → microtasks → macrotask |
| 7 | First repeating | Set scan left; first char seen twice |
| 8 | Stopwatch | setInterval + elapsed offset; cleanup on stop |
| 9 | Todo list | React.memo + useCallback + UUID keys |
| 10 | Infinite sum | Curried fn; empty () returns accumulated total |
Senior Interview Tips
| Do | Don't |
|---|---|
| Write polyfills from scratch | Say "I'd use lodash" |
| Explain output puzzles step by step | Guess the answer |
| Mention re-render optimization in Todo | Build naive list without memo |
| Handle edge cases (empty string, empty array) | Skip cleanup in stopwatch |
| Clarify first repeating vs non-repeating char | Assume wrong problem |
These 10 questions appear repeatedly in Senior React and Tech Lead rounds. Practice writing each from scratch in under 10 minutes.
196. defer vs async — which blocks HTML parsing?
Theory
Both <script defer> and <script async> are non-blocking downloads — the browser fetches them in parallel while continuing to parse HTML. The difference is when they execute:
| Attribute | Download | Execute | Blocks parsing? |
|---|---|---|---|
| (none) | Blocks parser | Immediately | Yes — blocks parsing AND download |
async | Parallel | As soon as downloaded | No parsing block, but execution blocks parsing |
defer | Parallel | After HTML fully parsed, before DOMContentLoaded | No — execution after parse complete |
type="module" | Parallel | Deferred by default | No |
Neither async nor defer blocks HTML parsing during download. The classic blocking script has
no attribute.
Execution of async scripts can interrupt parsing when they arrive. defer waits until parsing
finishes.
Pros & Cons
| defer | async |
|---|---|
| ✅ Preserves script order | ✅ Executes ASAP |
| ✅ Safe for scripts that depend on DOM | ✅ Good for independent analytics |
| ✅ Predictable — before DOMContentLoaded | ❌ Order not guaranteed |
| ❌ Waits for full parse | ❌ Can interrupt parsing on execute |
Real-Life Example
<!-- ❌ BLOCKS parsing — worst case -->
<script src="heavy-app.js"></script>
<!-- async — download parallel, execute when ready (may interrupt parse) -->
<script async src="analytics.js"></script>
<!-- defer — download parallel, execute after parse, in order -->
<script defer src="app.js"></script>
<script defer src="app-init.js"></script>
<!-- always runs after app.js -->
<!-- module — deferred by default -->
<script type="module" src="main.js"></script>Timeline — defer:
[Parse HTML=========>][Execute defer scripts][DOMContentLoaded][Load event]
Timeline — async (script arrives mid-parse):
[Parse HTML===][EXEC async][Parse HTML===][DOMContentLoaded]
↑ can interrupt parsing
Timeline — no attribute:
[STOP parse][Download+Execute script][Resume parse...]Interview answer (concise)
197. Difference between Promise.all() and Promise.allSettled()
Theory
Both run multiple promises in parallel, but handle failures differently:
Promise.all() | Promise.allSettled() | |
|---|---|---|
| Resolves when | All promises fulfill | All promises settle (fulfill OR reject) |
| Rejects when | Any promise rejects | Never rejects (always resolves) |
| Result | Array of values | Array of { status, value/reason } |
| Use when | All results required together | Need every outcome regardless of failure |
Pros & Cons
| Promise.all | Promise.allSettled |
|---|---|
| ✅ Fail-fast — don't wait for doomed requests | ✅ Partial success — one failure doesn't block others |
| ✅ Simple array of results | ✅ Perfect for independent UI sections |
| ❌ One failure kills entire operation | ❌ Must check each status manually |
| ❌ No partial results on error | ❌ Waits for all even after failures |
Real-Life Example
// Promise.all — ALL must succeed (checkout needs user + cart + payment)
async function loadCheckout(userId) {
try {
const [user, cart, paymentMethods] = await Promise.all([
fetchUser(userId),
fetchCart(userId),
fetchPaymentMethods(userId),
])
return { user, cart, paymentMethods }
} catch (err) {
// One API failed — entire checkout unavailable
showFullPageError('Unable to load checkout')
}
}
// Promise.allSettled — independent sections (the company scenario!)
async function loadDashboard(userId) {
const results = await Promise.allSettled([
fetchAnnouncements(),
fetchTeamMembers(userId),
fetchRecentProjects(userId),
])
return {
announcements: results[0].status === 'fulfilled' ? results[0].value : null,
team: results[1].status === 'fulfilled' ? results[1].value : null,
projects: results[2].status === 'fulfilled' ? results[2].value : null,
errors: results.map((r, i) => (r.status === 'rejected' ? { section: i, reason: r.reason } : null)).filter(Boolean),
}
}// Output shape of allSettled
;[
{ status: 'fulfilled', value: { title: 'Holiday notice' } },
{ status: 'rejected', reason: Error('503 Service Unavailable') },
{ status: 'fulfilled', value: [{ id: 1, name: 'Project Alpha' }] },
]Key interview point: the company scenario question maps directly to Promise.allSettled() + per-section error boundaries.
198. Event Loop — Microtasks vs Macrotasks
Theory
JavaScript is single-threaded. The event loop decides what runs next:
- Execute all synchronous code (call stack)
- Drain all microtasks (Job queue)
- Run one macrotask (Task queue)
- Repeat (render between tasks if needed)
| Type | Examples |
|---|---|
| Microtasks | Promise.then, queueMicrotask, MutationObserver |
| Macrotasks | setTimeout, setInterval, setImmediate, I/O, UI events |
Rule: After every macrotask, ALL microtasks run before the next macrotask.
Classic Interview Output
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// Output: 1 → 4 → 3 → 2
// Sync (1, 4) → Microtask (3) → Macrotask (2)Advanced Puzzle
console.log('start')
setTimeout(() => console.log('timeout 1'), 0)
Promise.resolve()
.then(() => {
console.log('promise 1')
setTimeout(() => console.log('timeout 2'), 0)
})
.then(() => console.log('promise 2'))
setTimeout(() => console.log('timeout 3'), 0)
console.log('end')
// start → end → promise 1 → promise 2 → timeout 1 → timeout 3 → timeout 2a quick-commerce app-Relevant Example
// Why this matters in React apps
async function addToCart(product) {
console.log('1: optimistic UI update')
setCart((prev) => [...prev, product]) // sync state schedule
console.log('2: API call starts')
await fetch('/api/cart', { method: 'POST', body: JSON.stringify(product) })
console.log('3: API done')
Promise.resolve().then(() => console.log('4: microtask after await'))
}
console.log('A')
addToCart(item)
console.log('B')
// A → 1 → 2 → B → (microtasks) → 3 → 4199. Execute N callback-based async tasks in series
Theory
Running async tasks in series means each task starts only after the previous one completes. Unlike Promise.all (parallel), series execution is needed when tasks depend on each other or when the resource (API, DB) cannot handle concurrency.
The classic pattern chains callbacks or uses reduce with promises.
Pros & Cons
| Pros | Cons |
|---|---|
| Safe for dependent operations | Slower than parallel |
| Respects rate limits naturally | One failure blocks the entire chain |
| Predictable order | Harder to cancel mid-chain |
Real-Life Example
function runSeries(tasks, finalCallback) {
let index = 0
const results = []
function next(err, result) {
if (err) return finalCallback(err)
if (result !== undefined) results.push(result)
if (index >= tasks.length) return finalCallback(null, results)
const task = tasks[index++]
task(next)
}
next()
}
// Real-life: Multi-step onboarding — each step depends on previous
runSeries(
[
(cb) => createAccount(userData, cb),
(cb) => sendVerificationEmail(userData.email, cb),
(cb) => setupDefaultPreferences(userData.id, cb),
(cb) => redirectToDashboard(userData.id, cb),
],
(err, results) => {
if (err) console.error('Onboarding failed:', err)
else console.log('Onboarding complete:', results)
},
)
// Modern promise version
function runSeriesPromises(tasks) {
return tasks.reduce(
(chain, task) => chain.then((results) => task().then((result) => [...results, result])),
Promise.resolve([]),
)
}200. Explain closures with a practical example
Theory
A closure is when a function remembers and accesses variables from its lexical scope even after the outer function has finished executing.
Closures enable:
- Data privacy / encapsulation
- Factory functions
- Memoization
- Event handlers with preserved state
- Module pattern
Memory consideration: Closures hold references to outer variables. Holding large objects in closures can cause memory leaks if not cleaned up.
Practical Example — Counter Module
function createCounter(initial = 0) {
let count = initial // private state
return {
increment() {
count++
},
decrement() {
count--
},
getCount() {
return count
},
}
}
const counter = createCounter(10)
counter.increment()
console.log(counter.getCount()) // 11
// count is NOT accessible directly — encapsulated via closurePractical Example — Memoization (common in interviews)
function memoize(fn) {
const cache = new Map() // closed over by returned function
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)
const result = fn(...args)
cache.set(key, result)
return result
}
}
const expensiveSum = memoize((a, b) => {
console.log('Computing...')
return a + b
})
expensiveSum(2, 3) // Computing... → 5
expensiveSum(2, 3) // 5 (cached, no log)Practical Example — React-style debounce
function debounce(fn, delay) {
let timerId // closure holds timer reference
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
const handleSearch = debounce((query) => {
console.log('API call:', query)
}, 300)201. Explain currying and function composition
Currying
Transform a function with multiple arguments into a chain of functions each taking one argument.
// Regular
function add(a, b, c) {
return a + b + c
}
add(1, 2, 3) // 6
// Curried
function curriedAdd(a) {
return function (b) {
return function (c) {
return a + b + c
}
}
}
curriedAdd(1)(2)(3) // 6
// Generic curry utility
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return (...more) => curried(...args, ...more)
}
}
const multiply = (a, b, c) => a * b * c
const curriedMultiply = curry(multiply)
curriedMultiply(2)(3)(4) // 24
curriedMultiply(2, 3)(4) // 24
curriedMultiply(2)(3, 4) // 24Function composition
Combine functions so output of one feeds into the next: compose(f, g)(x) === f(g(x)).
const compose =
(...fns) =>
(value) =>
fns.reduceRight((acc, fn) => fn(acc), value)
const pipe =
(...fns) =>
(value) =>
fns.reduce((acc, fn) => fn(acc), value)
const trim = (s) => s.trim()
const toLower = (s) => s.toLowerCase()
const removeSpaces = (s) => s.replace(/\s+/g, '-')
const slugify = compose(removeSpaces, toLower, trim)
console.log(slugify(' Hello World ')) // "hello-world"
const slugifyPipe = pipe(trim, toLower, removeSpaces)
console.log(slugifyPipe(' Hello World ')) // "hello-world"Real-world use
// Currying — partial application for reusable handlers
const log = curry((level, message) => console[level](message))
const logError = log('error')
const logInfo = log('info')
logError('Something failed')
logInfo('App started')202. Explain lexical scope and closure in detail
Lexical scope
Scope is determined by where code is written (lexical position), not where it is called.
const globalVar = 'global'
function outer() {
const outerVar = 'outer'
function inner() {
const innerVar = 'inner'
console.log(globalVar) // accessible
console.log(outerVar) // accessible
console.log(innerVar) // accessible
}
return inner
}
const fn = outer()
fn() // still accesses outerVar — closureScope chain
inner scope → outer scope → global scopeClosure — formal definition
A closure is a function bundled together with its lexical environment (scope). The environment records all local variables at the time the function was created.
function makeMultiplier(factor) {
// `factor` is closed over
return function (number) {
return number * factor
}
}
const double = makeMultiplier(2)
const triple = makeMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
// Each closure has its own `factor`Module pattern (classic closure use)
const bankAccount = (function () {
let balance = 0 // private via closure
return {
deposit(amount) {
balance += amount
},
getBalance() {
return balance
},
}
})()
bankAccount.deposit(100)
console.log(bankAccount.getBalance()) // 100
// balance is not accessible directlyLoop + closure classic fix
// Problem
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 3, 3, 3
}
// Fix with let (block scope per iteration)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 0, 1, 2
}
// Fix with IIFE closure
for (var i = 0; i < 3; i++) {
;((j) => {
setTimeout(() => console.log(j), 100)
})(i)
}203. Garbage Collection
Theory
JS uses automatic garbage collection. The main algorithm is mark-and-sweep: mark all objects reachable from roots (global, call stack, closures), sweep unmarked objects.
V8 uses generational GC: young generation (Scavenge, frequent) and old generation (Mark-Sweep-Compact, less frequent).
Common leak causes
- Global variables holding references
- Forgotten timers / intervals
- Event listeners not removed
- Closures holding large objects
- Detached DOM nodes referenced in JS
Real Example
// ❌ Leak
const cache = {}
function loadUser(id) {
cache[id] = fetchHugeUserObject(id) // grows forever
}
// ✅ Bounded cache
const cache = new Map()
const MAX = 100
function loadUserCached(id) {
if (cache.has(id)) return cache.get(id)
const data = fetchUser(id)
if (cache.size >= MAX) cache.delete(cache.keys().next().value)
cache.set(id, data)
return data
}
// ✅ WeakMap — doesn't prevent GC of keys
const domData = new WeakMap()
function attach(el, data) {
domData.set(el, data)
}
// When el removed from DOM and unreferenced, entry is GC'd204. How do map() and bind() methods work?
Theory
These are different concepts that interviewers often group together:
Array.prototype.map() — creates a new array by transforming each element
through a callback. Does not mutate the original array.
Function.prototype.bind() — creates a new function with a fixed this
context and optionally pre-filled arguments. Does not invoke the function immediately.
Pros & Cons
| map() | bind() |
|---|---|
| ✅ Immutable transformation | ✅ Fixes this in callbacks |
| ✅ Declarative, chainable | ✅ Partial application |
| ❌ Creates new array (memory) | ❌ Less needed with arrow functions/hooks |
| ❌ Can't short-circuit early | ❌ Slight memory overhead per bound function |
Real-Life Example — map()
const orders = [
{ id: 1, item: 'Biryani', price: 299 },
{ id: 2, item: 'Naan', price: 49 },
{ id: 3, item: 'Lassi', price: 79 },
]
// Transform API data for UI display
const orderSummary = orders.map((order) => ({
label: `${order.item} — ₹${order.price}`,
id: order.id,
}))
// [{ label: "Biryani — ₹299", id: 1 }, ...]
// React — map is the primary way to render lists
function OrderList({ orders }) {
return (
<ul>
{orders.map((order) => (
<li key={order.id}>
{order.item} — ₹{order.price}
</li>
))}
</ul>
)
}Real-Life Example — bind()
const deliveryPartner = {
name: 'Rahul',
getStatus(orderId) {
return `${this.name} is delivering order #${orderId}`
},
}
// bind — fix `this` and optionally pre-fill args
const getStatusForOrder = deliveryPartner.getStatus.bind(deliveryPartner)
getStatusForOrder(1042) // "Rahul is delivering order #1042"
// Partial application
const greet = function (greeting, name) {
return `${greeting}, ${name}!`
}
const sayHello = greet.bind(null, 'Hello')
sayHello('Amit') // "Hello, Amit!"
// Class component (legacy) — bind in constructor
class OrderForm extends React.Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit() {
this.props.onSubmit(this.state) // `this` is correct
}
}
// Modern React — arrow functions replace bind
function OrderForm({ onSubmit }) {
const [data, setData] = useState({})
const handleSubmit = () => onSubmit(data) // no bind needed
}205. How does garbage collection work in JavaScript?
Theory
JS uses automatic garbage collection — memory for unreachable objects is reclaimed.
Primary algorithm: Mark-and-Sweep
1. GC roots: global object, call stack variables, closures
2. Mark: traverse all reachable objects from roots
3. Sweep: free memory of unmarked (unreachable) objectsGenerational GC (V8)
| Generation | Characteristics | Collection frequency |
|---|---|---|
| Young (nursery) | Short-lived objects | Frequent (Scavenge) |
| Old | Long-lived, survived collections | Less frequent (Mark-Sweep-Compact) |
Objects that survive two Scavenge cycles are promoted to old generation.
What causes memory leaks
// 1. Global variables
window.cache = hugeArray
// 2. Forgotten timers
setInterval(() => {
/* holds closure ref */
}, 1000)
// 3. Detached DOM nodes still referenced
const el = document.getElementById('btn')
document.body.removeChild(el)
// el still in memory if referenced in JS
// 4. Closures holding large objects
function createHandler() {
const bigData = new Array(1e6).fill('x')
return () => console.log(bigData.length) // bigData never freed
}
// 5. Event listeners not removed
element.addEventListener('click', handler)
// forgot removeEventListener on cleanupWeakMap/WeakSet help avoid leaks by not preventing GC of keys.
206. How does the JavaScript Event Loop work?
Theory
JavaScript is single-threaded — one call stack. The event loop coordinates:
- Call stack — synchronous code execution
- Web APIs — setTimeout, fetch, DOM (run outside main thread)
- Microtask queue — Promises, queueMicrotask
- Macrotask queue — setTimeout, I/O, UI events
Order: Run all sync code → drain all microtasks → run one macrotask → repeat.
Pros & Cons
| Understanding event loop | Ignoring it |
|---|---|
| Predict async behavior | Surprised by Promise/setTimeout order |
| Avoid blocking UI | Long sync code freezes app |
| Debug race conditions | Stale closure bugs in async |
Real Example
console.log('1')
setTimeout(() => console.log('4'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('2')
// Output: 1 → 2 → 3 → 4// Senior insight — don't block the loop
function processPayments(transactions) {
// ❌ Blocks UI for 2 seconds
transactions.forEach(heavyValidation)
// ✅ Chunk with macrotasks
let i = 0
function chunk() {
const end = Math.min(i + 100, transactions.length)
for (; i < end; i++) heavyValidation(transactions[i])
if (i < transactions.length) setTimeout(chunk, 0)
}
chunk()
}207. How to control tab order in DOM (explain tabIndex)
Theory
Tab order determines which element receives focus when the user presses Tab. By default, focusable elements (links, buttons, inputs) are tabbed in DOM order.
tabIndex controls this:
tabIndex="0"— element is focusable and in natural tab ordertabIndex="-1"— focusable programmatically (focus()) but not in tab ordertabIndex="1+"— focusable and tabbed before natural order (avoid — confuses users)
Pros & Cons
| Approach | Pros | Cons |
|---|---|---|
| Natural DOM order | Accessible by default | Layout changes can break logical order |
tabIndex={0} on custom elements | Makes divs/spans keyboard-accessible | Must add keyboard handlers too |
tabIndex={-1} for modals | Focus trap without tab pollution | Requires focus management code |
| Positive tabIndex | — | Never use — unpredictable order |
Real-Life Example
function CheckoutModal({ isOpen, onClose }) {
const firstFocusableRef = useRef(null)
const lastFocusableRef = useRef(null)
useEffect(() => {
if (isOpen) firstFocusableRef.current?.focus()
}, [isOpen])
const handleTabKey = (e) => {
if (e.key !== 'Tab') return
if (e.shiftKey && document.activeElement === firstFocusableRef.current) {
e.preventDefault()
lastFocusableRef.current?.focus()
} else if (!e.shiftKey && document.activeElement === lastFocusableRef.current) {
e.preventDefault()
firstFocusableRef.current?.focus()
}
}
return (
<div role="dialog" aria-modal="true" onKeyDown={handleTabKey}>
<button ref={firstFocusableRef} tabIndex={0}>
Continue
</button>
<button tabIndex={0}>Cancel</button>
<button ref={lastFocusableRef} tabIndex={0} onClick={onClose}>
Close
</button>
</div>
)
}
// Skip navigation link (accessibility best practice)
;<a href="#main-content" className="sr-only focus:not-sr-only" tabIndex={0}>
Skip to main content
</a>208. How to override toString on String.prototype
Theory
String.prototype.toString returns the string value. You can override it, but mutating built-in prototypes is strongly discouraged — it breaks libraries, causes conflicts, and is considered bad practice. Prefer utility functions or wrapper classes instead.
If asked in an interview, show you know how but also know why not to.
Pros & Cons
| Pros | Cons |
|---|---|
| Global behavior change | Breaks other libraries expecting default behavior |
| Theoretical knowledge | Security risk in shared environments |
| — | Hard to debug, non-standard |
Real-Life Example
// ❌ Don't do this in production
String.prototype.toString = function () {
return `🎉 ${this.valueOf()}`
}
console.log('hello'.toString()) // "🎉 hello"
// Breaks: JSON.stringify, template literals, comparisons
// ✅ Better approach — utility function
function displayString(str) {
return `🎉 ${str}`
}
// ✅ Or a wrapper class
class DisplayString {
constructor(value) {
this.value = value
}
toString() {
return `🎉 ${this.value}`
}
}209. Implement Array.prototype.reduce polyfill
Theory
Array.prototype.reduce executes a reducer function on each element, carrying an accumulator from left to right, and returns a single final value. It is the most flexible array method — it can implement map, filter, flat, and more.
The reducer receives (accumulator, currentValue, index, array) and must return the next accumulator.
Pros & Cons
| Pros | Cons |
|---|---|
| Extremely versatile — one method, many patterns | Harder to read than map/filter for beginners |
| No intermediate arrays (unlike chaining) | Easy to write inefficient reducers |
| Works on sparse arrays with proper checks | Cannot short-circuit early (unlike for loop with break) |
Real-Life Example
Array.prototype.myReduce = function (callback, initialValue) {
if (this == null) throw new TypeError('Array is null or undefined')
if (typeof callback !== 'function') throw new TypeError(callback + ' is not a function')
const arr = Object(this)
const len = arr.length >>> 0
let accumulator
let startIndex = 0
if (arguments.length >= 2) {
accumulator = initialValue
} else {
while (startIndex < len && !(startIndex in arr)) startIndex++
if (startIndex >= len) {
throw new TypeError('Reduce of empty array with no initial value')
}
accumulator = arr[startIndex++]
}
for (let i = startIndex; i < len; i++) {
if (i in arr) {
accumulator = callback(accumulator, arr[i], i, arr)
}
}
return accumulator
}
// Real-life: Calculate total order value with tax and discounts
const order = [
{ name: 'Biryani', price: 299, qty: 2 },
{ name: 'Naan', price: 49, qty: 4 },
]
const total = order.myReduce((sum, item) => sum + item.price * item.qty, 0)
// 299*2 + 49*4 = 794210. Implement auto-retry for promises
Theory
Auto-retry wraps an async operation to automatically re-attempt on failure, typically with exponential backoff and optional jitter. This handles transient failures — network blips, 503 errors, rate limits — without user intervention.
Key parameters: max retries, base delay, backoff multiplier, which errors are retryable.
Pros & Cons
| Pros | Cons |
|---|---|
| Improves reliability on flaky networks | Can amplify load on failing servers |
| Better UX — fewer visible errors | Non-idempotent POST requests can duplicate data |
| Standard pattern in production systems | Needs careful error classification |
Real-Life Example
async function retry(fn, { retries = 3, delay = 1000, backoff = 2 } = {}) {
let lastError
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn()
} catch (err) {
lastError = err
if (attempt === retries) break
const wait = delay * Math.pow(backoff, attempt) + Math.random() * 100
await new Promise((r) => setTimeout(r, wait))
}
}
throw lastError
}
// Real-life: Retry payment status check after checkout
async function checkPaymentStatus(orderId) {
return retry(
() =>
fetch(`/api/orders/${orderId}/status`).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}),
{ retries: 5, delay: 500, backoff: 2 },
)
}211. Implement Promise.all polyfill
Theory
Promise.all takes an iterable of promises (or values) and returns a single promise that resolves when all inputs resolve, with results in the same order as inputs. If any promise rejects, the entire operation rejects immediately with that error.
It is the standard pattern for parallel async work where every result is required — like loading user profile, permissions, and preferences before rendering a dashboard.
Pros & Cons
| Pros | Cons |
|---|---|
| Parallel execution — faster than sequential | Fails entirely if one request fails |
| Preserves result order | No partial results on failure |
| Simple mental model | All requests run even after first failure (until rejection propagates) |
Real-Life Example
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('promises must be an array'))
}
const results = []
let completed = 0
const len = promises.length
if (len === 0) return resolve([])
promises.forEach((p, index) => {
Promise.resolve(p)
.then((value) => {
results[index] = value
completed++
if (completed === len) resolve(results)
})
.catch(reject)
})
})
}
// Real-life: Load checkout page data in parallel
async function loadCheckoutPage(userId) {
const [cart, addresses, paymentMethods] = await promiseAll([
fetch(`/api/cart/${userId}`).then((r) => r.json()),
fetch(`/api/addresses/${userId}`).then((r) => r.json()),
fetch(`/api/payments/${userId}`).then((r) => r.json()),
])
return { cart, addresses, paymentMethods }
}212. Implement Promise.any polyfill
Theory
Promise.any takes an iterable of promises and returns a promise that resolves with the first fulfilled result. It only rejects if all promises reject — with an AggregateError containing all rejection reasons.
Use it when you have multiple fallback sources and only need one success — CDN mirrors, redundant API endpoints, or fastest-wins scenarios.
Pros & Cons
| Pros | Cons |
|---|---|
| Resilient — one success is enough | Waits for all to fail before rejecting |
| Great for redundancy / racing sources | All requests still consume bandwidth |
| Ignores individual failures | No built-in timeout (must add manually) |
Real-Life Example
function promiseAny(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises) || promises.length === 0) {
return reject(new AggregateError([], 'All promises were rejected'))
}
const errors = []
let rejectedCount = 0
promises.forEach((p, index) => {
Promise.resolve(p)
.then(resolve)
.catch((err) => {
errors[index] = err
rejectedCount++
if (rejectedCount === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'))
}
})
})
})
}
// Real-life: Fetch image from fastest CDN mirror
async function loadProductImage(productId) {
const imageUrl = await promiseAny([
fetch(`https://cdn1.example.com/images/${productId}`),
fetch(`https://cdn2.example.com/images/${productId}`),
fetch(`https://cdn3.example.com/images/${productId}`),
])
return imageUrl.blob()
}213. Implement your own version of Promise.all()
Theory
Promise.all(iterable) returns a single promise that:
- Resolves with an array of results when all input promises resolve (order preserved)
- Rejects immediately when any input promise rejects
Implementation
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'))
}
const results = []
let completed = 0
const len = promises.length
if (len === 0) return resolve([])
promises.forEach((promise, index) => {
Promise.resolve(promise) // handles non-promise values
.then((value) => {
results[index] = value // preserve order
completed++
if (completed === len) resolve(results)
})
.catch(reject) // fail fast
})
})
}
// Tests
promiseAll([Promise.resolve(1), 42, Promise.resolve(3)]).then(console.log) // [1, 42, 3]
promiseAll([Promise.resolve(1), Promise.reject('error')]).catch(console.log) // "error"Edge cases to mention
- Empty array → resolves
[] - Non-promise values are wrapped via
Promise.resolve() - Order of results matches input order, not completion order
214. JavaScript Event Loop
Theory
JS is single-threaded. The event loop coordinates: call stack → microtasks (Promises) → macrotasks (setTimeout).
Real Example
console.log('1')
setTimeout(() => console.log('4'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('2')
// 1 → 2 → 3 → 4215. Polyfill for Array.map()
Array.prototype.myMap = function (callback, thisArg) {
if (this == null) {
throw new TypeError('Array.prototype.myMap called on null or undefined')
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function')
}
const array = Object(this)
const len = array.length >>> 0
const result = new Array(len)
for (let i = 0; i < len; i++) {
if (i in array) {
result[i] = callback.call(thisArg, array[i], i, array)
}
}
return result
}
// Test
console.log([1, 2, 3].myMap((x) => x * 2)) // [2, 4, 6]
console.log([1, , 3].myMap((x) => x * 2)) // [2, empty, 6] — sparse handledKey details
- Handle
thisbeing array-like (not just arrays) - Use
>>> 0for length (handles negatives) - Check
i in arrayfor sparse arrays - Don't mutate original array
216. Polyfill for Function.bind()
Function.prototype.myBind = function (thisArg, ...boundArgs) {
const fn = this
if (typeof fn !== 'function') {
throw new TypeError('Bind must be called on a function')
}
const boundFunction = function (...callArgs) {
// If called with `new`, `this` should be the new instance
const isNew = this instanceof boundFunction
return fn.apply(isNew ? this : thisArg, boundArgs.concat(callArgs))
}
// Preserve prototype for `new` calls
if (fn.prototype) {
boundFunction.prototype = Object.create(fn.prototype)
}
return boundFunction
}
// Test
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const person = { name: 'Amit' }
const boundGreet = greet.myBind(person, 'Hello')
console.log(boundGreet('!')) // "Hello, Amit!"
// Constructor case
function Person(name) {
this.name = name
}
const BoundPerson = Person.myBind(null)
const p = new BoundPerson('Rahul')
console.log(p.name) // "Rahul"217. Polyfills & Babel
Theory
Babel is a JavaScript compiler/transpiler that converts modern JS (ES2022+) into code that older browsers understand. It handles syntax transformation (arrow functions, classes, async/await).
Polyfills add missing API implementations at runtime (e.g., Promise,
Array.prototype.flat, fetch). Babel handles syntax; polyfills handle APIs.
@babel/preset-env uses browserslist to determine which transforms and polyfills are needed.
Pros & Cons
| Babel + polyfills | Ship modern JS only |
|---|---|
| ✅ Works on older browsers | ✅ Smaller bundle |
| ✅ Use latest syntax today | ❌ Breaks on IE/old Safari |
| ❌ Increases bundle size | ❌ Limits API usage |
| ❌ Build step required |
Real-Life Example
// .babelrc
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage", // auto-import only needed polyfills
"corejs": 3
}],
"@babel/preset-react",
"@babel/preset-typescript"
]
}// Modern code you write
const greet = async (name) => {
const data = await fetch(`/api/users/${name}`).then((r) => r.json())
return data.items?.flat() ?? []
}
// Babel transforms syntax (async/await → regenerator)
// core-js polyfills: Promise, Array.prototype.flat, optional chaining// Manual polyfill example — Promise.all
if (!Promise.all) {
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
const results = []
let done = 0
promises.forEach((p, i) => {
Promise.resolve(p)
.then((v) => {
results[i] = v
if (++done === promises.length) resolve(results)
})
.catch(reject)
})
})
}
}// browserslist — controls Babel output
{
"browserslist": [">0.2%", "not dead", "not ie 11"]
}Babel vs Polyfill:
| Babel | Polyfill | |
|---|---|---|
| Handles | Syntax (arrow, class, async) | Missing APIs (Promise, fetch, flat) |
| When | Build time | Runtime |
| Example | const fn = () => {} → var fn = function(){} | Adds Array.prototype.flat if missing |
| # | Topic | One-liner |
|---|---|---|
| 1 | Pagination | Offset or cursor; shareable pages |
| 2 | Infinite scroll | Intersection Observer + cursor API |
| 3 | Debouncing | Wait for pause before executing |
| 4 | WebSocket | Persistent bidirectional real-time connection |
| 5 | REST vs GraphQL | Fixed endpoints vs client-defined queries |
| 6 | localStorage vs Cookies | 5MB client-only vs 4KB auto-sent; httpOnly for auth |
| 7 | AuthN vs AuthZ | Who are you? vs What can you do? |
| 8 | Redux | dispatch → reducer → store → UI |
| 9 | Lazy loading | Defer until needed — lazy(), loading="lazy" |
| 10 | Code splitting | Dynamic import → separate chunks |
| 11 | Bundle optimization | Analyze, split, import selectively, <200KB |
| 12 | Tree shaking | ESM dead code elimination at build time |
| 13 | Memoization | useMemo=value, useCallback=fn, memo=component |
| 14 | Caching | HTTP headers, CDN, React Query, Service Worker |
| 15 | CSR/SSR/SSG/ISR | Browser / server / build / build+revalidate |
| 16 | Web Vitals | LCP≤2.5s, INP≤200ms, CLS≤0.1 |
| 17 | Cross-browser | Can I Use, Autoprefixer, polyfills, browserslist |
| 18 | Optimistic UI | Update immediately, rollback on failure |
| 19 | Suspense | Declarative fallback while loading |
| 20 | Images | AVIF → WebP → JPEG; srcset, lazy, dimensions |
| 21 | a11y | Semantic HTML, ARIA, keyboard, WCAG AA |
| 22 | Webpack | Module bundler — entry, loaders, plugins, chunks |
| 23 | Micro-frontends | Independent deployable frontend apps |
| 24 | Testing | Jest=unit, RTL=component, Playwright=E2E |
| 25 | Babel/Polyfills | Babel=syntax transform; polyfill=missing APIs |
218. Promise & Async/Await Output Puzzle
Theory
Remember: sync first → all microtasks (Promises, await) → one macrotask (setTimeout).
Senior interviews give multi-layer puzzles mixing async/await, Promise chains, and setTimeout.
Puzzle 1 — Classic
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')Output: 1 → 4 → 3 → 2
Why: Sync (1, 4) → microtask (3) → macrotask (2)
Puzzle 2 — async/await
async function foo() {
console.log('A')
await Promise.resolve()
console.log('B')
}
console.log('C')
foo()
console.log('D')Output: C → A → D → B
Why: await pauses foo after A; D runs synchronously; B resumes as microtask.
Puzzle 3 — Nested promises
console.log('start')
setTimeout(() => console.log('timeout'), 0)
Promise.resolve()
.then(() => {
console.log('promise 1')
return Promise.resolve()
})
.then(() => console.log('promise 2'))
Promise.resolve().then(() => console.log('promise 3'))
console.log('end')Output: start → end → promise 1 → promise 3 → promise 2 → timeout
Puzzle 4 — async function return
async function getData() {
return 42
}
console.log(getData())
getData().then(console.log)Output: Promise { 42 } → 42
Why: async functions always return a Promise.
Puzzle 5 — Senior trap
async function run() {
console.log(1)
await null
console.log(2)
await null
console.log(3)
}
run()
console.log(4)Output: 1 → 4 → 2 → 3
219. Promises
Theory
A Promise represents a value that may be available now, later, or never. It has three states:
- Pending — initial state
- Fulfilled — operation succeeded
- Rejected — operation failed
Once settled (fulfilled or rejected), a promise cannot change state. .then() handles success, .catch() handles errors, .finally() runs regardless.
Pros & Cons
| Promises | Callbacks |
|---|---|
| ✅ Avoid callback hell | ✅ Simple for one-off async |
✅ .then chain is readable | ❌ Pyramid of doom |
✅ Promise.all, race, any | ❌ Hard error handling |
| ❌ Still need careful error handling |
Real-Life Example
// Creating promises
function fetchOrder(orderId) {
return fetch(`/api/orders/${orderId}`).then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
}
// Chaining
fetchOrder('ORD-42')
.then((order) => enrichWithDriverInfo(order))
.then((order) => renderOrderUI(order))
.catch((err) => showErrorToast(err.message))
.finally(() => hideSpinner())
// Promise.all — parallel, all must succeed
const [user, cart, addresses] = await Promise.all([fetchUser(userId), fetchCart(userId), fetchAddresses(userId)])
// Promise.race — first to settle wins
const result = await Promise.race([
fetchFromPrimaryCDN(url),
timeout(5000), // reject after 5s
])
// Custom Promise.all polyfill concept
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = []
let done = 0
if (!promises.length) return resolve([])
promises.forEach((p, i) => {
Promise.resolve(p)
.then((val) => {
results[i] = val
if (++done === promises.length) resolve(results)
})
.catch(reject)
})
})
}220. Prototype & Inheritance
Theory
Every JavaScript object has an internal link to another object called its prototype ([[Prototype]], accessible via Object.getPrototypeOf() or __proto__). When you access a property, JavaScript walks the prototype chain until it finds the property or reaches null.
Constructor functions (pre-ES6) and classes (ES6 syntactic sugar) create objects that inherit from a shared prototype.
Pros & Cons
| Prototypal inheritance | Classical inheritance (Java/C++) |
|---|---|
| ✅ Flexible — objects inherit from objects | ❌ Rigid class hierarchies |
| ✅ Dynamic — add methods at runtime | |
| ✅ Memory efficient — shared methods on prototype | |
❌ Confusing this and prototype chain |
Real-Life Example
// Constructor + prototype (ES5 style)
function Restaurant(name, cuisine) {
this.name = name
this.cuisine = cuisine
}
Restaurant.prototype.getDetails = function () {
return `${this.name} (${this.cuisine})`
}
Restaurant.prototype.isOpen = function () {
return this.status === 'open'
}
const r1 = new Restaurant('Spice Garden', 'North Indian')
const r2 = new Restaurant('Dosa Corner', 'South Indian')
console.log(r1.getDetails()) // "Spice Garden (North Indian)"
// getDetails lives on Restaurant.prototype — shared, not duplicated
// ES6 class — syntactic sugar over prototypes
class Order {
constructor(id, items) {
this.id = id
this.items = items
}
getTotal() {
return this.items.reduce((sum, i) => sum + i.price, 0)
}
}
class DeliveryOrder extends Order {
constructor(id, items, address) {
super(id, items)
this.address = address
}
getTotal() {
return super.getTotal() + 40 // delivery fee
}
}
const order = new DeliveryOrder('ORD-1', [{ price: 299 }])
console.log(order.getTotal()) // 339
// Prototype chain check
order instanceof DeliveryOrder // true
order instanceof Order // true
order.hasOwnProperty('id') // true
order.hasOwnProperty('getTotal') // false (on prototype)221. Prototypes and Prototype Chain
Theory
Every JS object has an internal link [[Prototype]] (accessed via Object.getPrototypeOf() or __proto__). When you access a property, JS walks the prototype chain until it finds the property or reaches null.
Functions have a prototype property used when called with new.
Real Example
function Transaction(amount, type) {
this.amount = amount;
this.type = type;
}
Transaction.prototype.getFormatted = function () {
return `₹${this.amount} (${this.type})`;
};
Transaction.prototype.isCredit = function () {
return this.type === "credit";
};
const txn = new Transaction(500, "credit");
console.log(txn.getFormatted()); // "₹500 (credit)"
// Prototype chain
txn → Transaction.prototype → Object.prototype → null
console.log(txn.hasOwnProperty("amount")); // true
console.log(txn.hasOwnProperty("getFormatted")); // false — on prototype
// ES6 class — syntactic sugar over prototypes
class Payment {
constructor(amount) { this.amount = amount; }
getFormatted() { return `₹${this.amount}`; }
}// Senior: Object.create for pure prototypal inheritance
const animal = {
breathe() {
return 'breathing'
},
}
const dog = Object.create(animal)
dog.bark = () => 'woof'
dog.breathe() // "breathing" — inherited222. var, let & const
Theory
var, let, and const are three ways to declare variables in JavaScript. They differ in scope, hoisting behavior, and reassignment rules.
var— function-scoped, hoisted asundefined, can be redeclaredlet— block-scoped, hoisted but in TDZ until declared, cannot be redeclaredconst— block-scoped, same asletbut cannot be reassigned (objects/arrays can still be mutated)
TDZ (Temporal Dead Zone): The period between entering a scope and the line where let/const is declared. Accessing the variable during TDZ throws ReferenceError.
Pros & Cons
| var | let | const | |
|---|---|---|---|
| Pros | Works everywhere (legacy) | Block scope, no redeclare bugs | Intent is clear — won't reassign |
| Cons | Hoisting bugs, no block scope | — | Can't reassign (even if you need to) |
| Use when | Never in modern code | Value will change | Default choice |
Real-Life Example
// Block scope
function processOrder() {
if (true) {
var oldWay = 'var' // function-scoped
let newWay = 'let' // block-scoped
const API = 'v2' // block-scoped, constant binding
}
console.log(oldWay) // "var" — leaked out
// console.log(newWay); // ReferenceError
}
// const does NOT freeze objects
const cart = { items: [], total: 0 }
cart.items.push({ id: 1, name: 'Biryani' }) // ✅ mutation OK
cart.total = 299 // ✅ mutation OK
// cart = {}; // ❌ TypeError — rebinding
// Classic var loop bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100) // 0, 1, 2
}Rule: Use const by default → let when reassignment is needed → avoid var.
223. var, let, and const
Theory
| var | let | const | |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | undefined | TDZ | TDZ |
| Redeclare | Yes | No | No |
| Reassign | Yes | Yes | No (binding) |
| Global object prop | Yes (window) | No | No |
const prevents rebinding, not mutation of objects.
Real Example
const config = { apiUrl: '/api/v2', retries: 3 }
config.retries = 5 // ✅ mutation OK
// config = {}; // ❌ TypeError
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 0, 1, 2
}
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100) // 3, 3, 3
}224. What are WeakMap and WeakSet?
Theory
Map / Set | WeakMap / WeakSet | |
|---|---|---|
| Keys | Any type | Objects only |
| GC | Strong references — prevent GC | Weak references — don't prevent GC |
| Iterable | Yes | No |
| Size property | Yes | No |
WeakMap
const wm = new WeakMap()
let user = { id: 1, name: 'Amit' }
wm.set(user, { lastLogin: Date.now() })
console.log(wm.get(user)) // { lastLogin: ... }
user = null // object can now be garbage collected
// WeakMap entry disappears automaticallyWeakSet
const visited = new WeakSet()
function processNode(node) {
if (visited.has(node)) return
visited.add(node)
// process...
}Practical use cases
// 1. Private data associated with DOM nodes
const privateData = new WeakMap()
class Component {
constructor(el) {
privateData.set(el, { listeners: [] })
}
}
// 2. Caching metadata without memory leaks
const cache = new WeakMap()
function getMetadata(obj) {
if (!cache.has(obj)) {
cache.set(obj, computeExpensiveMetadata(obj))
}
return cache.get(obj)
}
// 3. Track processed objects in recursive algorithms
const seen = new WeakSet()
function deepTraverse(obj) {
if (seen.has(obj)) return
seen.add(obj)
// ...
}225. What happens behind the scenes when async/await executes?
Syntax sugar over Promises + Generators
async function fetchUser() {
const res = await fetch('/api/user')
const data = await res.json()
return data
}
// Roughly equivalent to:
function fetchUser() {
return fetch('/api/user').then((res) => res.json())
}Execution flow
1. async function ALWAYS returns a Promise
2. Code before first `await` runs synchronously
3. At `await`:
- Expression is evaluated
- If not a Promise, wrapped in Promise.resolve()
- Execution of async function PAUSES
- Control returns to caller
- `.then()` handler registered on the promise
4. When awaited promise resolves:
- Function resumes after await
- Resolved value assigned to variable
- Runs on microtask queue
5. If awaited promise rejects:
- Function throws (unless try/catch)
- Returned promise rejectsMicrotask queue demonstration
console.log('1')
async function demo() {
console.log('2')
await Promise.resolve()
console.log('3') // microtask — after sync code
}
demo()
console.log('4')
// Output: 1, 2, 4, 3Error handling
async function risky() {
try {
const data = await fetch('/api').then((r) => {
if (!r.ok) throw new Error('HTTP error')
return r.json()
})
return data
} catch (err) {
console.error('Caught:', err.message)
throw err // re-throw to caller
}
}Interview answer
226. What is createAsyncThunk?
Answer
It is used to handle asynchronous logic like API calls. It auto-generates pending, fulfilled, and rejected action types.
227. What is a Promise?
Theory
A Promise is an object representing the eventual result of an asynchronous operation. It has three states:
- Pending — initial state, operation in progress
- Fulfilled — operation completed successfully
- Rejected — operation failed
Once settled (fulfilled or rejected), a promise cannot change state. Promises chain with .then(), .catch(), .finally().
Pros & Cons
| Promises | Callbacks |
|---|---|
| ✅ Avoid callback hell | ✅ Simple for one-off async |
✅ Composable — all, race, allSettled | ❌ Error handling is painful |
| ✅ Clear success/error paths | ❌ Pyramid of doom |
| ❌ Still need discipline for errors |
Real-Life Example
// Creating a promise
function fetchEmployee(id) {
return new Promise((resolve, reject) => {
fetch(`/api/employees/${id}`)
.then((res) => {
if (!res.ok) reject(new Error(`HTTP ${res.status}`))
else resolve(res.json())
})
.catch(reject)
})
}
// Consuming
fetchEmployee('E101')
.then((employee) => renderProfile(employee))
.catch((err) => showErrorToast(err.message))
.finally(() => hideSpinner())
// async/await — syntactic sugar over promises
async function loadProfile(id) {
try {
const employee = await fetchEmployee(id)
renderProfile(employee)
} catch (err) {
showErrorToast(err.message)
} finally {
hideSpinner()
}
}228. What is Closure?
Theory
A closure is a function bundled with its lexical environment — the scope where it was created. The inner function retains access to outer variables even after the outer function has finished executing.
Closures enable: data privacy, factory functions, memoization, debouncing, and module patterns.
Pros & Cons
| Pros | Cons |
|---|---|
| Encapsulation / private state | Memory — outer vars stay alive while closure exists |
| Powerful callback patterns | Accidental leaks if not cleaned up |
| Module pattern without classes | Harder to debug deep chains |
Real Example
function createWallet(initialBalance) {
let balance = initialBalance // private via closure
return {
deposit(amount) {
balance += amount
},
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds')
balance -= amount
},
getBalance() {
return balance
},
}
}
const wallet = createWallet(1000)
wallet.deposit(500)
console.log(wallet.getBalance()) // 1500
// balance is NOT accessible directly — encapsulated// React-relevant: debounce hook uses closure
function useDebounce(callback, delay) {
let timerId // closed over
return function (...args) {
clearTimeout(timerId)
timerId = setTimeout(() => callback(...args), delay)
}
}229. What is Hoisting in JavaScript?
Theory
Hoisting is JavaScript's behavior of processing declarations before executing code. During the creation phase of an execution context, the engine registers variable and function declarations in memory — before the first line runs.
Important: Only declarations are hoisted, not initializations.
| Declaration | Hoisted as |
|---|---|
var x | undefined |
let x / const x | uninitialized (TDZ) |
function foo(){} | full function |
var fn = function(){} | var fn → undefined |
class MyClass | uninitialized (TDZ) |
Pros & Cons
| Pros | Cons |
|---|---|
| Function declarations callable before definition | var hoisting causes subtle bugs |
| Flexible code organization | Misleading mental model ("moved to top") |
| Easy to use variables before assignment |
Real-Life Example
// var hoisting
console.log(status) // undefined (not ReferenceError)
var status = 'active'
// Equivalent to:
// var status;
// console.log(status);
// status = "active";
// Function declaration — fully hoisted
authenticate()
function authenticate() {
console.log('User authenticated')
}
// Function expression — only var hoisted
// processOrder(); // TypeError: processOrder is not a function
var processOrder = function () {
console.log('Processing...')
}
// let/const — hoisted but TDZ
// console.log(theme); // ReferenceError
let theme = 'dark'// Classic interview trap
var price = 100
function getPrice() {
console.log(price) // undefined (not 100!)
var price = 200
return price
}
getPrice() // logs undefined, returns 200230. What is Hoisting?
Theory
Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the compilation phase — before code execution.
| Declaration | Hoisted? | Initialized? |
|---|---|---|
var | ✅ Yes | As undefined |
let | ✅ Yes (TDZ) | ❌ Not until declaration line |
const | ✅ Yes (TDZ) | ❌ Not until declaration line |
function declaration | ✅ Yes | Fully initialized |
function expression (var fn = ...) | var only | undefined until assignment |
TDZ (Temporal Dead Zone): Accessing let/const before their declaration throws ReferenceError.
Pros & Cons
| Pros | Cons |
|---|---|
| Function declarations usable before definition | var hoisting causes subtle bugs |
| Flexible function organization | TDZ errors confuse beginners |
| Easy to accidentally reference uninitialized variables |
Real-Life Example
// var hoisting
console.log(city) // undefined (not ReferenceError)
var city = 'Bangalore'
// let hoisting + TDZ
// console.log(area); // ReferenceError — TDZ
let area = 'Koramangala'
// Function declaration — fully hoisted
greet('Amit') // works!
function greet(name) {
console.log(`Hello, ${name}`)
}
// Function expression — only var hoisted
// sayBye("Amit"); // TypeError: sayBye is not a function
var sayBye = function (name) {
console.log(`Bye, ${name}`)
}
// Classic interview trap
var x = 10
function foo() {
console.log(x) // undefined (not 10!)
var x = 20
}
foo()
// Equivalent to:
function foo() {
var x // hoisted
console.log(x) // undefined
x = 20
}// Real-life best practice — avoid hoisting bugs
// ✅ Use const/let (block scoped, TDZ catches errors early)
const API_URL = import.meta.env.VITE_API_URL
let retryCount = 0
// ✅ Declare functions before use OR use function declarations intentionally
function formatPrice(amount) {
return `₹${amount.toFixed(2)}`
}231. What is the difference between bind and apply in JavaScript?
Theory
Both call, apply, and bind let you control the this context of a function. They are methods on Function.prototype.
| Method | Invokes immediately? | Arguments | Returns |
|---|---|---|---|
call | ✅ Yes | Comma-separated | Function result |
apply | ✅ Yes | Array of arguments | Function result |
bind | ❌ No | Comma-separated (partial apply) | New function with bound this |
Pros & Cons
| bind | apply |
|---|---|
Creates reusable function with fixed this | Invokes immediately — one-time use |
| Supports partial application | Useful when args are already in an array |
| No immediate execution — defer call | Must know all args at call time |
Real-Life Example
const restaurant = {
name: 'Spice Garden',
getDetails(prefix) {
return `${prefix}: ${this.name}`
},
}
// apply — invoke immediately with array of args
restaurant.getDetails.call(restaurant, 'Welcome') // "Welcome: Spice Garden"
restaurant.getDetails.apply(restaurant, ['Welcome']) // same result
// apply shines with dynamic arrays
const args = ['Order from']
restaurant.getDetails.apply(restaurant, args) // "Order from: Spice Garden"
// bind — returns a new function, does NOT invoke
const getWelcome = restaurant.getDetails.bind(restaurant, 'Welcome')
getWelcome() // "Welcome: Spice Garden" — called later
// Real React use case — binding event handlers in class components
class SearchBar extends React.Component {
constructor(props) {
super(props)
this.handleSearch = this.handleSearch.bind(this) // ensure `this` is the component
}
handleSearch(query) {
this.props.onSearch(query) // `this` correctly refers to component instance
}
}
// Modern React — arrow functions or hooks eliminate bind need
function SearchBar({ onSearch }) {
const handleSearch = (query) => onSearch(query) // no bind needed
return <input onChange={(e) => handleSearch(e.target.value)} />
}Quick comparison
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const user = { name: 'Amit' }
greet.apply(user, ['Hello', '!']) // "Hello, Amit!" — called now, args as array
greet.call(user, 'Hello', '!') // "Hello, Amit!" — called now, args individually
const boundGreet = greet.bind(user, 'Hello')
boundGreet('!') // "Hello, Amit!" — called later, partial args232. What is the difference between synchronous and asynchronous iteration?
Synchronous iteration
Works with iterable objects (Array, Map, Set, strings).
const arr = [1, 2, 3]
// for...of
for (const item of arr) {
console.log(item)
}
// Iterator protocol
const iterator = arr[Symbol.iterator]()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }Asynchronous iteration
Works with async iterables — objects with [Symbol.asyncIterator]().
async function* asyncGenerator() {
yield await fetchData(1)
yield await fetchData(2)
yield await fetchData(3)
}
// for await...of
async function main() {
for await (const chunk of asyncGenerator()) {
console.log(chunk)
}
}Comparison
| Sync | Async | |
|---|---|---|
| Protocol | Symbol.iterator | Symbol.asyncIterator |
| Loop | for...of | for await...of |
| Return | { value, done } | Promise<{ value, done }> |
| Use case | In-memory data | Streams, paginated APIs, file reads |
Practical async iterable — paginated API
const paginatedFetch = {
async *[Symbol.asyncIterator]() {
let cursor = null
do {
const res = await fetch(`/api/items?cursor=${cursor ?? ''}`)
const { items, nextCursor } = await res.json()
for (const item of items) yield item
cursor = nextCursor
} while (cursor)
},
}
for await (const item of paginatedFetch) {
console.log(item)
}233. What is the Event Loop and how does the Microtask Queue work?
Theory
JavaScript is single-threaded — one call stack. The event loop coordinates execution between the call stack, macrotask queue (tasks), and microtask queue (jobs).
Execution order:
- Run all synchronous code (call stack)
- Drain all microtasks (promises,
queueMicrotask,MutationObserver) - Run one macrotask (
setTimeout,setInterval, I/O, UI events) - Repeat
Pros & Cons
| Pros | Cons |
|---|---|
| Simple concurrency model — no locks | Long sync code blocks everything |
| Microtasks enable fast promise resolution | Microtask starvation if queue never empties |
| Non-blocking I/O via callbacks | "Callback hell" without async/await |
Real-Life Example
console.log('1: sync')
setTimeout(() => console.log('4: macrotask'), 0)
Promise.resolve()
.then(() => console.log('3: microtask 1'))
.then(() => console.log('3b: microtask 2'))
queueMicrotask(() => console.log('3c: microtask 3'))
console.log('2: sync')
// Output: 1: sync → 2: sync → 3: microtask 1 → 3c: microtask 3 → 3b: microtask 2 → 4: macrotaskReal-life impact: If you await in a loop without yielding, you starve the UI. Batch work with setTimeout(0) or requestAnimationFrame for long computations.
234. What is the Event Loop?
Theory
JavaScript runs on a single thread — only one piece of code executes at a time on the call stack. The Event Loop is the mechanism that coordinates execution between:
- Call Stack — currently running synchronous code
- Web APIs — browser APIs (
setTimeout,fetch, DOM events) that run outside the main thread - Callback Queue (Macrotask Queue) — callbacks waiting to enter the call stack
- Microtask Queue — higher-priority jobs (Promises,
queueMicrotask)
Execution order:
- Run all synchronous code on the call stack
- Drain all microtasks completely
- Run one macrotask
- Repeat
This is why Promise.then runs before setTimeout, even when both are scheduled at the same time.
Pros & Cons
| Pros | Cons |
|---|---|
| Simple concurrency model — no thread locks | Long synchronous code blocks the entire UI |
| Non-blocking I/O via callbacks/promises | Easy to create microtask starvation |
| Predictable single-thread execution | "Callback hell" without async/await discipline |
Real-Life Example
console.log('1: User clicks button')
setTimeout(() => {
console.log('4: Analytics event sent')
}, 0)
fetch('/api/orders').then(() => console.log('3: Orders loaded'))
console.log('2: UI updated immediately')
// Output:
// 1: User clicks button
// 2: UI updated immediately
// 3: Orders loaded ← microtask (Promise)
// 4: Analytics event sent ← macrotask (setTimeout)// Real React impact — don't block the event loop
function HeavyDashboard({ data }) {
// ❌ Blocks UI for 2 seconds — user can't scroll or click
const processed = processMillionRows(data)
// ✅ Yield to event loop — UI stays responsive
const [processed, setProcessed] = useState([])
useEffect(() => {
const chunk = () => {
const result = processChunk(data, 0, 1000)
setProcessed((prev) => [...prev, ...result])
if (hasMore) requestIdleCallback(chunk)
}
chunk()
}, [data])
}Interview answer (concise)
235. Why a fintech app specifically
Theory
Generic "fintech is growing" answers fail. a fintech app wants specific motivation — product, design culture, tech challenges, user base.
Real-Life Example — Points to personalize
| Generic (avoid) | a fintech app-specific (use) |
|---|---|
| "Fintech is hot" | "a fintech app rewards responsible credit behavior — rare mission alignment" |
| "Good salary" | "a fintech app's design-first culture — consumer fintech UX at Indian scale" |
| "React job" | "Complex KYC, payments, real-time credit — frontend depth I want" |
| "Bangalore startup" | "Engineering blog / open source / craft standards I've followed" |
TypeScript
236. How does extends work in TypeScript and what is the difference between type and interface?
Theory
Both type and interface define object shapes in TypeScript. extends inherits properties from another type or interface.
interface Animal {
name: string
}
interface Dog extends Animal {
breed: string
}
// Dog = { name: string; breed: string }type vs interface — key differences
| Feature | interface | type |
|---|---|---|
| Object shapes | ✅ | ✅ |
extends / inheritance | ✅ extends | ✅ & intersection |
| Declaration merging | ✅ (same name merges) | ❌ (duplicate = error) |
| Primitives, unions, tuples | ❌ | ✅ |
| Computed properties | ❌ | ✅ |
implements in class | ✅ | ✅ (with object types) |
When to use which
Use interface | Use type |
|---|---|
| Object/class contracts | Unions: type Status = "active" | "inactive" |
| Public API surfaces (libraries) | Tuples: type Point = [number, number] |
| When you need declaration merging | Mapped types: type Readonly<T> = { readonly [K in keyof T]: T[K] } |
| React component props (convention) | Utility compositions |
Pros & Cons
| interface | type |
|---|---|
| Extendable, mergeable — good for libraries | More flexible — unions, primitives, computed |
| Clear OOP semantics | Cannot be reopened/merged |
| Slightly better error messages for objects | Required for advanced type operations |
Real-Life Example
// interface — component props (the company React project pattern)
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
onClick: () => void;
}
interface IconButtonProps extends ButtonProps {
icon: React.ReactNode;
iconPosition?: "left" | "right";
}
function IconButton({ icon, iconPosition = "left", label, ...rest }: IconButtonProps) {
return (
<button {...rest}>
{iconPosition === "left" && icon}
{label}
{iconPosition === "right" && icon}
</button>
);
}
// type — unions and API response states
type ApiState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
type User = { id: string; name: string; email: string };
type Admin = User & { permissions: string[] }; // intersection via type
// type — function signature
type Fetcher<T> = (url: string) => Promise<T>;
// interface merging — extending third-party types
interface Window {
analytics: { track: (event: string, data?: object) => void };
}
// Now window.analytics is typed everywhereextends with generics
interface PaginatedResponse<T> {
data: T[]
page: number
totalPages: number
}
interface RestaurantListResponse extends PaginatedResponse<Restaurant> {
city: string
}
// { data: Restaurant[]; page: number; totalPages: number; city: string }237. TypeScript in Large Codebases
Theory
TypeScript at scale is about boundaries, consistency, and maintainability — not sprinkling : any everywhere.
Key practices:
- Strict mode (
strict: true) - Shared types between API and UI
- Discriminated unions for state machines
- Generic components with constraints
- Branded types for IDs
- Zod/io-ts for runtime validation at API boundary
API Boundary Validation
import { z } from 'zod'
const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
price: z.number().positive(),
category: z.enum(['electronics', 'grocery', 'pharmacy']),
inStock: z.boolean(),
})
type Product = z.infer<typeof ProductSchema>
async function fetchProduct(id: string): Promise<Product> {
const res = await fetch(`/api/products/${id}`)
const json = await res.json()
return ProductSchema.parse(json) // runtime + compile-time safety
}Discriminated Unions — Async State
type AsyncState<T> =
{ status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }
function DataView<T>({ state }: { state: AsyncState<T> }) {
switch (state.status) {
case 'idle':
return null
case 'loading':
return <Spinner />
case 'error':
return <ErrorBanner error={state.error} />
case 'success':
return <Content data={state.data} />
}
}Generic Components
interface DataTableProps<T extends { id: string }> {
data: T[]
columns: Column<T>[]
onRowClick?: (row: T) => void
}
function DataTable<T extends { id: string }>({ data, columns, onRowClick }: DataTableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={col.key}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.id} onClick={() => onRowClick?.(row)}>
{columns.map((col) => (
<td key={col.key}>{col.render(row)}</td>
))}
</tr>
))}
</tbody>
</table>
)
}Branded Types — Prevent ID Mix-ups
type UserId = string & { readonly __brand: 'UserId' }
type OrderId = string & { readonly __brand: 'OrderId' }
function fetchOrders(userId: UserId): Promise<Order[]> {
/* ... */
}
const userId = 'abc' as UserId
fetchOrders(userId) // ✅
fetchOrders('abc') // ❌ Type error — plain string not assignableLarge Codebase Rules
| Rule | Why |
|---|---|
No any — use unknown + narrow | Prevents silent bugs |
satisfies over as | Preserves literal types |
| Colocate types with features | features/cart/types.ts |
| API types generated from OpenAPI | Single source of truth |
ESLint @typescript-eslint strict | Catch issues in CI |
238. Why do we use TypeScript?
Theory
TypeScript is a typed superset of JavaScript that compiles to plain JS. It adds static type checking at compile time, catching errors before runtime.
Why teams adopt TypeScript
- Catch bugs early — typos, wrong argument types, null access
- Better IDE support — autocomplete, refactoring, go-to-definition
- Self-documenting code — types serve as inline documentation
- Safer refactoring — rename a field, compiler finds all usages
- Team scalability — contracts between modules are explicit
Pros & Cons
| Pros | Cons |
|---|---|
| Fewer runtime type errors | Build step required |
| Excellent autocomplete | Learning curve for JS developers |
| Safer large codebases | Can be verbose (any abuse negates benefits) |
| Interfaces document API contracts | Slower initial development for small projects |
Real-Life Example
// JavaScript — bug discovered at runtime in production
function calculateDiscount(price, discountPercent) {
return price - (price * discountPercent) / 100
}
calculateDiscount('299', '10') // "299" - NaN → broken UI
// TypeScript — caught at compile time
function calculateDiscount(price: number, discountPercent: number): number {
return price - (price * discountPercent) / 100
}
calculateDiscount('299', '10') // ❌ Compile error: string not assignable to number
// Real React project — typed API response
interface Restaurant {
id: string
name: string
rating: number
cuisine: string[]
isOpen: boolean
}
async function fetchRestaurants(city: string): Promise<Restaurant[]> {
const res = await fetch(`/api/restaurants?city=${city}`)
return res.json()
}
// Autocomplete works: restaurant.rating, restaurant.cuisine
// Typo caught: restaurant.raiting → compile errorInterview answer (concise)
Next.js
239. You are migrating a React SPA to Next.js. What challenges would you expect and how would you handle them?
Description
SPAs rely on client-side routing, browser-only APIs, and often a single index.html. Next.js introduces file-based routing, Server Components, and different data-fetching semantics.
Theory
| SPA habit | Next.js change |
|---|---|
react-router | App Router file system (app/page.tsx) |
useEffect + fetch | Server Component async fetch or React Query in client islands |
window / localStorage on first render | 'use client' + useEffect, or dynamic ssr: false |
Env vars REACT_APP_* | NEXT_PUBLIC_* (client) vs server-only env |
Global CSS in index.js | app/layout.tsx imports |
| Auth in context only | Middleware + cookies for SSR session |
Pros & Cons
| Incremental migration (Next + existing SPA routes) | Big-bang rewrite |
|---|---|
| ✅ Ship value per route | ❌ Long freeze, high risk |
| ✅ Learn App Router gradually | ❌ Hard to test everything |
| ❌ Temporary dual patterns | ❌ Regression bugs |
Real Example — Incremental migration plan
Phase 1 — Shellnext.config.js → redirects old paths
app/layout.tsx → shared providers (only client wrappers where needed)
Migrate /marketing and /blog first (SSG wins)// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(req: NextRequest) {
const token = req.cookies.get('session')?.value
if (!token && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url))
}
return NextResponse.next()
}// ❌ SPA
;<Route path="/products/:id" element={<ProductPage />} />
// ✅ App Router
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return <ProductDetail product={product} />
}// Map, charts, rich text editor — mark client, dynamic import
'use client'
// or dynamic(() => import('./map'), { ssr: false })| Problem | Fix |
|---|---|
| Hydration mismatch | Don't use Date.now() or random IDs in SSR HTML |
| Double data fetch | Trust Server Components; don't refetch same data in useEffect |
localStorage in render | Move to useEffect or client-only component |
| SEO still missing | Ensure marketing pages are Server/SSG, not client-only |
240. You need to upload large files from the frontend to a cloud storage service. How would you implement the flow in Next.js?
Description
Uploading multi-GB files through your Next.js server wastes memory, hits body-size limits, and times out on serverless. The standard pattern is presigned URLs — client uploads directly to S3/R2/GCS.
Theory
| Flow | Pros | Cons |
|---|---|---|
| Direct to cloud (presigned) | Scalable, no server memory | Need CORS config on bucket |
| Through Next.js API | Simple for small files | Breaks on large files / serverless |
| Multipart upload | Resumable, parallel chunks | More client logic |
Steps: Client requests presigned URL → Next.js Route Handler validates auth + file metadata → returns signed PUT URL → client uploads directly → client notifies app to save metadata.
Pros & Cons
| Presigned direct upload | Proxy through API route |
|---|---|
| ✅ Handles large files | ✅ Full server control |
| ✅ No serverless timeout | ❌ 4.5 MB limits on many platforms |
| ❌ Must validate before signing | ❌ Server RAM bottleneck |
Real Example — S3 presigned upload (500 MB video)
// app/api/uploads/presign/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { NextRequest } from 'next/server'
const s3 = new S3Client({ region: process.env.AWS_REGION })
const MAX_SIZE = 500 * 1024 * 1024 // 500 MB
const ALLOWED_TYPES = ['video/mp4', 'application/pdf']
export async function POST(req: NextRequest) {
const session = await getSession(req)
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })
const { fileName, fileType, fileSize } = await req.json()
if (!ALLOWED_TYPES.includes(fileType) || fileSize > MAX_SIZE) {
return Response.json({ error: 'Invalid file' }, { status: 400 })
}
const key = `uploads/${session.userId}/${crypto.randomUUID()}-${fileName}`
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: fileType,
})
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 })
return Response.json({ uploadUrl, key })
}// components/file-uploader.tsx
'use client'
export function FileUploader() {
async function handleUpload(file: File) {
const presign = await fetch('/api/uploads/presign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
fileType: file.type,
fileSize: file.size,
}),
}).then((r) => r.json())
await fetch(presign.uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
})
await fetch('/api/uploads/complete', {
method: 'POST',
body: JSON.stringify({ key: presign.key, size: file.size }),
})
}
return <input type="file" onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])} />
}Large files: Use S3 multipart upload API from the client (or library like tus / @aws-sdk/lib-storage).
Testing & Tooling
241. How would you implement A/B testing in a Next.js application?
Description
A/B tests split users into variants to compare metrics (CTR, conversion). Implementation must assign users consistently (same variant on return visits) without flicker and without hurting SEO.
Theory
| Approach | How it works |
|---|---|
| Middleware + cookie | Assign variant on first visit; edge-fast |
| Feature flags (LaunchDarkly, Flagsmith) | Remote config, gradual rollouts |
| Vercel Flags / Edge Config | Low-latency variant lookup |
| Client-only | Simple but causes flicker — avoid for hero CTA tests |
Best practice: Assign variant in middleware, store in cookie, render correct variant on server.
Pros & Cons
| Server-side assignment (middleware) | Client-side random on mount |
|---|---|
| ✅ No flicker | ❌ Flash of wrong variant |
| ✅ SEO sees stable content | ❌ Skews analytics |
| ❌ Needs cookie consent in EU | ✅ Easier to prototype |
Real Example — Homepage CTA A/B test
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const VARIANTS = ['control', 'variant-b'] as const
function pickVariant(): string {
return VARIANTS[Math.floor(Math.random() * VARIANTS.length)]
}
export function middleware(req: NextRequest) {
const res = NextResponse.next()
let variant = req.cookies.get('ab-cta')?.value
if (!variant || !VARIANTS.includes(variant as (typeof VARIANTS)[number])) {
variant = pickVariant()
res.cookies.set('ab-cta', variant, {
maxAge: 60 * 60 * 24 * 30, // 30 days — consistent experience
path: '/',
})
}
res.headers.set('x-ab-cta', variant)
return res
}// app/page.tsx
import { cookies } from 'next/headers'
import { HeroCTA } from '@/components/hero-cta'
export default function HomePage() {
const variant = cookies().get('ab-cta')?.value ?? 'control'
return (
<main>
<h1>Welcome</h1>
<HeroCTA variant={variant} />
</main>
)
}// components/hero-cta.tsx
'use client'
import { trackEvent } from '@/lib/analytics'
export function HeroCTA({ variant }: { variant: string }) {
const isB = variant === 'variant-b'
function handleClick() {
trackEvent('cta_click', { variant, page: 'home' })
}
return (
<a href="/signup" onClick={handleClick} className={isB ? 'btn-green' : 'btn-blue'}>
{isB ? 'Start free trial' : 'Sign up'}
</a>
)
}Analytics: Send variant with every conversion event; analyze in Amplitude/Mixpanel/GA4.
import { getFlag } from '@/lib/flags'
export default async function PricingPage() {
const showAnnual = await getFlag('pricing-annual-default', false)
return <PricingTable defaultTab={showAnnual ? 'annual' : 'monthly'} />
}242. Testing — RTL, Jest, Playwright
Theory
Frontend testing pyramid:
| Layer | Tool | Tests |
|---|---|---|
| Unit | Jest / Vitest | Pure functions, reducers, utils |
| Component | React Testing Library (RTL) | User behavior, rendering |
| E2E | Playwright / Cypress | Full user flows in browser |
RTL philosophy: Test how users interact — query by role, label, text. Not implementation details.
Pros & Cons
| Tool | Best for | Avoid for |
|---|---|---|
| Jest | Unit tests, snapshots, mocks | Visual layout |
| RTL | Component behavior | E2E flows |
| Playwright | Cross-browser E2E, CI | Unit logic |
Real-Life Example
// Jest + RTL — component test
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import LoginForm from './LoginForm'
describe('LoginForm', () => {
it('shows error on invalid credentials', async () => {
const user = userEvent.setup()
render(<LoginForm onSubmit={mockSubmit} />)
await user.type(screen.getByLabelText(/email/i), 'wrong@test.com')
await user.type(screen.getByLabelText(/password/i), 'wrong')
await user.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/invalid/i)
})
})
})
// Jest — reducer unit test
describe('cartReducer', () => {
it('adds item to cart', () => {
const state = cartReducer(undefined, addItem({ id: '1', name: 'Biryani', price: 299 }))
expect(state.items).toHaveLength(1)
expect(state.items[0].name).toBe('Biryani')
})
})// Playwright — E2E test
import { test, expect } from '@playwright/test'
test('user can complete checkout', async ({ page }) => {
await page.goto('/products')
await page.getByRole('button', { name: 'Add to Cart' }).first().click()
await page.getByRole('link', { name: 'Cart' }).click()
await expect(page.getByText('1 item')).toBeVisible()
await page.getByRole('button', { name: 'Checkout' }).click()
await page.getByLabel('Card number').fill('4242424242424242')
await page.getByRole('button', { name: 'Pay' }).click()
await expect(page.getByText('Order confirmed')).toBeVisible()
})243. Webpack
Theory
Webpack is a module bundler that takes JavaScript modules (and assets) and bundles them for the browser. It supports code splitting, tree shaking, loaders (CSS, images), and plugins.
Key concepts: entry, output, loaders, plugins, chunks, HMR (Hot Module Replacement).
Modern alternatives: Vite (esbuild dev + Rollup prod), Turbopack (Next.js), Rollup, esbuild.
Pros & Cons
| Webpack | Vite |
|---|---|
| ✅ Mature, huge plugin ecosystem | ✅ Faster dev server (native ESM) |
| ✅ Highly configurable | ✅ Simpler config |
| ❌ Slow dev server on large apps | ❌ Less mature plugin ecosystem |
| ❌ Complex configuration | ✅ Better DX for new projects |
Real-Life Example
// webpack.config.js — simplified
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js', // cache busting
clean: true,
},
module: {
rules: [
{ test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ },
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
{ test: /\.(png|svg|jpg)$/, type: 'asset/resource' },
],
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new MiniCssExtractPlugin({ filename: '[name].[contenthash].css' }),
],
optimization: {
splitChunks: { chunks: 'all' },
runtimeChunk: 'single',
},
devServer: { hot: true, port: 3000 },
}244. What is Snapshot Testing?
Theory
Snapshot testing captures the rendered output of a component (or serialized data) and saves it as a snapshot file. On future test runs, the output is compared against the saved snapshot. If they differ, the test fails — you either fix a bug or update the snapshot intentionally.
Common tools: Jest (toMatchSnapshot), Vitest, React Testing Library.
Snapshots can capture:
- React component HTML/JSON tree
- Serialized objects
- Error messages
Pros & Cons
| Pros | Cons |
|---|---|
| ✅ Fast to write — good for regression | ❌ Developers blindly update snapshots |
| ✅ Catches unintended UI changes | ❌ Large diffs hard to review |
| ✅ Good for stable, presentational components | ❌ Not a substitute for behavior tests |
| ✅ Documents component output over time | ❌ Fragile if snapshots include dynamic data (dates, IDs) |
Real-Life Example
// EmployeeCard.tsx
function EmployeeCard({ name, role, department }) {
return (
<div className="employee-card">
<h3>{name}</h3>
<p>{role}</p>
<span className="dept-badge">{department}</span>
</div>
)
}
// EmployeeCard.test.tsx
import { render } from '@testing-library/react'
import EmployeeCard from './EmployeeCard'
describe('EmployeeCard', () => {
it('matches snapshot', () => {
const { container } = render(<EmployeeCard name="Priya Sharma" role="Senior Developer" department="Engineering" />)
expect(container.firstChild).toMatchSnapshot()
})
})// First run creates: __snapshots__/EmployeeCard.test.tsx.snap
exports[`EmployeeCard matches snapshot 1`] = `
<div class="employee-card">
<h3>Priya Sharma</h3>
<p>Senior Developer</p>
<span class="dept-badge">Engineering</span>
</div>
`Best practices:
- Use snapshots for stable presentational components
- Combine with behavior tests (clicks, form submission)
- Mock dates/IDs to avoid flaky snapshots
- Review snapshot diffs carefully in PRs — don't auto-accept blindly
// Better — snapshot + behavior test together
it('calls onSelect when clicked', async () => {
const onSelect = jest.fn()
render(<EmployeeCard name="Amit" onSelect={onSelect} />)
await userEvent.click(screen.getByRole('button', { name: /select/i }))
expect(onSelect).toHaveBeenCalledWith('Amit')
})Behavioral & Soft Skills
245. Culture Fit & Behavioral Questions
Theory
a quick-commerce app's culture round assesses how you work, not what you know. They evaluate problem-solving approach, ownership, collaboration, and fit for a fast-paced consumer product (10-minute delivery).
Use the STAR method: Situation → Task → Action → Result.
Previous Projects — How to Answer
Structure your answer:
- What — product/feature in one sentence
- Your role — what you specifically built
- Tech stack — React, state management, APIs
- Impact — metrics if possible (load time, conversion, bug reduction)
- Challenge — one hard problem you solved
Example answer:
Problem Solving Approach
Interviewers want your process, not just the answer:
1. Clarify requirements → "Should toggle affect neighbors?"
2. Break down the problem → State, UI, events separately
3. Start simple → Basic toggle before row/column
4. Test edge cases → Empty grid, all selected, rapid clicks
5. Iterate → Add a11y, reset, count after MVP worksExample answer:
Engineering Mindset
Traits a quick-commerce app likely values:
| Trait | Example to mention |
|---|---|
| Ownership | "I didn't wait for QA — I added unit tests for edge cases" |
| User-first | "I optimized skeleton loaders because users on 3G saw blank screens" |
| Pragmatism | "Shipped MVP with long polling, migrated to WebSocket in v2" |
| Learning | "I hadn't used IndexedDB — I read docs, built a POC in a day" |
| Performance | "Virtualized a 500-item product list — scroll went from janky to 60fps" |
Team Collaboration
Example questions & answers:
"Tell me about a disagreement with a teammate." "How do you handle tight deadlines?" "How do you code review?"Questions to Ask Them
Shows genuine interest:
- "How does the frontend team handle real-time inventory updates across warehouses?"
- "What's the balance between web and mobile web at a quick-commerce app?"
- "How do you measure frontend performance in production?"
- "What does the on-call rotation look like for frontend engineers?"
| Round | Topic | One-liner |
|---|---|---|
| 1 | CORS | Server must allow cross-origin; fix on backend |
| 1 | WebSockets | Persistent duplex — live orders, wss + reconnect |
| 1 | Long polling vs WS | Polling = held HTTP; WS = true real-time |
| 1 | Storage | local = persistent; session = tab; cookies = server; IDB = large |
| 1 | Event loop | Sync → all microtasks → one macrotask |
| 2 | Grid toggle | 2D boolean array, immutable map toggle |
| 3 | Culture | STAR stories, process, collaboration examples |
246. Describe a disagreement within your team and how you resolved it
What interviewers assess
- Empathy and listening
- Data-driven decisions
- Willingness to disagree and commit
Sample answer skeleton
247. How did you handle a critical production issue?
What interviewers assess
- Calm under pressure
- Systematic debugging
- Communication with stakeholders
- Post-incident learning (blameless)
Sample answer skeleton
248. How do you mentor junior developers and review code?
Mentoring pillars
- Pair programming on real tickets, not toy examples
- Graduated ownership — start with small PRs, grow to feature ownership
- Review their reviews — teach them what to look for
- Document patterns — ADRs, internal wiki, code examples
- Psychological safety — questions are encouraged; mistakes are learning opportunities
Code review checklist I share
□ Does it solve the right problem?
□ Is the abstraction appropriate (not over-engineered)?
□ Edge cases: empty, error, loading states
□ Accessibility: keyboard, ARIA, contrast
□ Performance: unnecessary re-renders, large bundles
□ Tests for critical paths
□ Security: XSS, exposed secrets, auth checksSample answer skeleton
| Topic | One-liner |
|---|---|
var / let / const | Block scope + TDZ for let/const; prefer const |
| Closures | Function + lexical env; enables privacy, memoization, debounce |
| Custom fetch hook | Loading/error/data + AbortController + cleanup |
| Reconciliation | Fiber diff → minimal DOM updates; render vs commit phase |
| Keys | Stable identity for list items; avoid index on mutable lists |
| Architecture | Feature folders, design system, service layer, monorepo |
| RSC | Server-only components; zero client JS; direct data access |
| Infinite scroll | Intersection Observer + cursor pagination + virtualization |
| Web Vitals | LCP (load), INP (responsive), CLS (stable layout) |
| State | Query for server, Zustand/Redux for global, URL for shareable |
| Caching | HTTP headers, CDN, React Query, Service Worker |
| Scale | CDN, SSR, code split, observability, feature flags, resilience |
| Behavioral | STAR method, quantify impact, show leadership + humility |
249. Tell us about a challenging project you worked on
Framework
| STAR | What to include |
|---|---|
| Situation | Business context, scale, constraints |
| Task | Your specific responsibility |
| Action | Technical decisions, trade-offs, collaboration |
| Result | Metrics: performance, revenue, reliability, team velocity |
Sample answer skeleton
General & Advanced
250. == vs ===
Theory
==— loose equality with type coercion===— strict equality, no coercion
Always prefer === in production. Use == null to check both null and undefined.
Coercion surprises
0 == false // true
'' == false // true
null == undefined // true
;[] == false // true
'5' == 5 // true
0 === false // false
null === undefined // falseReal Example
function validateAmount(input) {
// ❌ "0" == 0 is true — wrong for payment validation
if (input == 0) return 'Invalid amount'
// ✅ Strict check
if (input === 0 || input === '0') return 'Invalid amount'
if (Number(input) <= 0) return 'Invalid amount'
}251. A decision you regretted
Theory
Tests humility, learning, and judgment. Structure: decision → why it seemed right → what went wrong → what you do differently now.
Real-Life Example — Answer skeleton
a fintech app tip: Defensiveness ends interviews. Acknowledge feedback, thank them, show what changed.
252. Accessibility (a11y)
Theory
Accessibility ensures apps are usable by everyone — screen reader users, keyboard-only users, people with motor/cognitive disabilities. Standard: WCAG 2.1 AA.
Principles (POUR): Perceivable, Operable, Understandable, Robust.
Pros & Cons
| Accessible apps | Ignoring a11y |
|---|---|
| ✅ 15%+ larger audience | ❌ Legal liability |
| ✅ Better SEO | ❌ Fails enterprise RFPs |
| ✅ Better UX for all | ❌ Lighthouse score penalty |
Real-Life Example
// Semantic, keyboard-accessible form
<form onSubmit={handleSubmit} aria-labelledby="form-title">
<h2 id="form-title">Contact Us</h2>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
required
aria-describedby="email-error"
aria-invalid={!!errors.email}
/>
{errors.email && (
<p id="email-error" role="alert">{errors.email}</p>
)}
<button type="submit">Send</button>
</form>
// Skip link
<a href="#main" className="sr-only focus:not-sr-only">Skip to content</a>
// Live region for dynamic updates
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>253. Arrow Functions
Theory
Arrow functions (() => {}) are a concise function syntax introduced in ES6. Key differences from regular functions:
| Feature | Regular function | Arrow function |
|---|---|---|
this | Dynamic — depends on caller | Lexical — inherited from enclosing scope |
arguments | Available | Not available — use rest ...args |
new / constructor | Can be used with new | Cannot |
prototype | Has .prototype | No |
| Syntax | Verbose | Concise, implicit return |
Pros & Cons
| Pros | Cons |
|---|---|
| Concise syntax | No this binding — wrong for object methods |
| Implicit return for expressions | Can't be used as constructors |
| Great for callbacks and array methods | No arguments object |
Lexical this fixes callback bugs | Not ideal for prototype methods |
Real-Life Example
// Concise syntax
const double = (x) => x * 2;
const sum = (a, b) => a + b;
const getUser = (id) => ({ id, name: "Amit" }); // implicit return object
// Array methods — arrow functions shine
const prices = [299, 49, 79, 150];
const withTax = prices.map((p) => p * 1.05);
const affordable = prices.filter((p) => p < 100);
const total = prices.reduce((sum, p) => sum + p, 0);
// Lexical this — fixes callback bug
const store = {
products: ["Biryani", "Naan"],
printAll() {
// ❌ Regular function loses `this`
this.products.forEach(function (p) {
// console.log(this.category, p);
});
// ✅ Arrow inherits `this`
this.products.forEach((p) => {
console.log(this.products.length, p); // works
});
},
};
// When NOT to use arrow functions
const obj = {
name: "Test",
regular() { return this.name; }, // "Test"
arrow: () => this?.name, // undefined (lexical this)
};
// React event handlers — both work in function components
<button onClick={() => handleClick(id)}>Click</button>
<button onClick={handleClick}>Click</button>254. Authentication vs Authorization
Theory
- Authentication (AuthN) — Who are you? Verifying identity (login, OAuth, JWT, biometrics).
- Authorization (AuthZ) — What can you do? Checking permissions after identity is known (roles, ACLs, policies).
Authentication always comes before authorization. You can't authorize an unknown user.
Pros & Cons
| Authentication | Authorization | |
|---|---|---|
| Handles | Identity verification | Permission checking |
| Examples | Login, SSO, MFA | RBAC, ACL, policy engine |
| Frontend role | Login UI, token storage | Route guards, conditional UI |
| Backend role | Validate credentials | Enforce access rules |
Real-Life Example
// Authentication — login flow
async function login(email: string, password: string) {
const res = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
// Server sets httpOnly cookie — frontend never sees raw token
return res.ok
}
// Authorization — role-based UI (UX only — backend MUST enforce)
function AdminPanel() {
const { user } = useAuth() // authenticated user
// Authorization check
if (!user?.roles.includes('admin')) {
return <Navigate to="/unauthorized" />
}
return <AdminDashboard />
}
// Backend always validates — frontend guards are UX only
// DELETE /api/users/99 → 403 Forbidden if not admin
// regardless of what frontend shows| Layer | Authentication | Authorization |
|---|---|---|
| Frontend | Login form, session check | Hide/show buttons, route guards |
| Backend | Validate JWT/session | Check role on every API call |
| Token | JWT with sub (user ID) | JWT with roles/permissions claims |
255. Auto Increment/Decrement Counter
Theory
A counter is the classic useState exercise. Requirements:
- Click Increment → count increases by 1
- Click Decrement → count decreases by 1
- Use functional updaters
(prev) => prev + 1to avoid stale closure bugs - Optional senior touches: min/max bounds, disable buttons at limits, accessibility
Basic Implementation
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
const increment = () => setCount((prev) => prev + 1)
const decrement = () => setCount((prev) => prev - 1)
return (
<div>
<h2>Count: {count}</h2>
<button onClick={decrement}>Decrement</button>
<button onClick={increment}>Increment</button>
</div>
)
}Senior-Level Implementation
import { useState, useCallback } from 'react'
function Counter({ initial = 0, min = -Infinity, max = Infinity, step = 1 }) {
const [count, setCount] = useState(initial)
const increment = useCallback(() => {
setCount((prev) => Math.min(prev + step, max))
}, [step, max])
const decrement = useCallback(() => {
setCount((prev) => Math.max(prev - step, min))
}, [step, min])
const reset = useCallback(() => setCount(initial), [initial])
return (
<div role="group" aria-label="Counter">
<output aria-live="polite" aria-atomic="true">
{count}
</output>
<button onClick={decrement} disabled={count <= min} aria-label="Decrement">
−
</button>
<button onClick={increment} disabled={count >= max} aria-label="Increment">
+
</button>
<button onClick={reset} aria-label="Reset counter">
Reset
</button>
</div>
)
}
// Usage
;<Counter initial={0} min={0} max={10} step={1} />useReducer Variant (if interviewer asks)
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
case 'RESET':
return action.payload
default:
return state
}
}
function Counter() {
const [count, dispatch] = useReducer(counterReducer, 0)
return (
<>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>−</button>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
</>
)
}256. Batching in React
Theory
Batching groups multiple state updates into a single re-render for performance.
React 18 automatic batching: All updates batch — inside events, timeouts, promises, native handlers.
flushSync() opts out — forces synchronous render.
Real Example
function Counter() {
const [a, setA] = useState(0)
const [b, setB] = useState(0)
const handleClick = () => {
setA((x) => x + 1)
setB((x) => x + 1)
// React 18: ONE re-render, not two
}
// Also batched in async
setTimeout(() => {
setA(1)
setB(2)
}, 1000) // single re-render
}import { flushSync } from 'react-dom'
flushSync(() => setA(1)) // renders immediately
setB(2) // separate render257. Best Practices in React
Theory
Production React follows consistent patterns for maintainability, performance, and reliability.
Core practices checklist
| Practice | Why |
|---|---|
const by default, immutable state updates | Predictable renders |
| Colocate state, lift only when needed | Less re-renders |
| Custom hooks for shared logic | DRY |
| Keys from stable IDs | List performance + state |
| Error Boundaries per route/feature | Fault isolation |
| Code split by route | Faster initial load |
| Profile before memoizing | Avoid premature optimization |
| Separate UI from business logic | Testability |
| TypeScript for large apps | Catch bugs early |
| Cleanup in useEffect | No memory leaks |
Real Example — folder structure
src/
├── features/
│ └── checkout/
│ ├── components/ # UI only
│ ├── hooks/ # useCheckout
│ ├── api/ # checkoutService
│ └── types.ts
├── shared/
│ └── components/ # Button, Modal
└── app/
└── router.tsx| # | Topic | One-Line Answer |
|---|---|---|
| 1 | React | Component-based UI library; state drives view |
| 2 | JSX | HTML-like syntax compiled to createElement |
| 3 | Components | Reusable UI building blocks |
| 4 | Functional vs Class | Functions + hooks = modern standard |
| 5 | Props | Read-only inputs from parent |
| 6 | State | Mutable internal data; triggers re-render |
| 7 | State vs Props | Internal vs external; mutable vs read-only |
| 8 | Virtual DOM | JS tree + diff → minimal DOM updates |
| 9 | React Router | URL → component, no page reload |
| 10 | Conditional rendering | Ternary, &&, early return |
| 11 | useEffect | Side effects after render + cleanup |
| 12 | Dependency array | Controls when effect re-runs |
| 13 | useState | Local state + setter |
| 14 | useContext | Read context without drilling |
| 15 | Context API | Share theme/auth across tree |
| 16 | Custom hooks | Reusable stateful logic |
| 17 | Keys | Stable IDs for list identity |
| 18 | React.memo | Skip render if props equal |
| 19 | useRef | Mutable ref, no re-render |
| 20 | useRef vs useState | No re-render vs triggers re-render |
| 21 | Code splitting | Bundle chunks on demand |
| 22 | Lazy loading | Defer until needed |
| 23 | Suspense | Fallback while loading |
| 24 | Error Boundaries | Catch render errors, show fallback |
| 25 | Controlled vs Uncontrolled | State-driven input vs DOM ref |
| 26 | HOC | Wrap component for reuse; prefer hooks |
| 27 | Prop drilling | Fix with Context/composition |
| 28 | Reconciliation | Diff + minimal DOM patch |
| 29 | Strict Mode | Dev double-invoke for bug detection |
| 30 | Best practices | Small components, cleanup, split, profile |
How to Answer in Interviews
Use this 3-step pattern for any React question:
- One sentence definition — what it is
- One sentence why it matters — when you'd use it
- One real example — from a project or simple code
Example for useEffect:
Don't memorize paragraphs. Understand the concept, then explain it simply with one example. That is what interviewers remember.
258. Bridging Fundamentals and Real Engineering
The Disconnect Is Real
Interviewers ask flatten arrays. You ship Web Vitals budgets. Both can coexist — the trick is answering the puzzle quickly, then pivoting to depth.
The "Answer + Pivot" Technique
Interviewer: "How do you flatten an array?"
You:
You answered the puzzle in 20 seconds and showed senior thinking in 10 more.
When They Ask "What Do You Actually Work On?"
Have 3 prepared stories:
| Story | Demonstrates |
|---|---|
| Performance win | LCP/INP improvement with before/after metrics |
| State/architecture decision | Why Zustand vs Redux, TanStack Query adoption |
| Production incident | How you debugged and fixed a real bug |
Questions to Ask Interviewers
Surface whether the role matches real engineering:
- "How do you measure Core Web Vitals in production?"
- "What's your approach to server state — Redux, React Query, or both?"
- "What does your frontend architecture look like at scale?"
- "How do frontend engineers participate in on-call or incident response?"
Their answers tell you if the team cares about what you've actually been doing for 5 years.
Quick Map — Puzzle → Real Skill
| Interview puzzle | Real-world parallel |
|---|---|
| Flatten array | Nested API/CMS tree traversal |
| Debounce | Search input, auto-save |
| Event loop | Async UI update ordering |
| Deep clone | Immutable state updates |
| Two sum | Finding duplicate cart items, matching IDs |
| LRU cache | TanStack Query cache eviction |
| Topic | One-liner |
|---|---|
| Flatten array | flat(Infinity), recursive, or stack iterative |
| LCP | Fast meaningful content — images, fonts, critical JS |
| CLS | Reserve space — dimensions, skeletons, font-display |
| INP | Responsive input — defer heavy work, startTransition |
| State at scale | Query for server, Zustand/Redux for global, useState local |
| React Query | Hierarchical keys, staleTime, invalidate on mutation |
| TypeScript | Strict, Zod at boundary, discriminated unions |
| Monitoring | web-vitals → analytics, Sentry, Lighthouse CI |
| Architecture | Feature folders, API layer, import boundaries |
| Debugging | RUM → reproduce → Profile → fix → verify |
Fundamentals get you through the screen. Real engineering skills — Web Vitals, state architecture, production debugging — get you the offer and the job. Prepare for both.
259. Browser Storage
Theory
| Storage | Capacity | Lifetime | Scope | Sent to server? |
|---|---|---|---|---|
| localStorage | ~5–10 MB | Until cleared | Per origin | No |
| sessionStorage | ~5–10 MB | Tab session | Per tab | No |
| Cookies | ~4 KB | Configurable expiry | Per domain | Yes (auto) |
| IndexedDB | Large (50MB+) | Until cleared | Per origin | No |
Pros & Cons
| Storage | Best for | Avoid for |
|---|---|---|
| localStorage | Theme, preferences, recent searches | Auth tokens (XSS risk) |
| sessionStorage | Form wizard state, tab-specific data | Cross-tab sync |
| Cookies | Session ID, httpOnly auth | Large data |
| IndexedDB | Offline cart, large cache, images | Simple key-value |
Real Example
// localStorage — user preferences (non-sensitive)
localStorage.setItem('theme', 'dark')
localStorage.setItem('recentSearches', JSON.stringify(['milk', 'bread']))
// sessionStorage — checkout flow (tab-scoped)
sessionStorage.setItem('checkoutStep', '2')
sessionStorage.setItem('deliveryAddress', JSON.stringify(address))
// Cookies — set by server (httpOnly, Secure, SameSite)
// Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=86400
// IndexedDB — offline cart
const dbRequest = indexedDB.open('AppCart', 1)
dbRequest.onupgradeneeded = (e) => {
const db = e.target.result
db.createObjectStore('cart', { keyPath: 'productId' })
}React Hook — localStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setStoredValue = (newValue) => {
const next = newValue instanceof Function ? newValue(value) : newValue
setValue(next)
localStorage.setItem(key, JSON.stringify(next))
}
return [value, setStoredValue]
}Security Note (Interview Gold)
// ❌ NEVER — XSS can steal tokens
localStorage.setItem('jwt', token)
// ✅ Auth tokens in httpOnly cookies — JS cannot read them
// Server: Set-Cookie: token=...; HttpOnly; Secure; SameSite=Strict260. Build a traffic light system in React
Theory
This is a classic state + timing interview problem. Requirements:
- Cycle: Red → Green → Yellow (repeat)
- Durations: Red = 5s, Green = 3s, Yellow = 2s
- Use
useStatefor current light - Use
useEffect+setTimeoutfor timing - Cleanup timeout on unmount or state change to prevent memory leaks
Pros & Cons of approaches
| setTimeout in useEffect | setInterval |
|---|---|
| ✅ Precise per-state duration | ❌ Fixed interval — awkward for different durations |
| ✅ Easy cleanup | ❌ Drift over time |
| ✅ Clear state machine |
Real-Life Example — Full solution
import { useEffect, useState } from 'react'
type Light = 'red' | 'green' | 'yellow'
const LIGHT_CONFIG: Record<Light, { duration: number; next: Light; label: string }> = {
red: { duration: 5000, next: 'green', label: 'STOP' },
green: { duration: 3000, next: 'yellow', label: 'GO' },
yellow: { duration: 2000, next: 'red', label: 'SLOW' },
}
function TrafficLight() {
const [activeLight, setActiveLight] = useState<Light>('red')
const [countdown, setCountdown] = useState(LIGHT_CONFIG.red.duration / 1000)
useEffect(() => {
const { duration, next } = LIGHT_CONFIG[activeLight]
const seconds = duration / 1000
setCountdown(seconds)
// Countdown ticker (updates every second)
const countdownInterval = setInterval(() => {
setCountdown((prev) => (prev > 1 ? prev - 1 : seconds))
}, 1000)
// Switch to next light after duration
const switchTimer = setTimeout(() => {
setActiveLight(next)
}, duration)
// Cleanup — critical for interview points
return () => {
clearTimeout(switchTimer)
clearInterval(countdownInterval)
}
}, [activeLight])
return (
<div className="traffic-light-container">
<h2>Traffic Light</h2>
<div className="traffic-light" role="status" aria-live="polite">
<LightBulb color="red" isActive={activeLight === 'red'} />
<LightBulb color="yellow" isActive={activeLight === 'yellow'} />
<LightBulb color="green" isActive={activeLight === 'green'} />
</div>
<p className="status">
{LIGHT_CONFIG[activeLight].label} — {countdown}s remaining
</p>
</div>
)
}
function LightBulb({ color, isActive }: { color: Light; isActive: boolean }) {
return (
<div
className={`bulb bulb--${color} ${isActive ? 'bulb--active' : ''}`}
aria-label={`${color} light ${isActive ? 'on' : 'off'}`}
/>
)
}
export default TrafficLight.traffic-light-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
font-family: system-ui, sans-serif;
}
.traffic-light {
display: flex;
flex-direction: column;
gap: 12px;
border-radius: 12px;
background: #222;
padding: 16px;
}
.bulb {
opacity: 0.2;
transition:
opacity 0.3s,
box-shadow 0.3s;
border-radius: 50%;
width: 60px;
height: 60px;
}
.bulb--red {
background: #ff4444;
}
.bulb--yellow {
background: #ffcc00;
}
.bulb--green {
background: #44cc44;
}
.bulb--active {
opacity: 1;
box-shadow: 0 0 20px currentColor;
}
.bulb--red.bulb--active {
box-shadow: 0 0 24px #ff4444;
}
.bulb--yellow.bulb--active {
box-shadow: 0 0 24px #ffcc00;
}
.bulb--green.bulb--active {
box-shadow: 0 0 24px #44cc44;
}
.status {
font-weight: 600;
font-size: 1.25rem;
}Alternative — state machine with single effect (cleaner)
import { useEffect, useReducer } from 'react'
type Light = 'red' | 'green' | 'yellow'
const SEQUENCE: { light: Light; duration: number }[] = [
{ light: 'red', duration: 5000 },
{ light: 'green', duration: 3000 },
{ light: 'yellow', duration: 2000 },
]
function trafficReducer(state: number) {
return (state + 1) % SEQUENCE.length
}
function TrafficLightMachine() {
const [index, dispatch] = useReducer(trafficReducer, 0)
const { light, duration } = SEQUENCE[index]
useEffect(() => {
const timer = setTimeout(() => dispatch(), duration)
return () => clearTimeout(timer)
}, [index, duration])
const colors: Light[] = ['red', 'green', 'yellow']
return (
<div className="traffic-light">
{colors.map((c) => (
<div key={c} className={`bulb bulb--${c} ${light === c ? 'bulb--active' : ''}`} />
))}
<p>
Current: {light.toUpperCase()} ({duration / 1000}s)
</p>
</div>
)
}What interviewers look for
| Checkpoint | Why |
|---|---|
| Correct cycle order | Red → Green → Yellow |
| Correct timings | 5s, 3s, 2s |
useEffect cleanup | clearTimeout on unmount |
| Dependency array | [activeLight] triggers next cycle |
| Accessible markup | aria-live, aria-label on bulbs |
| No memory leaks | Don't stack timers without cleanup |
Cycle diagram
┌──────────────────────────────────────┐
│ │
▼ │
┌──────┐ 5s ┌───────┐ 3s ┌────────┐ │
│ RED │ ───► │ GREEN │ ───► │ YELLOW │─┘
└──────┘ └───────┘ └────────┘
2s261. Caching (Client + Server)
Theory
Caching stores responses to avoid redundant network requests and computation.
Client-side layers:
- HTTP cache — browser cache via
Cache-Controlheaders - Service Worker — offline cache (PWA)
- React Query / SWR — in-memory cache with stale-while-revalidate
- localStorage / IndexedDB — persistent client cache
Server-side layers:
- CDN — edge cache for static assets and SSR pages
- Redis / Memcached — API response cache
- ISR — regenerate static pages on interval
Pros & Cons
| Aggressive caching | No caching |
|---|---|
| ✅ Fast repeat visits | ❌ Every request hits server |
| ✅ Lower server load | ❌ Poor UX on slow networks |
| ❌ Stale data risk | ❌ Higher CDN/API costs |
| ❌ Cache invalidation complexity |
Real-Life Example
// HTTP cache headers (server)
// Static assets — immutable
Cache-Control: public, max-age=31536000, immutable
// API — short TTL
Cache-Control: private, max-age=60, stale-while-revalidate=30// React Query — client cache
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // fresh for 5 min
gcTime: 30 * 60 * 1000, // garbage collect after 30 min
refetchOnWindowFocus: true,
},
},
})
// Prefetch on hover
function ProductLink({ id }) {
const qc = useQueryClient()
return (
<Link
to={`/product/${id}`}
onMouseEnter={() =>
qc.prefetchQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
})
}>
View Product
</Link>
)
}// Service Worker — stale-while-revalidate
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.open('api-v1').then((cache) =>
cache.match(event.request).then((cached) => {
const network = fetch(event.request).then((res) => {
cache.put(event.request, res.clone())
return res
})
return cached || network
}),
),
)
}
})262. Can We Use Hooks inside Conditions?
Theory
No. Hooks must be called at the top level of a React function — never
inside if, for, loops, or nested functions.
Why: React stores hooks in a linked list on the Fiber node, indexed by call order. Conditional hooks change the order between renders, causing state to map to the wrong hook.
Real Example
// ❌ WRONG
function Bad({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null) // breaks rules!
}
}
// ✅ CORRECT — hook always called, condition inside
function Good({ isLoggedIn }) {
const [user, setUser] = useState(null)
useEffect(() => {
if (isLoggedIn) {
fetchUser().then(setUser)
} else {
setUser(null)
}
}, [isLoggedIn])
}
// ✅ CORRECT — early return AFTER all hooks
function Dashboard({ isLoggedIn }) {
const [data, setData] = useState(null)
useEffect(() => {
if (isLoggedIn) loadData()
}, [isLoggedIn])
if (!isLoggedIn) return <Login /> // OK — after hooks
return <div>{data}</div>
}263. Class Lifecycle Methods
Theory
| Phase | Methods |
|---|---|
| Mount | constructor, render, componentDidMount |
| Update | render, componentDidUpdate |
| Unmount | componentWillUnmount |
| Error | componentDidCatch, getDerivedStateFromError |
Deprecated: componentWillMount, componentWillReceiveProps, componentWillUpdate.
Real Example
class UserList extends React.Component {
componentDidMount() {
this.fetchUsers()
}
componentDidUpdate(prevProps) {
if (prevProps.teamId !== this.props.teamId) this.fetchUsers()
}
componentWillUnmount() {
this.abortController?.abort()
}
componentDidCatch(error) {
this.setState({ error })
}
}264. Coding: Find the first non-repeated character
Theory
Find the first character in a string that appears exactly once. Two-pass approach:
- Count frequency of each character
- Iterate string left-to-right, return first char with count === 1
Time: O(n), Space: O(k) where k = unique characters.
Pros & Cons
| Hash map (Map) | Array (ASCII) |
|---|---|
| ✅ Works with Unicode | ✅ O(1) space for limited charset |
| ✅ Handles any character | ❌ Limited to known charset size |
| Slightly more memory | Faster for English-only |
Real-Life Example
Input: "I love coding"
function firstNonRepeated(str) {
const freq = new Map()
for (const char of str) {
freq.set(char, (freq.get(char) || 0) + 1)
}
for (const char of str) {
if (freq.get(char) === 1) return char
}
return null
}
console.log(firstNonRepeated('I love coding')) // "I"Walk-through:
Char frequencies:
" " → 1
"I" → 1 ← first non-repeated (index 0)
"l" → 1 (but "I" comes first)
"o" → 2
"v" → 1
"e" → 2
"c" → 1
"d" → 1
"i" → 1
"n" → 1
"g" → 1
Scan left to right: "I" at index 0 has freq 1 → return "I"// Case-sensitive variant
firstNonRepeated('I love coding') // "I"
firstNonRepeated('i love coding') // "i"
// Ignore spaces (if interviewer asks)
function firstNonRepeatedNoSpaces(str) {
const cleaned = str.replace(/\s/g, '')
const freq = new Map()
for (const char of cleaned) freq.set(char, (freq.get(char) || 0) + 1)
for (const char of cleaned) if (freq.get(char) === 1) return char
return null
}
firstNonRepeatedNoSpaces('I love coding') // "I" (still first unique)265. Common JavaScript Gotchas
Theory
JavaScript has quirks from its design history. Interviewers test whether you know the traps — not to trick you, but to verify deep understanding.
Top gotchas
Real-Life Example
1. typeof null === "object"
typeof null // "object" — historical bug
null === null // true — use this instead2. Floating point precision
0.1 + 0.2 === 0.3 // false (0.30000000000000004)
// Fix: Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON
// Or: use integers (paise not rupees)
const totalPaise = 10 + 20 // 30 paise
const total = totalPaise / 100 // 0.33. == vs ===
;[] == false // true
;[] === false // false
// Always use ===4. Closure in loop with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0) // 3, 3, 3
}
// Fix: use let5. this in callbacks
const obj = {
name: 'Amit',
getName() {
return this.name
},
}
const fn = obj.getName
fn() // undefined — lost context
// Fix: fn = obj.getName.bind(obj) or arrow in class field6. Array holes
const arr = [1, , 3]
arr.map((x) => x * 2) // [2, empty, 6] — skips empty slot
arr.length // 37. parseInt without radix
parseInt('08') // 8 (modern) but was octal in old ES3
parseInt('08', 10) // always safe8. JSON.stringify limitations
JSON.stringify({ a: undefined, b: function () {} })
// '{"b":{}}' — undefined and functions omitted
JSON.stringify({ date: new Date() })
// '{"date":"2026-03-15T..."}' — Date becomes string9. NaN is not equal to itself
NaN === NaN // false
Number.isNaN(NaN) // true
isNaN('hello') // true (coerces!)
Number.isNaN('hello') // false10. Sort mutates and is lexicographic by default
;[10, 2, 1].sort() // [1, 10, 2] — string sort!
;[10, 2, 1].sort((a, b) => a - b) // [1, 2, 10]266. Conditional Rendering
Theory
Render different UI based on state or props using:
if/elsebefore return- Ternary
condition ? <A /> : <B /> - Logical AND
isLoggedIn && <Dashboard /> - Early return for loading/error states
Real Example
function UserProfile({ userId }) {
const { data, isLoading, error } = useUser(userId)
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
if (!data) return <EmptyState message="User not found" />
return (
<div>
<h1>{data.name}</h1>
{data.isPremium && <PremiumBadge />}
</div>
)
}267. const vs let
Theory
Use const by default. Use let only when reassignment is needed. Never use var.
const prevents rebinding but allows object/array mutation.
Real Example
const API_URL = '/api/v2' // never reassign
const user = { name: 'Amit' }
user.name = 'Rahul' // ✅ mutation OK
let retryCount = 0 // will reassign
retryCount++
for (let i = 0; i < 3; i++) {
// block-scoped loop var
setTimeout(() => console.log(i), 100)
}268. Cross Browser Compatibility
Theory
Different browsers (Chrome, Firefox, Safari, Edge) and versions implement web standards with varying support. Cross-browser compatibility ensures your app works consistently across target browsers.
Tools: Can I Use, Autoprefixer, Polyfills, Babel, Normalized CSS.
Pros & Cons
| Supporting older browsers | Modern-only |
|---|---|
| ✅ Wider audience | ✅ Smaller bundles, modern APIs |
| ❌ Polyfills increase bundle | ❌ Excludes IE/old Android users |
| ❌ More testing surface | ❌ May lose enterprise clients |
Real-Life Example
/* Autoprefixer adds vendor prefixes automatically */
.grid {
display: grid;
/* Autoprefixer outputs: */
/* display: -ms-grid; */
/* display: grid; */
}
.card {
user-select: none;
/* → -webkit-user-select: none; */
/* → user-select: none; */
}// Feature detection — not browser detection
if ("IntersectionObserver" in window) {
setupInfiniteScroll();
} else {
setupPagination(); // fallback
}
// browserslist in package.json
{
"browserslist": [">0.2%", "not dead", "not op_mini all"]
}// CSS @supports fallback
@supports (display: grid) {
.layout { display: grid; }
}
@supports not (display: grid) {
.layout { display: flex; flex-wrap: wrap; }
}269. CSS Specificity
Theory
Specificity determines which CSS rule wins when multiple rules target the same element. Calculated as weights:
| Selector | Weight |
|---|---|
| Inline style | 1000 |
#id | 100 |
.class, [attr], :pseudo-class | 10 |
element, ::pseudo-element | 1 |
* (universal) | 0 |
!important overrides specificity (avoid in production).
Real Example
/* Specificity: 1 */
p {
color: black;
}
/* Specificity: 10 */
.text {
color: blue;
}
/* Specificity: 100 */
#main {
color: green;
}
/* Specificity: 11 — .text + p */
.text p {
color: red;
} /* wins over p and .text alone */
/* Specificity: 110 — #main + .text */
#main .text {
color: purple;
} /* wins */<p class="text" id="main">Color is purple (#main .text = 110)</p>270. Data Types
Theory
JavaScript has 8 data types — 7 primitives and 1 non-primitive:
| Type | typeof | Mutable | Stored |
|---|---|---|---|
string | "string" | No | Value |
number | "number" | No | Value |
boolean | "boolean" | No | Value |
undefined | "undefined" | No | Value |
null | "object" ⚠️ | No | Value |
symbol | "symbol" | No | Value |
bigint | "bigint" | No | Value |
object | "object" | Yes | Reference |
Primitives are immutable and compared by value. Objects (including arrays, functions, dates) are compared by reference.
Pros & Cons
| Primitives | Objects |
|---|---|
| ✅ Fast comparison by value | ✅ Can hold complex nested data |
| ✅ Immutable — thread-safe conceptually | ✅ Methods and properties |
| ❌ Limited structure | ❌ Compared by reference — easy bugs |
| ❌ Mutable — accidental changes |
Real-Life Example
// Primitives — passed by value
let price = 299
let discount = price
discount = 199
console.log(price) // 299 — unchanged
// Objects — passed by reference
const orderA = { total: 500 }
const orderB = orderA
orderB.total = 600
console.log(orderA.total) // 600 — same object!
// typeof quirks
typeof null // "object" (historical bug)
typeof [] // "object"
typeof function () {} // "function" (special case)
Array.isArray([]) // true — reliable array check
// Checking for null/undefined safely
const value = null
value == null // true (checks null OR undefined)
value === null // true
value ?? 'default' // "default" (nullish coalescing)// Real-life: API response typing mindset
const user = {
id: 'u_42',
name: 'Amit',
roles: ['admin', 'editor'],
metadata: { lastLogin: '2026-03-15' },
}
// typeof user === "object" — always check structure, not just type271. Debugging Real-World Performance Issues
Theory
Production performance bugs rarely announce themselves. Systematic debugging:
- Reproduce — RUM data, user report, Sentry trace
- Measure — Chrome DevTools Performance tab
- Identify — long tasks, layout thrashing, memory leaks
- Fix — targeted, not premature optimization
- Verify — before/after vitals in staging
Debugging Checklist
| Symptom | Likely cause | Tool |
|---|---|---|
| Slow first load | Large JS bundle, no code split | Coverage tab, webpack analyzer |
| Page jumps on load | Images/fonts without dimensions | Layout Shift regions in Performance |
| Scroll jank | Too many DOM nodes, no virtualization | Performance → Frames |
| Input lag (bad INP) | Long tasks on main thread | Performance → Main thread flame chart |
| Memory grows over time | Leaked listeners, intervals, closures | Memory tab → heap snapshots |
| API feels slow | Waterfall, no parallelization | Network tab |
| Re-render storm | Context value, missing memo | React DevTools Profiler |
React DevTools Profiler
// Find unnecessary re-renders
// 1. Record interaction in Profiler
// 2. Look for components that re-rendered but props didn't change
// 3. Fix: memo, useMemo, split context, move state down
// Common fix — unstable reference
// ❌ New object every render
;<Child config={{ theme: 'dark', size: 'lg' }} />
// ✅ Stable reference
const config = useMemo(() => ({ theme: 'dark', size: 'lg' }), [])
;<Child config={config} />Long Task Detection
// Observe long tasks in production
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
sendToAnalytics({
type: 'long-task',
duration: entry.duration,
page: location.pathname,
})
}
}
})
observer.observe({ type: 'longtask', buffered: true })Memory Leak Pattern
// ❌ Leak — listener not removed
useEffect(() => {
window.addEventListener('scroll', handleScroll)
}, [])
// ✅ Cleanup
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [handleScroll])
// ❌ Leak — interval in module scope
setInterval(pollStatus, 1000)
// ✅ Cleanup + AbortController for fetch
useEffect(() => {
const id = setInterval(pollStatus, 1000)
const controller = new AbortController()
fetch(url, { signal: controller.signal })
return () => {
clearInterval(id)
controller.abort()
}
}, [])Case Study — Slow Product List
Problem: LCP 4.2s on /products, INP 450ms on filter
RUM: 80% users on mobile 3G
Step 1: Network — 1.2MB JS bundle (main culprit)
Fix 1: Route-level code splitting → bundle 380KB
Step 2: LCP element — unoptimized category hero image
Fix 2: WebP + priority preload → LCP 2.1s
Step 3: Filter input — re-renders 500-item list every keystroke
Fix 3: useDeferredValue + memo ProductCard → INP 120ms
Verify: Lighthouse CI passed, RUM p75 LCP < 2.5s after 48h272. Deep Copy vs Shallow Copy
Theory
| Shallow Copy | Deep Copy | |
|---|---|---|
| Top level | New reference | New reference |
| Nested objects | Shared | Independent copies |
| Methods | spread, Object.assign, slice | structuredClone, JSON, recursive |
| Mutation risk | Nested changes affect both | Fully isolated |
Real Example
const original = {
name: 'Amit',
scores: [90, 85],
address: { city: 'Bangalore' },
}
// Shallow
const shallow = { ...original }
shallow.scores.push(100)
console.log(original.scores) // [90, 85, 100] — mutated!
// Deep — structuredClone (modern)
const deep = structuredClone(original)
deep.scores.push(100)
console.log(original.scores) // [90, 85] — unchanged
// Deep — JSON (limitations: no Date, Map, undefined, functions)
const jsonDeep = JSON.parse(JSON.stringify(original))
// Deep — manual recursive
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value
if (seen.has(value)) return seen.get(value)
if (value instanceof Date) return new Date(value)
const clone = Array.isArray(value) ? [] : {}
seen.set(value, clone)
for (const key of Reflect.ownKeys(value)) {
clone[key] = deepClone(value[key], seen)
}
return clone
}// React — immutable update is shallow spread per level
setUser((prev) => ({
...prev,
address: { ...prev.address, city: 'Delhi' }, // deep enough for one nested level
}))273. Dependent field state structure
Theory
When field B depends on field A (e.g., city depends on state, verification method depends on ID type), flat state becomes awkward. Structure options:
- Derived state — compute B options from A, don't store redundantly
- Nested state —
{ address: { state, city } } - Reset dependent on parent change — when A changes, clear B
Pros & Cons
| Derived (computed) | Stored separately |
|---|---|
| ✅ Single source of truth | ❌ Can desync (city invalid for new state) |
| ✅ No stale city when state changes | ❌ Must manually reset |
| Must recalculate on render | Extra sync logic |
Real-Life Example
const STATES_CITIES: Record<string, string[]> = {
Karnataka: ['Bangalore', 'Mysore'],
Maharashtra: ['Mumbai', 'Pune'],
}
function AddressForm() {
const [state, setState] = useState('')
const [city, setCity] = useState('')
// Derived — cities depend on state
const cityOptions = state ? (STATES_CITIES[state] ?? []) : []
const handleStateChange = (newState: string) => {
setState(newState)
setCity('') // reset dependent field — CRITICAL
}
return (
<>
<Select label="State" value={state} onChange={handleStateChange} options={Object.keys(STATES_CITIES)} />
<Select label="City" value={city} onChange={setCity} options={cityOptions} disabled={!state} />
</>
)
}
// KYC: IFSC depends on bank selection
function BankDetails() {
const [bankCode, setBankCode] = useState('')
const [ifsc, setIfsc] = useState('')
const handleBankChange = (code: string) => {
setBankCode(code)
setIfsc('') // reset
}
// Validate IFSC prefix matches bank
const validateIFSC = (value: string) => {
if (!bankCode) return 'Select bank first'
if (!value.startsWith(bankCode.slice(0, 4))) return "IFSC doesn't match selected bank"
return null
}
}State design answer for a fintech app: "Parent field change dispatches SET_FIELD for parent + clears child. Child options are derived, not stored. Cross-field validation runs on submit and on parent blur."
274. Destructuring
Theory
Destructuring extracts values from arrays or properties from objects into distinct variables. It works in assignments, function parameters, and nested structures.
Supports defaults, renaming, and rest patterns.
Pros & Cons
| Pros | Cons |
|---|---|
| Cleaner variable extraction | Can be overused on deeply nested data |
| Self-documenting function params | Undefined nested destructuring throws |
| Easy swap without temp variable | Defaults only apply for undefined, not null |
Real-Life Example
// Object destructuring
const user = { id: 'u_1', name: 'Amit', email: 'amit@test.com', role: 'admin' }
const { name, email, role = 'user' } = user
const { id: userId } = user // rename
// Nested
const order = {
id: 'ORD-1',
customer: { name: 'Rahul', city: 'Delhi' },
items: [{ name: 'Biryani', price: 299 }],
}
const {
customer: { name: customerName },
items: [firstItem],
} = order
// Array destructuring
const [first, second, ...rest] = [10, 20, 30, 40]
// first=10, second=20, rest=[30,40]
// Function parameters
function renderOrder({ id, status, items = [] }) {
return `Order ${id}: ${status} (${items.length} items)`
}
// Swap without temp
let a = 1,
b = 2
;[a, b] = [b, a]
// React — props destructuring
function ProductCard({ name, price, image, onAddToCart }) {
return (
<div>
<img src={image} alt={name} />
<h3>{name}</h3>
<p>₹{price}</p>
<button onClick={onAddToCart}>Add</button>
</div>
)
}
// API response
const {
data: restaurants,
meta: { total, page },
} = await apiResponse275. Difference between unique_ptr and shared_ptr
unique_ptr — exclusive ownership
#include <memory>
auto ptr1 = std::make_unique<int>(10);
// auto ptr2 = ptr1; // ❌ compile error — not copyable
auto ptr2 = std::move(ptr1); // ✅ transfer ownership
// ptr1 is now nullptr- Zero overhead vs raw pointer (no ref counting)
- Cannot be copied, only moved
- Use by default
shared_ptr — shared ownership
auto sp1 = std::make_shared<int>(20);
auto sp2 = sp1; // ref count = 2
{
auto sp3 = sp1; // ref count = 3
} // sp3 destroyed, ref count = 2
// Memory freed when ref count hits 0- Reference counted — thread-safe ref count increment/decrement
- Slight overhead (control block)
- Risk of circular references → use
weak_ptrto break cycles
When to use which
| Scenario | Choice |
|---|---|
| Single owner | unique_ptr |
| Shared ownership (cache, graph) | shared_ptr |
| Observer (break cycles) | weak_ptr |
| Factory returns ownership | unique_ptr |
276. Difference between vector and list
| Feature | std::vector | std::list |
|---|---|---|
| Memory | Contiguous array | Doubly linked nodes |
| Random access | O(1) operator[] | O(n) |
| Insert at end | O(1) amortized | O(1) |
| Insert in middle | O(n) — shift elements | O(1) with iterator |
| Cache locality | Excellent | Poor (pointer chasing) |
| Memory overhead | Low | High (two pointers per node) |
| Iterator invalidation | On reallocation / erase | Only erased element |
When to use
// vector — default choice (cache-friendly, fast iteration)
std::vector<int> nums = {1, 2, 3, 4, 5};
nums.push_back(6); // fast
int x = nums[2]; // O(1) random access
// list — frequent middle insert/erase, no reallocation needed
std::list<int> lst = {1, 2, 3};
auto it = std::next(lst.begin());
lst.insert(it, 99); // O(1), no invalidation of other iterators277. Difference between Deep Copy and Shallow Copy
Theory
| Shallow Copy | Deep Copy | |
|---|---|---|
| Top-level | New reference | New reference |
| Nested objects | Shared reference | New references |
| Methods | Object.assign, spread {...obj}, Array.slice | structuredClone, recursive fn, JSON.parse/stringify |
Practical Example
const original = {
name: 'Amit',
address: { city: 'Delhi', pin: 110001 },
hobbies: ['cricket', 'coding'],
}
// Shallow copy
const shallow = { ...original }
shallow.address.city = 'Mumbai'
console.log(original.address.city) // "Mumbai" — nested object shared!
// Deep copy — structuredClone (modern)
const deep = structuredClone(original)
deep.address.city = 'Bangalore'
console.log(original.address.city) // "Delhi" — unchanged
// Deep copy — JSON trick (limitations: no Date, Map, undefined, functions)
const jsonDeep = JSON.parse(JSON.stringify(original))Manual deep copy function
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value
if (seen.has(value)) return seen.get(value) // handle circular refs
if (value instanceof Date) return new Date(value)
if (value instanceof RegExp) return new RegExp(value)
const clone = Array.isArray(value) ? [] : {}
seen.set(value, clone)
for (const key of Reflect.ownKeys(value)) {
clone[key] = deepClone(value[key], seen)
}
return clone
}278. Difference between Normal Functions and Arrow Functions
Theory
Both define functions, but differ in syntax, this binding, and capabilities.
| Feature | Normal function | Arrow function |
|---|---|---|
| Syntax | function fn() {} | const fn = () => {} |
this | Dynamic — depends on how called | Lexical — inherited from enclosing scope |
arguments | Available | Not available — use ...args |
new keyword | Can be constructor | Cannot |
prototype | Has .prototype | No |
| Implicit return | No (unless IIFE) | Yes for single expressions |
yield (generators) | Yes | No |
Pros & Cons
| Normal functions | Arrow functions |
|---|---|
✅ Correct this for object methods | ✅ Concise syntax |
| ✅ Can be constructors | ✅ Fixes this in callbacks |
✅ Has arguments | ✅ Great for array methods |
❌ this lost in callbacks | ❌ Wrong this for object methods |
| ❌ Not usable as constructors |
Real-Life Example
const orderService = {
orders: ['ORD-1', 'ORD-2'],
// ✅ Normal — `this` is orderService
listOrders() {
return this.orders
},
// ❌ Arrow — `this` is NOT orderService
brokenList: () => this?.orders, // undefined
// ✅ Arrow inside normal — inherits `this` from listWithIds
listWithIds() {
return this.orders.map((id) => `ID: ${id}`)
},
}
// Callback pattern
button.addEventListener('click', function () {
this.classList.add('active') // `this` = button element
})
button.addEventListener('click', () => {
// `this` = enclosing scope (e.g., component instance or undefined)
})// React — arrow functions are standard in function components
function OrderList({ orders }) {
const handleCancel = (id: string) => cancelOrder(id) // arrow — no `this` needed
return orders.map((order) => <OrderCard key={order.id} order={order} onCancel={() => handleCancel(order.id)} />)
}279. Difference between PATCH and PUT
Theory
Both are HTTP methods for updating resources, but they differ in scope and idempotency expectations.
- PUT replaces the entire resource with the request body. Missing fields are typically set to null/default.
- PATCH applies partial updates — only the fields in the request body are changed.
Pros & Cons
| PUT | PATCH | |
|---|---|---|
| Pros | Idempotent, simple full replacement semantics | Bandwidth-efficient, precise partial updates |
| Cons | Must send full resource, risk overwriting fields | Harder to implement correctly, less universally supported |
Real-Life Example
PUT /api/users/42
Content-Type: application/json
{
"name": "Amit Sharma",
"email": "amit@example.com",
"phone": "9876543210",
"address": "Delhi"
}
# If phone was omitted, it gets cleared
# PATCH — update only the phone number
PATCH /api/users/42
Content-Type: application/json
{
"phone": "9876543210"
}
# name, email, address remain unchanged// Frontend usage
await fetch('/api/users/42', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: newPhone }),
})280. Difference between position: relative, absolute, fixed, and static
Theory
The CSS position property controls how an element is placed in the document and whether it's removed from normal flow.
| Value | Removed from flow? | Positioned relative to | Scroll behavior |
|---|---|---|---|
static | No (default) | Normal document flow | Scrolls with page |
relative | No | Its own original position | Scrolls with page |
absolute | Yes | Nearest positioned ancestor (or viewport) | Scrolls with ancestor |
fixed | Yes | Viewport | Stays fixed on screen |
top, right, bottom, left, and z-index only work when position is not static.
Pros & Cons
| Position | Best for | Avoid when |
|---|---|---|
static | Normal layout | You need offset control |
relative | Offset without leaving flow, containing block for absolute children | Full-page overlays |
absolute | Tooltips, badges, dropdowns inside a container | Main page layout |
fixed | Sticky headers, modals, FAB buttons | Inside scrollable containers (use sticky) |
Real-Life Example
/* static — default, no offset */
.card {
position: static;
}
/* relative — nudge element, create positioning context */
.card-badge {
position: relative;
top: -8px; /* visual offset, space still reserved in flow */
}
.product-card {
position: relative; /* containing block for absolute children */
}
/* absolute — positioned inside .product-card */
.sale-tag {
position: absolute;
top: 8px;
right: 8px;
background: red;
padding: 4px 8px;
color: white;
}
/* fixed — stays on screen while scrolling */
.app-header {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 100;
}
.floating-cart-btn {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 50;
}<!-- Visual hierarchy -->
<div class="product-card" style="position: relative">
<img src="biryani.jpg" alt="Biryani" />
<span class="sale-tag" style="position: absolute; top: 8px; right: 8px">20% OFF</span>
</div>Document flow with absolute:
┌─ .product-card (relative) ─────────┐
│ ┌ SALE TAG (absolute) ┐ │
│ │ top:8 right:8 │ │
│ └────────────────────┘ │
│ [Product Image] │
│ Product Name — ₹299 │
└────────────────────────────────────┘Bonus — position: sticky: Acts as relative until scroll threshold, then fixed within its container. Great for table headers and section nav.
281. Difference between undefined and not defined
Theory
These are often confused but mean different things:
undefined | Not defined | |
|---|---|---|
| Meaning | Variable exists but has no assigned value | Variable does not exist in scope |
| Error? | No error when accessed | ReferenceError |
| typeof | "undefined" | ReferenceError (can't use typeof on undeclared) |
| Common causes | Declared but not initialized, missing object property, function with no return | Typo in variable name, accessing before declaration (TDZ) |
Pros & Cons
| Knowing the difference | Confusing them |
|---|---|
| ✅ Faster debugging | ❌ Misdiagnose ReferenceError as undefined |
| ✅ Correct typeof checks | ❌ Use == null incorrectly |
| ✅ Understand optional chaining |
Real-Life Example
// undefined — variable exists, no value
let username
console.log(username) // undefined
console.log(typeof username) // "undefined"
const user = { name: 'Amit' }
console.log(user.age) // undefined (property doesn't exist)
console.log(typeof user.age) // "undefined"
function getDiscount() {
// no return statement
}
console.log(getDiscount()) // undefined
// Not defined — variable doesn't exist
console.log(usrname) // ReferenceError: usrname is not defined (typo)
console.log(notDeclared) // ReferenceError
// TDZ — technically "not defined" until declaration line
// console.log(count); // ReferenceError (TDZ, not undefined)
let count = 5// Safe checks in production
if (user.email !== undefined) {
/* has email field */
}
if (user.email == null) {
/* null OR undefined */
}
const email = user.email ?? 'guest@example.com' // nullish coalescing
// Optional chaining
const city = user?.address?.city // undefined if any part missing, no errorInterview answer (concise)
282. display: none vs visibility: hidden
Theory
display: none | visibility: hidden | |
|---|---|---|
| Visible? | No | No |
| Takes space? | No — removed from layout | Yes — blank space remains |
| Accessibility | Hidden from screen readers | Hidden from screen readers |
| Triggers reflow? | Yes | No |
| Children visible? | All hidden | Can override with visible on child |
| Transitions? | No smooth fade | Can animate opacity separately |
Real Example
/* Remove from layout — modals closed, mobile menu hidden */
.modal-closed {
display: none;
}
/* Hide but preserve space — skeleton placeholders, accessibility */
.sr-only-hidden {
visibility: hidden;
width: 0;
height: 0;
}
/* Toggle menu */
.nav-mobile {
display: none;
}
.nav-mobile.open {
display: block;
}
/* Collapse table column without reflow */
.col-hidden {
visibility: hidden;
}283. Engineering Quality & Ownership
What they're assessing
Testing, monitoring, on-call mindset, production responsibility.
Ownership signals
| Signal | Example phrase |
|---|---|
| Prevent regressions | "I added E2E for critical paths in CI" |
| Monitor production | "Dashboards for error rate and Core Web Vitals" |
| Postmortems | "Blameless RCA after checkout outage" |
| Mentorship | "Pairing juniors on hooks and testing" |
| Security | "OWASP review on file upload flow" |
Sample answer — Ownership
284. Event Bubbling and Capturing
Theory
Events travel in three phases:
- Capturing — window → target (top down)
- Target — event reaches element
- Bubbling — target → window (bottom up)
Default listeners run in bubbling phase. Use { capture: true } for capturing.
Event delegation attaches one listener on parent, uses bubbling + event.target.
Real Example
<div id="list">
<button data-id="1">Pay</button>
<button data-id="2">Refund</button>
</div>// Bubbling — click button → list → body → html
document.getElementById('list').addEventListener('click', (e) => {
const btn = e.target.closest('button[data-id]')
if (!btn) return
handlePayment(btn.dataset.id)
})
// stopPropagation — modal content click doesn't close overlay
overlay.addEventListener('click', closeModal)
modalContent.addEventListener('click', (e) => e.stopPropagation())
// Capturing — intercept before children
document.addEventListener('click', logAllClicks, { capture: true })285. Event bubbling vs event delegation
Theory
Event bubbling is a mechanism — after an event fires on a target, it propagates upward through ancestors (target → parent → ... → window).
Event delegation is a pattern — attach one listener on a parent and handle
events from children using event.target and bubbling.
They are related but not the same. Delegation uses bubbling. You can bubble without delegating (each child has its own listener that bubbles up).
When each actually matters
| Situation | Bubbling matters | Delegation matters |
|---|---|---|
| Nested click handlers | stopPropagation() to prevent parent handler | One listener for 100 list items |
| Modal overlay click-to-close | Click on content bubbles to overlay | — |
| Dynamically added DOM nodes | — | Delegation works without re-binding |
| Memory / performance | Many listeners vs one | 1000 rows → 1 listener |
Pros & Cons
| Event delegation | Individual listeners |
|---|---|
| ✅ Fewer listeners, less memory | ✅ Explicit per-element control |
| ✅ Works for dynamic children | ✅ Easier to debug which handler fired |
❌ Must filter with closest() / matches() | ❌ Must re-bind on DOM changes |
| ❌ Doesn't work for non-bubbling events* | ❌ 1000 items = 1000 listeners |
*Non-bubbling: focus, blur, scroll — use capture phase or direct binding.
Real-Life Example
// BUBBLING — event travels up
document.getElementById('card').addEventListener('click', () => {
console.log('card clicked')
})
document.getElementById('btn').addEventListener('click', (e) => {
console.log('button clicked')
e.stopPropagation() // prevents "card clicked"
})
// DELEGATION — one listener on parent
const transactionList = document.getElementById('transactions')
transactionList.addEventListener('click', (e) => {
const row = e.target.closest('[data-txn-id]')
if (!row) return
const action = e.target.closest('[data-action]')?.dataset.action
const txnId = row.dataset.txnId
if (action === 'view') openTransaction(txnId)
if (action === 'dispute') openDispute(txnId)
})
// New transactions appended later — delegation still works, zero re-bindingInterview answer (concise)
286. Explain copy constructor and move constructor
Copy constructor — deep copy
class String {
char* data;
size_t len;
public:
String(const char* s) : len(strlen(s)), data(new char[len + 1]) {
strcpy(data, s);
}
// Copy constructor
String(const String& other) : len(other.len), data(new char[other.len + 1]) {
strcpy(data, other.data); // deep copy
std::cout << "Copy ctor\n";
}
~String() { delete[] data; }
};Move constructor — steal resources
// Move constructor
String(String&& other) noexcept
: data(other.data), len(other.len) {
other.data = nullptr;
other.len = 0;
std::cout << "Move ctor\n";
}Rule of Five
If you define any of these, consider defining all:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
String& operator=(const String& other) { /* copy assign */ }
String& operator=(String&& other) noexcept { /* move assign */ }= default and = delete
class NonCopyable {
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};287. Explain event delegation with a practical example
Theory
Event delegation attaches a single listener to a parent element instead of
many listeners on each child. Events bubble up — the parent handles them using event.target.
Benefits
- Fewer listeners → better memory and performance
- Works for dynamically added children
- Simpler cleanup
Practical Example
<ul id="menu">
<li data-action="edit">Edit</li>
<li data-action="delete">Delete</li>
<li data-action="share">Share</li>
</ul>const menu = document.getElementById('menu')
// ✅ One listener instead of three
menu.addEventListener('click', (event) => {
const item = event.target.closest('li[data-action]')
if (!item || !menu.contains(item)) return
const action = item.dataset.action
switch (action) {
case 'edit':
console.log('Edit clicked')
break
case 'delete':
console.log('Delete clicked')
break
case 'share':
console.log('Share clicked')
break
}
})
// Dynamically added items work automatically
const newItem = document.createElement('li')
newItem.dataset.action = 'archive'
newItem.textContent = 'Archive'
menu.appendChild(newItem)When NOT to delegate
- Events that don't bubble (
focus,blur,scroll) — use capture phase or direct binding - Very deep trees where
closest()traversal is costly (rare)
288. Explain frontend caching strategies
Theory — Cache layers
flowchart LR
Browser[Browser Cache] --> CDN[CDN Edge]
CDN --> App[App Cache / Service Worker]
App --> Memory[In-Memory Cache]
Memory --> API[API / Origin]1. HTTP caching
Cache-Control: public, max-age=31536000, immutable
# HTML — short cache or no cache
Cache-Control: no-cache
# API responses — private, short TTL
Cache-Control: private, max-age=60
ETag: "abc123"Use content hashing in filenames: app.a1b2c3.js → safe immutable caching.
2. CDN caching
- Cache static assets and SSR pages at edge (CloudFront, Cloudflare, Fastly)
- Use stale-while-revalidate for semi-dynamic content
- Cache keys should include locale, device type if responses differ
3. Application-level (React Query)
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 min — data considered fresh
gcTime: 30 * 60 * 1000, // 30 min — garbage collect unused
refetchOnWindowFocus: true,
retry: 2,
},
},
})
// Prefetch on hover for instant navigation
function RestaurantLink({ id }) {
const queryClient = useQueryClient()
return (
<Link
to={`/restaurant/${id}`}
onMouseEnter={() =>
queryClient.prefetchQuery({
queryKey: ['restaurant', id],
queryFn: () => fetchRestaurant(id),
})
}>
View
</Link>
)
}4. Service Worker (PWA)
// Workbox strategy: stale-while-revalidate for API, cache-first for assets
workbox.routing.registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new workbox.strategies.StaleWhileRevalidate({ cacheName: 'api-cache' }),
)5. Local storage / IndexedDB
- Persist user preferences, draft forms, offline cart
- Not for sensitive tokens (use httpOnly cookies)
- IndexedDB for larger structured offline data
Cache invalidation strategies
| Strategy | When to use |
|---|---|
| TTL (time-to-live) | Predictable staleness tolerance |
| Event-driven invalidation | WebSocket/push updates order status |
| Versioned keys | Deploy new API version → new cache key |
| Optimistic updates + rollback | Instant UI, reconcile on error |
289. Explain map, filter, and reduce with examples
Theory
Three essential array methods for immutable data transformation:
| Method | Returns | Purpose |
|---|---|---|
map | New array (same length) | Transform each element |
filter | New array (0 to n length) | Keep elements matching a condition |
reduce | Single value (any type) | Accumulate into one result |
None mutate the original array.
Pros & Cons
| Pros | Cons |
|---|---|
| Declarative and readable | Chaining creates intermediate arrays |
| Immutable by default | Can't break early (use for...of) |
| Chainable | Slightly slower than manual loop in hot paths |
Real-Life Example
const menuItems = [
{ id: 1, name: 'Biryani', price: 299, category: 'main', inStock: true },
{ id: 2, name: 'Naan', price: 49, category: 'bread', inStock: true },
{ id: 3, name: 'Lassi', price: 79, category: 'drink', inStock: false },
{ id: 4, name: 'Kebab', price: 199, category: 'main', inStock: true },
]
// map — transform for dropdown options
const options = menuItems.map((item) => ({
label: `${item.name} — ₹${item.price}`,
value: item.id,
}))
// [{ label: "Biryani — ₹299", value: 1 }, ...]
// filter — available main courses under ₹250
const affordableMains = menuItems.filter((item) => item.category === 'main' && item.inStock && item.price <= 250)
// [{ id: 4, name: "Kebab", price: 199, ... }]
// reduce — calculate total order value
const cart = [
{ id: 1, quantity: 2 },
{ id: 2, quantity: 4 },
]
const orderTotal = cart.reduce((total, cartItem) => {
const product = menuItems.find((m) => m.id === cartItem.id)
return total + product.price * cartItem.quantity
}, 0)
// 299*2 + 49*4 = 794
// reduce — group by category
const byCategory = menuItems.reduce((groups, item) => {
const cat = item.category
if (!groups[cat]) groups[cat] = []
groups[cat].push(item)
return groups
}, {})
// { main: [...], bread: [...], drink: [...] }
// Chaining all three
const summary = menuItems
.filter((item) => item.inStock)
.map((item) => item.name)
.reduce((text, name, index, arr) => {
return text + name + (index < arr.length - 1 ? ', ' : '')
}, 'Available: ')// React — primary list rendering pattern
function MenuList({ items }) {
return (
<ul>
{items
.filter((item) => item.inStock)
.map((item) => (
<MenuItem key={item.id} item={item} />
))}
</ul>
)
}290. Explain move semantics
Theory
Move semantics (C++11) transfer resources from a source object to a destination without copying — the source is left in a valid but unspecified state.
Why needed
std::vector<int> createLargeVector() {
std::vector<int> v(1'000'000, 42);
return v; // move, not copy (RVO/NRVO or move constructor)
}Without move semantics, returning large objects copies all elements.
Move vs Copy
std::string a = "hello";
std::string b = a; // copy — both own their data
std::string c = std::move(a); // move — c steals a's buffer, a is empty
std::cout << a; // "" (valid but empty)
std::cout << c; // "hello"std::move is a cast
// std::move doesn't move anything — it casts to rvalue reference
// enabling move constructor/assignment to be called
void process(std::string&& s); // accepts rvalue
std::string name = "Amit";
process(std::move(name)); // explicitly treat as rvalueMove constructor example
class Buffer {
int* data;
size_t size;
public:
// Move constructor
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};291. Find Missing Numbers in Array
Theory
Given [1, 2, 3, 5, 7], find missing numbers in the sequence → [4, 6].
Approaches: loop with expected counter, Set comparison, or math range filter.
Implementation
function findMissingNumbers(arr) {
if (!arr.length) return []
const set = new Set(arr)
const min = Math.min(...arr)
const max = Math.max(...arr)
const missing = []
for (let i = min; i <= max; i++) {
if (!set.has(i)) missing.push(i)
}
return missing
}
console.log(findMissingNumbers([1, 2, 3, 5, 7])) // [4, 6]
console.log(findMissingNumbers([1, 3, 6, 10])) // [2, 4, 5, 7, 8, 9]// Alternative — without Set (the company may ask no built-ins)
function findMissingNumbersLoop(arr) {
const missing = []
for (let i = 0; i < arr.length - 1; i++) {
const diff = arr[i + 1] - arr[i]
for (let j = 1; j < diff; j++) {
missing.push(arr[i] + j)
}
}
return missing
}
console.log(findMissingNumbersLoop([1, 2, 3, 5, 7])) // [4, 6]292. Find the First Non-Repeating Character in a string
Theory
Count character frequencies, then find the first character with count 1.
Approach 1 — Two passes with hash map
function firstNonRepeating(s) {
const freq = new Map()
for (const char of s) {
freq.set(char, (freq.get(char) || 0) + 1)
}
for (const char of s) {
if (freq.get(char) === 1) return char
}
return null // or -1 if no such character
}
console.log(firstNonRepeating('leetcode')) // "l"
console.log(firstNonRepeating('aabbcc')) // null
console.log(firstNonRepeating('swiss')) // "w"Complexity
| Time | Space | |
|---|---|---|
| Hash map | O(n) | O(k) where k = unique chars |
| Array (ASCII) | O(n) | O(1) — fixed 26 or 128 size |
ASCII-optimized version
function firstNonRepeatingASCII(s) {
const freq = new Array(26).fill(0)
for (const char of s) {
freq[char.charCodeAt(0) - 97]++
}
for (const char of s) {
if (freq[char.charCodeAt(0) - 97] === 1) return char
}
return null
}C++ version
#include <string>
#include <unordered_map>
char firstNonRepeating(const std::string& s) {
std::unordered_map<char, int> freq;
for (char c : s) freq[c]++;
for (char c : s) {
if (freq[c] == 1) return c;
}
return '\0';
}293. First Repeating Character
Theory
Find the first character that appears more than once when scanning left to right.
"success" → "c" (index 2 — first char with count > 1 on second occurrence)
Note: This is different from first non-repeating character ("success" → "u").
Algorithm
- Track seen characters in a Set
- Scan left to right
- If char already in Set → return it (first repeat)
- Else add to Set
Implementation
function firstRepeatingChar(str) {
const seen = new Set()
for (const char of str) {
if (seen.has(char)) return char
seen.add(char)
}
return null // no repeat
}
console.log(firstRepeatingChar('success')) // "c"
console.log(firstRepeatingChar('abcdef')) // null
console.log(firstRepeatingChar('abccba')) // "c" (first repeat at index 2)
console.log(firstRepeatingChar('programming')) // "r"// Alternative — frequency map + second pass for first index
function firstRepeatingCharMap(str) {
const freq = new Map()
for (const char of str) {
freq.set(char, (freq.get(char) || 0) + 1)
}
for (const char of str) {
if (freq.get(char) > 1) return char
}
return null
}Walk-through: "success"
s → seen={s}
u → seen={s,u}
c → seen={s,u,c}
c → already in seen → return "c" ✅294. Flatten a Nested Array
Theory
Convert [[1, [2, 3]], 4] → [1, 2, 3, 4].
Approach 1 — Recursive
function flatten(arr) {
const result = []
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatten(item))
} else {
result.push(item)
}
}
return result
}
console.log(flatten([1, [2, [3, 4]], 5])) // [1, 2, 3, 4, 5]Approach 2 — Iterative with stack
function flattenIterative(arr) {
const stack = [...arr]
const result = []
while (stack.length) {
const next = stack.pop()
if (Array.isArray(next)) {
stack.push(...next)
} else {
result.push(next)
}
}
return result.reverse() // stack reverses order
}Approach 3 — Built-in (ES2019)
const flat = [1, [2, [3, 4]], 5].flat(Infinity)Flatten with depth control
function flattenDepth(arr, depth = 1) {
if (depth <= 0) return arr.slice()
return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
acc.push(...flattenDepth(item, depth - 1))
} else {
acc.push(item)
}
return acc
}, [])
}
console.log(flattenDepth([1, [2, [3]]], 1)) // [1, 2, [3]]
console.log(flattenDepth([1, [2, [3]]], 2)) // [1, 2, 3]C++ version (recursive)
#include <vector>
#include <variant>
// For vector of variants (int or nested vector) — simplified:
std::vector<int> flatten(const std::vector<std::vector<int>>& nested) {
std::vector<int> result;
for (const auto& inner : nested) {
result.insert(result.end(), inner.begin(), inner.end());
}
return result;
}React
| Topic | One-liner |
|---|---|
| Re-render triggers | State, context, parent update |
| Fiber | Interruptible work units; render + commit phases |
| RSC vs Client | Server = no client JS; Client = "use client" + hooks |
| Thousands of rows | Virtualization + memo + pagination |
| Lifecycle | setState → render → commit DOM → layout effects → paint → useEffect |
| Avoid Context | Fast-changing / granular state |
| useMemo / useCallback / memo | Value / function / component memoization |
| Race conditions | AbortController or request ID |
| Code splitting | React.lazy + Suspense per route |
| State update internals | Enqueue → batch → reconcile → commit |
| Component library | Tokens, primitives, a11y, Storybook, semver |
JavaScript
| Topic | One-liner |
|---|---|
| Promise.all | All resolve or first reject; preserve order |
| WeakMap/WeakSet | Weak refs; keys are objects only |
| Event delegation | One parent listener + event.target |
| Shallow vs deep | Nested refs shared vs structuredClone |
| GC | Mark-and-sweep from roots; generational in V8 |
| Closure | Function + lexical environment |
| Async iteration | Symbol.asyncIterator + for await...of |
| async/await | Promise sugar; pauses on microtask queue |
C++
| Topic | One-liner |
|---|---|
| Smart pointers | RAII auto memory management |
| unique vs shared | Exclusive vs ref-counted shared ownership |
| Move semantics | Transfer resources; std::move casts to rvalue |
| RAII | Acquire in ctor, release in dtor |
| vector vs list | Contiguous/cache-friendly vs linked/flexible |
| ctor/dtor order | Base → members → body; reverse for destruction |
| Copy vs move ctor | Deep copy vs steal resources |
| vtable | vptr → function pointer array for polymorphism |
DSA
| Problem | Approach |
|---|---|
| First non-repeating char | Frequency map + second pass |
| Two Sum | Hash map complement lookup O(n) |
| Debounce | Reset timer on each call |
| Throttle | Flag + cooldown window |
| Flatten array | Recursion or stack; flat(Infinity) |
295. Flatten Array without Array.flat()
Theory
Convert nested arrays into a single flat array by recursively expanding any array element until all values are non-array primitives.
Given input: [1,2,3,[4,5,6,[7,8,[10,11]]],9]
Expected output: [1,2,3,4,5,6,7,8,10,11,9]
Pros & Cons
| Recursive | Iterative (stack) |
|---|---|
| ✅ Clean, readable | ✅ No stack overflow on deep nesting |
| ❌ Stack limit on very deep arrays | Slightly more code |
Implementations
// Approach 1 — Recursive reduce
function flatten(arr) {
return arr.reduce((acc, val) => {
return acc.concat(Array.isArray(val) ? flatten(val) : val)
}, [])
}
// Approach 2 — Iterative with stack
function flattenIterative(arr) {
const stack = [...arr]
const result = []
while (stack.length) {
const next = stack.pop()
if (Array.isArray(next)) {
stack.push(...next)
} else {
result.push(next)
}
}
return result.reverse()
}
// Approach 3 — toString trick (ONLY for numbers — not general)
// [1,[2,3]].toString() → "1,2,3" — avoid in interviews unless askedTest
const input = [1, 2, 3, [4, 5, 6, [7, 8, [10, 11]]], 9]
console.log(flatten(input))
// [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 9]296. Flatten Array without Built-in Methods
Theory
Client round asked to flatten without built-in array methods (no flat, reduce, concat). Use manual loops and recursion.
Input: [1, 2, [3, [4, 5]], 6] → [1, 2, 3, 4, 5, 6]
Implementation
function flatten(arr) {
const result = []
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
const nested = flatten(arr[i])
for (let j = 0; j < nested.length; j++) {
result[result.length] = nested[j]
}
} else {
result[result.length] = arr[i]
}
}
return result
}
console.log(flatten([1, 2, [3, [4, 5]], 6])) // [1, 2, 3, 4, 5, 6]
console.log(flatten([1, 2, 3, [4, 5, 6, [7, 8, [10, 11]]], 9]))
// [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 9]// Iterative — stack, no recursion
function flattenIterative(arr) {
const stack = []
for (let i = arr.length - 1; i >= 0; i--) stack[stack.length] = arr[i]
const result = []
while (stack.length > 0) {
const item = stack.pop()
if (Array.isArray(item)) {
for (let i = item.length - 1; i >= 0; i--) stack[stack.length] = item[i]
} else {
result[result.length] = item
}
}
return result
}| Round | Topic | One-liner |
|---|---|---|
| 1 | Closure | Function + remembered outer scope |
| 1 | Event loop | Sync → micro → macro |
| 1 | Debounce/throttle | Pause vs rate-limit |
| 1 | a11y | Semantic HTML, ARIA, keyboard |
| 1 | Flex vs Grid | 1D vs 2D layout |
| 1 | Missing numbers | Set + loop min to max |
| 1 | CSR vs SSR | Browser vs server render |
| 1 | call/apply/bind | Invoke now vs bind later |
| 1 | Star rating | 10 buttons, hover, ARIA radiogroup |
| 1 | Deep/shallow | structuredClone vs spread |
| 1 | Redux | dispatch → reducer → store |
| 1 | Architecture | Feature folders, layers |
| 2 | Objects | Literals, class, Map, factory |
| 2 | const vs let | const default, let to reassign |
| 2 | Spread/rest | Expand vs collect |
| 2 | Specificity | inline > id > class > element |
| 2 | none vs hidden | Remove layout vs hide in place |
| Client | Hooks in if | Never — same order required |
| Client | useMemo vs effect | useMemo in render, effect after paint |
| Client | Render vs commit | Diff vs DOM update |
| Client | Batching | Multiple setState → one render |
| Client | Flatten no built-ins | for-loop + recursion |
the company tests breadth across 3 rounds — JS, CSS, live coding, and React internals. The client round especially probes whether you understand how React works under the hood.
297. Flatten Nested Array
Theory
Convert [1, [2, [3, 4]], 5] → [1, 2, 3, 4, 5].
Approaches: recursive, iterative with stack, built-in flat(Infinity).
Real Example
// Built-in
;[1, [2, [3, 4]], 5].flat(Infinity) // [1, 2, 3, 4, 5]
;[1, [2, [3, 4]], 5].flat(1) // [1, 2, [3, 4], 5] — one level
// Recursive — interview implementation
function flatten(arr, depth = Infinity) {
if (depth <= 0) return arr.slice()
return arr.reduce((acc, val) => {
if (Array.isArray(val)) return acc.concat(flatten(val, depth - 1))
return acc.concat(val)
}, [])
}
// Iterative — stack (no recursion limit issues)
function flattenIterative(arr) {
const stack = [...arr]
const result = []
while (stack.length) {
const next = stack.pop()
if (Array.isArray(next)) stack.push(...next)
else result.push(next)
}
return result.reverse()
}
console.log(flatten([1, [2, [3, 4]], 5])) // [1, 2, 3, 4, 5]// Real-life: flatten nested API categories for search
const categories = ['All', ['Food', ['North Indian', ['Biryani']]], 'Drinks']
const searchable = flatten(categories)
// ["All", "Food", "North Indian", "Biryani", "Drinks"]298. Focus on accessibility
Theory
Accessibility (a11y) ensures your app is usable by everyone — screen reader users, keyboard-only users, people with motor impairments, and users on slow connections. It's also a legal requirement in many countries (ADA, WCAG 2.1).
Key practices: semantic HTML, ARIA attributes where needed, keyboard navigation, focus management, color contrast, and alt text.
Pros & Cons
| Accessible apps | Ignoring a11y |
|---|---|
| ✅ Larger user base | ❌ Legal risk |
| ✅ Better SEO (semantic HTML) | ❌ Excludes disabled users |
| ✅ Better keyboard UX for power users | ❌ Fails automated audits |
| ✅ Required by enterprise clients | — |
Real-Life Example
// ❌ Inaccessible
<div onClick={handleSubmit}>Submit</div>
<input placeholder="Search" />
<div className="modal">...</div>
// ✅ Accessible
<button type="submit" onClick={handleSubmit}>Submit</button>
<label htmlFor="search-input" className="sr-only">Search restaurants</label>
<input
id="search-input"
type="search"
placeholder="Search restaurants"
aria-describedby="search-hint"
/>
<span id="search-hint" className="sr-only">Type to search by name or cuisine</span>
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
ref={modalRef}
tabIndex={-1}
>
<h2 id="modal-title">Confirm Order</h2>
...
</div>Accessibility checklist:
| Practice | Example |
|---|---|
| Semantic HTML | <button>, <nav>, <main>, <article> |
| Labels | <label htmlFor="email"> or aria-label |
| Keyboard | All actions reachable via Tab + Enter/Space |
| Focus trap | Modal keeps focus inside until closed |
| Color contrast | 4.5:1 ratio for text (WCAG AA) |
| Alt text | <img alt="Chicken Biryani from Spice Garden" /> |
| Skip link | "Skip to main content" for screen readers |
| Live regions | aria-live="polite" for dynamic status updates |
npm install -D @axe-core/react eslint-plugin-jsx-a11y
npx playwright test --grep a11y299. Form Handling in React
Theory
React forms typically use controlled components with a single state object or libraries like React Hook Form / Formik for validation and performance.
Pattern: collect values on change, validate on blur/submit, prevent default on submit.
Real Example
function LoginForm({ onSubmit }) {
const [form, setForm] = useState({ email: '', password: '' })
const [errors, setErrors] = useState({})
const handleChange = (e) => {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }))
}
const handleSubmit = (e) => {
e.preventDefault()
const errs = validate(form)
if (Object.keys(errs).length) return setErrors(errs)
onSubmit(form)
}
return (
<form onSubmit={handleSubmit} noValidate>
<input name="email" value={form.email} onChange={handleChange} />
{errors.email && <span>{errors.email}</span>}
<input name="password" type="password" value={form.password} onChange={handleChange} />
<button type="submit">Login</button>
</form>
)
}300. Given an array, how do you find the second largest element?
Theory
Find the second largest unique element in an array. Common approaches:
| Approach | Time | Space | Notes |
|---|---|---|---|
| Sort + scan | O(n log n) | O(1) or O(n) | Simple but overkill |
| Two variables (one pass) | O(n) | O(1) | Optimal — interview favorite |
| Set + sort | O(n log n) | O(n) | Handles duplicates easily |
Watch for edge cases: duplicates, array length < 2, all same elements.
Pros & Cons
| One-pass (O(n)) | Sort approach |
|---|---|
| Optimal time complexity | Easier to write under pressure |
| O(1) space | Handles duplicates naturally with Set |
| Must handle edge cases carefully | Slower for large arrays |
Real-Life Example
// ✅ Optimal — single pass, O(n) time, O(1) space
function findSecondLargest(arr) {
if (arr.length < 2) return null
let largest = -Infinity
let secondLargest = -Infinity
for (const num of arr) {
if (num > largest) {
secondLargest = largest
largest = num
} else if (num > secondLargest && num < largest) {
secondLargest = num
}
// if num === largest, skip (duplicate)
}
return secondLargest === -Infinity ? null : secondLargest
}
console.log(findSecondLargest([10, 5, 20, 8, 20])) // 10
console.log(findSecondLargest([5, 5, 5])) // null (no second largest)
console.log(findSecondLargest([42, 17])) // 17// Alternative — sort with dedup (simpler, good for interviews if you explain trade-off)
function findSecondLargestSort(arr) {
const unique = [...new Set(arr)]
if (unique.length < 2) return null
unique.sort((a, b) => b - a)
return unique[1]
}// Real-life use: Find runner-up score in a leaderboard
const scores = [95, 87, 95, 72, 88, 87]
const runnerUp = findSecondLargest(scores) // 88Walk-through
[10, 5, 20, 8, 20]
num=10: largest=10, second=-∞
num=5: largest=10, second=5
num=20: largest=20, second=10
num=8: largest=20, second=10 (8 < 10)
num=20: skip (duplicate of largest)
→ return 10301. Handling Complex Codebases
What they're assessing
Navigation, refactoring courage, documentation, and pragmatism.
Strategies to describe
| Strategy | Example |
|---|---|
| Map before change | Dependency graph, feature flags, strangler fig migration |
| Thin slices | Refactor one route/module per sprint |
| Tests as safety net | Characterization tests before big moves |
| Conventions | ADRs, folder structure, CODEOWNERS |
| Tooling | TypeScript strict mode, knip for dead code |
Sample answer skeleton
302. How do you authenticate your React application?
Theory
Round 2 revisits auth with more depth. Cover the full flow: login → token storage → protected routes → refresh → logout → role-based access.
Complete auth flow diagram
User submits login
↓
POST /api/auth/login
↓
Server validates → sets httpOnly cookie (access + refresh)
↓
Frontend stores user object in Context (NOT the token)
↓
ProtectedRoute checks user → render or redirect
↓
API calls include credentials: "include"
↓
401 → try /api/auth/refresh → retry or logout
↓
Logout → POST /api/auth/logout → clear cookie → clear ContextReal Example — Role-based access
function RequireRole({ role, children }: { role: string; children: React.ReactNode }) {
const { user } = useAuth()
if (!user?.roles.includes(role)) return <Navigate to="/unauthorized" />
return children
}
;<Route
path="/admin"
element={
<ProtectedRoute>
<RequireRole role="admin">
<AdminPanel />
</RequireRole>
</ProtectedRoute>
}
/>303. How do you ensure the performance of a React application?
Theory
React performance optimization spans rendering, bundling, network, and measurement. Optimize based on data — profile first, then fix the biggest bottleneck.
Key strategies
| Category | Techniques |
|---|---|
| Rendering | React.memo, useMemo, useCallback, virtualization |
| Bundling | Code splitting, React.lazy, tree shaking |
| Network | TanStack Query caching, prefetch, CDN, compression |
| Images | WebP/AVIF, lazy load, responsive srcset |
| State | Colocate state, avoid unnecessary context re-renders |
| Measurement | React Profiler, Lighthouse, Web Vitals RUM |
Pros & Cons
| Profile-driven optimization | Premature optimization |
|---|---|
| ✅ Fixes real bottlenecks | ❌ Wasted dev time |
| ✅ Measurable improvements | ❌ Added complexity everywhere |
| ✅ Better user experience | ❌ useMemo on cheap operations |
Real-Life Example
// 1. Code splitting — route level
const Analytics = lazy(() => import('./pages/Analytics'))
// 2. Virtualization — 10,000 row table
import { useVirtualizer } from '@tanstack/react-virtual'
// 3. Memoization — after profiling shows slow child re-renders
const DataRow = React.memo(function DataRow({ row, onEdit }) {
return (
<tr>
<td>{row.name}</td>
<td>
<button onClick={() => onEdit(row.id)}>Edit</button>
</td>
</tr>
)
})
// 4. Server state caching
const { data } = useQuery({
queryKey: ['employees', deptId],
queryFn: () => fetchEmployees(deptId),
staleTime: 5 * 60 * 1000,
})
// 5. Defer non-urgent updates
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
// 6. Measure
import { Profiler } from 'react'
;<Profiler
id="EmployeeList"
onRender={(id, phase, duration) => {
if (duration > 16) console.warn(`Slow: ${id} ${duration}ms`)
}}>
<EmployeeList />
</Profiler>Performance budget example:
| Metric | Target |
|---|---|
| Initial JS bundle | < 200 KB gzipped |
| LCP | < 2.5s |
| Component render | < 16ms (60fps) |
304. How Do You Flatten an Array?
Theory
Flattening converts a nested array into a single-level array.
[1, [2, [3, 4]], 5] → [1, 2, 3, 4, 5]Interviewers test: recursion, iteration, built-in knowledge, edge cases (empty arrays, non-array items, depth limit).
Built-in Methods
const nested = [1, [2, [3, 4]], 5]
// Shallow — one level
nested.flat() // [1, 2, [3, 4], 5]
nested.flat(1) // same
// Deep — all levels
nested.flat(Infinity) // [1, 2, 3, 4, 5]
// Alternative
;[].concat(...nested) // [1, [2, [3, 4]], 5] — shallow onlyRecursive — No Built-ins
function flatten(arr) {
const result = []
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flatten(item))
} else {
result.push(item)
}
}
return result
}
flatten([1, [2, [3, 4]], 5]) // [1, 2, 3, 4, 5]reduce
function flatten(arr) {
return arr.reduce((acc, item) => acc.concat(Array.isArray(item) ? flatten(item) : item), [])
}Iterative — Stack (no recursion)
function flattenIterative(arr) {
const stack = [...arr].reverse()
const result = []
while (stack.length) {
const item = stack.pop()
if (Array.isArray(item)) {
stack.push(...item.reverse())
} else {
result.push(item)
}
}
return result
}With Depth Limit
function flattenDepth(arr, depth = 1) {
if (depth <= 0) return arr.slice()
return arr.reduce((acc, item) => {
if (Array.isArray(item)) {
acc.push(...flattenDepth(item, depth - 1))
} else {
acc.push(item)
}
return acc
}, [])
}
flattenDepth([1, [2, [3]]], 1) // [1, 2, [3]]
flattenDepth([1, [2, [3]]], 2) // [1, 2, 3]Senior Touch — Connect to Real Work
// Real-world: flatten nested categories for a filter dropdown
const categories = [
{
id: 1,
name: 'Electronics',
children: [
{ id: 2, name: 'Phones', children: [] },
{ id: 3, name: 'Laptops', children: [] },
],
},
]
function flattenCategories(nodes, result = []) {
for (const node of nodes) {
result.push({ id: node.id, name: node.name })
if (node.children?.length) flattenCategories(node.children, result)
}
return result
}305. How do you implement authentication in React?
Theory
React authentication involves: login flow, token/session storage, protected routes, token refresh, and logout. Security-critical logic must be enforced on the backend — frontend guards are for UX only.
Common approaches:
- JWT in httpOnly cookies (recommended)
- OAuth 2.0 / OIDC (Google, GitHub SSO)
- Session-based auth with cookie
Pros & Cons
| httpOnly cookies | localStorage JWT |
|---|---|
| ✅ XSS-safe — JS can't read token | ❌ Any XSS script steals token |
| ✅ Auto-sent with requests | ✅ Simple to implement |
| ❌ CSRF risk (use SameSite) | ❌ Not recommended for production |
Real Example
// auth/AuthContext.tsx
type User = { id: string; name: string; role: string }
const AuthContext = createContext<{
user: User | null
isLoading: boolean
login: (email: string, password: string) => Promise<void>
logout: () => Promise<void>
} | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
// Check session on mount
useEffect(() => {
fetch('/api/auth/me', { credentials: 'include' })
.then((r) => (r.ok ? r.json() : null))
.then(setUser)
.finally(() => setIsLoading(false))
}, [])
const login = async (email: string, password: string) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (!res.ok) throw new Error('Invalid credentials')
const userData = await res.json()
setUser(userData)
}
const logout = async () => {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' })
setUser(null)
}
return <AuthContext.Provider value={{ user, isLoading, login, logout }}>{children}</AuthContext.Provider>
}
export const useAuth = () => {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be inside AuthProvider')
return ctx
}// Protected route
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth()
const location = useLocation()
if (isLoading) return <FullPageSpinner />
if (!user) return <Navigate to="/login" state={{ from: location }} replace />
return children
}
// App routing
;<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>// Axios interceptor — attach credentials + handle 401
api.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 401) {
// Try refresh token
const refreshed = await fetch('/api/auth/refresh', { credentials: 'include' })
if (refreshed.ok) return api.request(error.config)
window.location.href = '/login'
}
return Promise.reject(error)
},
)306. How do you manage state in large React applications?
Theory — State categories
flowchart TD
S[Application State] --> Server[Server State]
S --> Client[Client State]
S --> URL[URL State]
S --> Form[Form State]
Server --> RQ[TanStack Query / SWR]
Client --> Local[useState / useReducer]
Client --> Global[Zustand / Redux Toolkit]
URL --> Router[URL search params]
Form --> RHF[React Hook Form]Decision framework
| State type | Tool | Example |
|---|---|---|
| Remote/server data | TanStack Query | Restaurant list, user profile |
| Global UI state | Zustand / Context | Theme, sidebar open, cart |
| Complex cross-feature | Redux Toolkit | Multi-step checkout, permissions |
| URL-shareable state | Router search params | Filters, pagination, selected tab |
| Form state | React Hook Form | Checkout form, settings |
| Ephemeral local | useState | Modal open, hover state |
Practical Example — Zustand for cart (a food-delivery platform-relevant)
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
type CartItem = { id: string; name: string; qty: number }
type CartStore = {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
clear: () => void
}
export const useCartStore = create<CartStore>()(
persist(
(set) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id)
if (existing) {
return {
items: state.items.map((i) => (i.id === item.id ? { ...i, qty: i.qty + 1 } : i)),
}
}
return { items: [...state.items, item] }
}),
removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
clear: () => set({ items: [] }),
}),
{ name: 'cart-storage' },
),
)Anti-patterns to avoid
- Storing server data in Redux when React Query handles it better
- Prop drilling through 5+ levels instead of composition or context
- One giant global store with no domain separation
- Duplicating state across URL, store, and component
Interview answer (concise)
307. How does addEventListener work?
Theory
element.addEventListener(type, handler, options) registers a function to run when an event fires on that element.
Options:
capture: true— run in capturing phase (top → target)once: true— remove after first firepassive: true— won't call preventDefault (scroll performance)
Events flow: capturing → target → bubbling. Default listeners use bubbling.
Remove with removeEventListener(type, handler) — must pass the same function reference.
Pros & Cons
| addEventListener | inline onclick |
|---|---|
| ✅ Multiple handlers per event | ❌ One handler only |
| ✅ Capture/bubble control | ❌ No capture option |
| ✅ Remove with removeEventListener | ❌ Harder to clean up |
Real Example
const button = document.getElementById('submit')
// Basic
button.addEventListener('click', handleSubmit)
// With options
button.addEventListener('click', handleSubmit, { once: true })
document.addEventListener('keydown', handleEsc, { capture: true })
// Remove — same reference required
button.removeEventListener('click', handleSubmit)// React — useEffect cleanup pattern
function Modal({ isOpen, onClose }) {
useEffect(() => {
if (!isOpen) return
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleEsc)
return () => document.removeEventListener('keydown', handleEsc)
}, [isOpen, onClose])
}// Event delegation with addEventListener
document.getElementById('menu').addEventListener('click', (e) => {
const item = e.target.closest('[data-action]')
if (item) handleAction(item.dataset.action)
})308. How does error handling work in React?
Answer
Error Boundaries in React is done using Error Boundaries – special components that catch JavaScript errors in their child component tree during rendering, in lifecycle methods, and in constructors.
react-error-boundary is a lightweight library that allows you to use error boundaries easily with functional components in React. It simplifies error handling and provides a FallbackComponent, reset mechanisms, and more.
309. How does JavaScript code execution work?
Theory
JavaScript execution follows a structured pipeline:
1. Parsing
Source code → Tokens → AST (Abstract Syntax Tree)
2. Compilation (V8)
AST → Bytecode (Ignition interpreter) → Optimized machine code (TurboFan)
3. Execution
Global Execution Context created
→ Memory heap (variables, objects)
→ Call stack (function calls)
4. Runtime
Event loop coordinates sync code, microtasks, macrotasksExecution Context contains:
- Variable Environment —
var, function declarations,let/const - Lexical Environment — scope chain for lookups
thisbinding
Phases of execution context:
- Creation phase — hoist variables, create scope chain, set
this - Execution phase — run code line by line
Pros & Cons
| Understanding execution model | Ignoring it |
|---|---|
| ✅ Predict async behavior | ❌ Surprised by hoisting bugs |
| ✅ Debug closure/TDZ issues | ❌ Block UI with sync code |
| ✅ Write performant hot paths | ❌ Memory leaks from closures |
Real-Life Example
// Execution order walkthrough
const API_URL = 'https://api.example.com' // global context
function initApp() {
const userId = getUserId() // execution context for initApp
fetchOrders(userId)
}
function fetchOrders(userId) {
const endpoint = `/orders/${userId}` // new execution context
return fetch(`${API_URL}${endpoint}`)
.then((res) => res.json()) // callback → microtask queue
.then(renderOrders)
}
initApp()
// 1. Global context created
// 2. initApp() pushed to call stack
// 3. fetchOrders() pushed to call stack
// 4. fetch() handed to Web API, fetchOrders pops off stack
// 5. initApp pops off stack
// 6. fetch resolves → .then callback → microtask queue
// 7. renderOrders runs// Call stack overflow
function recurse() {
recurse() // RangeError: Maximum call stack size exceeded
}
// Event loop — non-blocking
console.log('1')
setTimeout(() => console.log('3'), 0)
Promise.resolve().then(() => console.log('2'))
console.log('1b')
// 1 → 1b → 2 → 3310. How does React work internally?
Theory
React's internal pipeline has four major stages when state changes:
State Change
↓
Schedule Update (priority lane assigned)
↓
Render Phase (interruptible)
→ Call component functions
→ Run hooks (linked list on Fiber node)
→ Build new Virtual DOM / Fiber tree
→ Diff against previous tree
↓
Commit Phase (synchronous)
→ Apply DOM mutations
→ Run useLayoutEffect
↓
Browser Paint
↓
Passive Effects (useEffect runs after paint)Fiber is the core data structure — each component instance is a Fiber node with links to parent, child, and sibling. This linked-list tree enables React to pause, resume, and prioritize work.
Pros & Cons of React's internal model
| Pros | Cons |
|---|---|
| Interruptible rendering keeps UI responsive | Mental model is complex for beginners |
| Double buffering (current ↔ work-in-progress tree) | Debugging across async boundaries is hard |
| Hooks stored as linked list — predictable order | Rules of Hooks exist because of this design |
Real-Life Example
function SearchPage() {
const [query, setQuery] = useState('') // Hook #1 on Fiber
const [results, setResults] = useState([]) // Hook #2 on Fiber
useEffect(() => {
// Scheduled AFTER browser paint
fetch(`/api/search?q=${query}`)
.then((r) => r.json())
.then(setResults)
}, [query])
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultList items={results} />
</div>
)
}
// What happens when user types "p":
// 1. onChange fires → setQuery("p") enqueued on Fiber
// 2. Render phase: SearchPage re-runs, hooks return new query
// 3. ResultList may or may not re-render (depends on memo)
// 4. Commit: input value updated in DOM
// 5. Browser paints
// 6. useEffect runs because query changed → API call firesKey internals to mention
| Concept | Purpose |
|---|---|
| Fiber node | Unit of work; holds state, props, hooks |
| Reconciliation | Diff algorithm — O(n) heuristic |
| Update queue | Circular linked list of pending state updates |
| Lanes | Priority system for concurrent features |
Object.is | Comparison for bailout (same value → skip re-render) |
311. How does the JavaScript Engine work?
Theory
A JavaScript engine (V8 in Chrome/Node, SpiderMonkey in Firefox, JavaScriptCore in Safari) compiles and executes JS code. Modern engines use a multi-tier compilation pipeline:
- Parser — converts source code into an Abstract Syntax Tree (AST)
- Ignition interpreter — quickly generates bytecode and starts execution
- Sparkplug (baseline compiler) — compiles hot functions to faster baseline machine code
- Maglev / TurboFan (optimizing compiler) — recompiles very hot functions with type specialization
- Garbage Collector — reclaims unreachable memory (generational mark-and-sweep)
Pros & Cons of JIT compilation
| Pros | Cons |
|---|---|
| Fast startup (interpret first) | Deoptimization penalties if types change |
| Hot code runs near-native speed | Memory overhead for compiled code caches |
| Adaptive — optimizes what matters | Hard to predict performance without profiling |
Real-Life Example
// Engine starts in interpreter (fast startup)
function add(a, b) {
return a + b
}
// Called 10,000 times with numbers → TurboFan optimizes for numbers
for (let i = 0; i < 10000; i++) add(i, i)
// Called once with string → DEOPTIMIZATION (bailout to slower path)
add('hello', 'world') // still works, but function may re-optimizeTakeaway for interviews: Write consistent types in hot loops. Avoid mixing types in functions called millions of times.
312. How does virtual table (vtable) work?
Theory
Enables runtime polymorphism — call the correct overridden function through a base pointer.
Mechanism
Each class with virtual functions has a vtable (array of function pointers)
Each object has a vptr (hidden pointer to its class's vtable)
class Animal {
virtual void speak() { cout << "..."; }
};
class Dog : public Animal {
void speak() override { cout << "Woof"; }
};
Animal* a = new Dog();
a->speak(); // "Woof" — resolved via vtable at runtimeMemory layout (simplified)
Dog object:
┌──────────┐
│ vptr │ ──→ Dog vtable: [ speak → Dog::speak, dtor → Dog::~Dog ]
├──────────┤
│ data... │
└──────────┘Cost of virtual functions
- Extra memory: vptr per object + vtable per class
- Indirect function call (cannot inline easily)
- Prevents some optimizations
Pure virtual — abstract class
class Shape {
public:
virtual double area() const = 0; // pure virtual
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
double area() const override { return 3.14159 * radius * radius; }
};Interview answer
313. How to handle forms in React?
Answer
You can handle forms using controlled components (state) or uncontrolled components (refs). Controlled forms provide better control over input validation and behavior.
314. How to measure performance in a React application?
Theory
React performance measurement spans lab metrics (controlled environment) and field metrics (real users). Key areas: render performance, bundle size, network timing, and Core Web Vitals.
Pros & Cons
| Tool | Pros | Cons |
|---|---|---|
| React DevTools Profiler | Flame charts, render duration | Dev environment only |
| Lighthouse | Comprehensive audit, CI-friendly | Lab data, not real users |
| Web Vitals (RUM) | Real user data | Needs instrumentation |
| Bundle analyzer | Finds large dependencies | Doesn't measure runtime |
Real-Life Example
import { Profiler } from 'react'
import { onLCP, onINP, onCLS } from 'web-vitals'
// 1. React Profiler — measure component render time
function onRenderCallback(id, phase, actualDuration) {
if (actualDuration > 16) {
// longer than one frame
console.warn(`Slow render: ${id} took ${actualDuration}ms`)
}
}
;<Profiler id="RestaurantList" onRender={onRenderCallback}>
<RestaurantList />
</Profiler>
// 2. Web Vitals in production
onLCP((metric) => analytics.track('LCP', metric))
onINP((metric) => analytics.track('INP', metric))
onCLS((metric) => analytics.track('CLS', metric))
// 3. Bundle analysis (package.json script)
// "analyze": "source-map-explorer build/static/js/*.js"Performance budget example:
- Initial JS bundle: < 200KB gzipped
- LCP: < 2.5s on 4G
- Component render: < 16ms (60fps)
315. How would you build a tool like Create React App?
Theory
Create React App (CRA) is a CLI tool that scaffolds a production-ready React project with zero configuration. Building one requires: project templating, bundler setup, dev server, build pipeline, and extensibility.
Core components to build
| Component | Technology |
|---|---|
| CLI scaffolding | commander or yargs |
| Project template | Copied files (package.json, src/, public/) |
| Bundler | Webpack or Vite |
| Dev server | HMR via webpack-dev-server or Vite |
| Transpilation | Babel (JSX, modern syntax) or esbuild |
| CSS handling | PostCSS, CSS Modules, Sass loader |
| Build optimization | Minification, tree shaking, code splitting |
| Testing setup | Jest + React Testing Library |
Pros & Cons
| Pros | Cons |
|---|---|
| Zero-config developer experience | Hides complexity — hard to customize |
| Consistent project structure | Ejected config is irreversible (CRA) |
| Fast onboarding | Opinionated — may not fit all projects |
Real-Life Example (simplified CLI)
#!/usr/bin/env node
const { program } = require('commander')
const fs = require('fs-extra')
const path = require('path')
const { execSync } = require('child_process')
program
.name('create-my-app')
.argument('<project-name>')
.action(async (projectName) => {
const targetDir = path.join(process.cwd(), projectName)
const templateDir = path.join(__dirname, '../template')
// 1. Copy template
await fs.copy(templateDir, targetDir)
// 2. Customize package.json
const pkg = await fs.readJson(path.join(targetDir, 'package.json'))
pkg.name = projectName
await fs.writeJson(path.join(targetDir, 'package.json'), pkg, {
spaces: 2,
})
// 3. Install dependencies
execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
console.log(`\n✅ Created ${projectName}`)
console.log(` cd ${projectName} && npm start`)
})
program.parse()template/
├── package.json # react, react-dom, vite
├── vite.config.js # bundler config
├── index.html
├── src/
│ ├── main.jsx
│ └── App.jsx
└── public/Modern alternative: Use Vite instead of Webpack for faster dev experience (what most CRA successors do).
316. How would you design a frontend system serving millions of users daily?
Theory — Full-system view
flowchart TB
Users[Millions of Users] --> CDN[Global CDN]
CDN --> LB[Load Balancer]
LB --> SSR[SSR / Edge Nodes]
LB --> Static[Static Asset Origin]
SSR --> BFF[BFF / API Gateway]
BFF --> Services[Microservices]
subgraph Client
PWA[PWA + Service Worker]
RUM[RUM / Analytics]
end
Users --> Client
Client --> CDNKey design decisions
1. Delivery & performance
- Multi-region CDN with edge caching
- SSR/ISR/RSC for fast first paint; static where possible
- Code splitting + route-based chunks; bundle budgets in CI
- Image optimization pipeline (WebP/AVIF, responsive, lazy load)
- HTTP/3, Brotli compression, early hints (
103)
2. Resilience
- Graceful degradation — core flows work if recommendations API fails
- Circuit breakers on client (stop retrying failing endpoints)
- Error boundaries per feature; fallback UI
- Retry with exponential backoff + jitter
- Offline support for critical paths (view past orders)
3. Scalability patterns
- BFF (Backend for Frontend) — tailor APIs per client, reduce over-fetching
- GraphQL or typed REST with field selection
- Feature flags — gradual rollouts, kill switches
- Micro-frontends (only if teams are truly independent) — Module Federation or iframe shells
4. Observability
- Real User Monitoring (RUM) for Web Vitals percentiles (p75, p95)
- Distributed tracing (correlation IDs from client → API)
- Error tracking (Sentry) with release tags
- Synthetic monitoring from multiple regions
5. Security
- CSP headers, Subresource Integrity for third-party scripts
- httpOnly secure cookies for auth
- Rate limiting awareness on client (debounce, disable double-submit)
- XSS prevention — never
dangerouslySetInnerHTMLwithout sanitization
6. Release & operations
- Blue-green or canary deploys
- Preview environments per PR
- Automated rollback on error rate spike
- Load testing (k6) simulating peak lunch/dinner traffic
Practical SLA thinking (a food-delivery platform context)
Numbers to mention in interviews
- Bundle size budget: e.g. < 200KB gzipped initial JS
- API p95 latency budget: < 300ms for critical paths
- Error rate SLO: < 0.1% client-side unhandled errors
- Cache hit ratio on CDN: > 90% for static assets
Use the STAR method: Situation → Task → Action → Result.
Keep answers 60–90 seconds. Quantify impact where possible.
317. Implement Lodash's flatten method
Theory
Lodash's flatten converts a nested array into a flatter structure. flatten(array) flattens one level deep. flattenDeep(array) flattens all levels recursively.
This is common when APIs return nested category trees, nested form field groups, or deeply nested menu structures that need a flat list for rendering or search.
Pros & Cons
| Pros | Cons |
|---|---|
| Simplifies nested data for UI rendering | Loses structural hierarchy unless you keep depth metadata |
| Enables flat search/filter across nested data | Deep recursion can hit stack limits on very deep nesting |
Native flat(Infinity) exists in modern JS | flat creates a new array (memory cost) |
Real-Life Example
function flatten(arr, depth = 1) {
if (!Array.isArray(arr)) return arr
return depth > 0
? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatten(val, depth - 1) : val), [])
: arr.slice()
}
function flattenDeep(arr) {
return flatten(arr, Infinity)
}
// Real-life: Flatten nested menu categories for search autocomplete
const menu = ['Home', ['Food', ['North Indian', ['Biryani', 'Kebab']], 'South Indian'], ['Drinks', 'Desserts']]
const searchableItems = flattenDeep(menu)
// ["Home", "Food", "North Indian", "Biryani", "Kebab", "South Indian", "Drinks", "Desserts"]318. Implement responsive design from the beginning
Theory
Responsive design ensures your UI works across mobile, tablet, and desktop. Build mobile-first
(start with small screens, add breakpoints up) using CSS Grid, Flexbox, relative units (rem, %, vw), and container
queries.
Don't treat responsive as an afterthought — retrofitting is expensive.
Pros & Cons
| Mobile-first responsive | Desktop-only then retrofit |
|---|---|
| ✅ Works on 60%+ mobile traffic | ❌ Expensive rework |
| ✅ Forces content prioritization | ❌ Broken layouts on phones |
| ✅ Better Core Web Vitals on mobile | ❌ Lost users on mobile |
Real-Life Example
/* Mobile-first — base styles for small screens */
.product-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
padding: 1rem;
}
/* Tablet */
@media (min-width: 768px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
padding: 1.5rem;
}
}
/* Desktop */
@media (min-width: 1024px) {
.product-grid {
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
padding: 2rem;
}
}// React — responsive with custom hook
function ProductPage() {
const isMobile = useMediaQuery('(max-width: 768px)')
return <div className="product-page">{isMobile ? <MobileProductLayout /> : <DesktopProductLayout />}</div>
}
// Tailwind example
;<div className="grid grid-cols-1 gap-4 p-4 md:grid-cols-2 md:p-6 lg:grid-cols-4">
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>319. Inline 5 Divs without Flex/Margin/Padding
Theory
Without flex, margin, or padding, place 5 divs in a horizontal row using display: inline-block. Inline-block elements sit side-by-side like text, respecting width/height unlike pure inline.
Watch for: whitespace gaps between inline-block elements (font-size zero hack on parent, or remove whitespace in HTML).
Pros & Cons
| inline-block | float (alternative) |
|---|---|
| ✅ Simple, in document flow | ❌ Removed from normal flow |
| ✅ Respects width/height | Needs clearfix |
| ❌ Whitespace gap between items | No gap issue |
Real Example
<div class="row">
<div class="box">1</div>
<!--
-->
<div class="box">2</div>
<!--
-->
<div class="box">3</div>
<!--
-->
<div class="box">4</div>
<!--
-->
<div class="box">5</div>
</div>/* Method 1 — font-size zero on parent */
.row {
font-size: 0; /* removes whitespace gap */
}
.box {
display: inline-block;
box-sizing: border-box;
border: 1px solid #333;
background: #4a90d9;
width: 20%;
height: 100px;
font-size: 16px; /* reset for content */
line-height: 100px;
text-align: center;
}
/* Method 2 — HTML comments remove whitespace (shown above) */
/* Method 3 — float (if inline-block not accepted) */
.box-float {
float: left;
width: 20%;
height: 100px;
}
.row-float::after {
display: table;
clear: both;
content: '';
}<!-- Alternative: calc width -->
<style>
.box-calc {
display: inline-block;
box-sizing: border-box;
background: coral;
width: calc(100% / 5);
height: 80px;
}
</style>320. Is JavaScript tightly coupled or loosely coupled?
Theory
Coupling measures how dependent one module is on another's internal details.
- Tightly coupled — modules know each other's internals; changing one breaks others
- Loosely coupled — modules interact through well-defined interfaces; changes are isolated
JavaScript as a language is flexible and supports both styles. By default, without discipline, JS code tends toward tight coupling — global variables, direct DOM manipulation, shared mutable state.
With good patterns, JS achieves loose coupling — modules, dependency injection, event-driven architecture, interfaces (TypeScript).
Pros & Cons
| Tight Coupling | Loose Coupling |
|---|---|
| Faster to write initially | More upfront design |
| Direct access — less boilerplate | Easier to test in isolation |
| Hard to maintain at scale | Easier to swap implementations |
| Changes ripple across codebase | Better team parallelization |
Real-Life Example
// ❌ Tightly coupled — OrderService directly depends on Stripe internals
class OrderService {
placeOrder(cart) {
const stripe = new Stripe('sk_live_xxx')
stripe.charges.create({ amount: cart.total, currency: 'inr' })
document.getElementById('status').innerText = 'Paid' // DOM coupling too
}
}
// ✅ Loosely coupled — depends on abstractions (interfaces)
class OrderService {
constructor(paymentGateway, notifier) {
this.paymentGateway = paymentGateway // injected
this.notifier = notifier
}
async placeOrder(cart) {
await this.paymentGateway.charge(cart.total)
this.notifier.notify('order_placed', cart)
}
}
// Swap Stripe for Razorpay without touching OrderService
const orderService = new OrderService(new RazorpayGateway(), new EmailNotifier())// React loose coupling — composition over inheritance
function CheckoutPage() {
return (
<PaymentProvider gateway="razorpay">
<CartSummary />
<PaymentForm /> {/* doesn't know which gateway */}
<OrderConfirmation />
</PaymentProvider>
)
}Interview answer (concise)
321. Join/Merge Objects from Object List
Theory
Given an array of objects, merge them into one object. Common variants:
| Variant | Behavior |
|---|---|
| Shallow merge | Later keys overwrite earlier (Object.assign / spread) |
| Deep merge | Nested objects merged recursively |
| Join by key | Group array items by a property into { [key]: [...] } |
the company likely asks shallow merge or deep merge of an object array.
Shallow Merge — Later Wins
const list = [
{ a: 1, b: 2 },
{ b: 3, c: 4 },
{ c: 5, d: 6 },
]
function mergeObjects(list) {
return list.reduce((acc, obj) => ({ ...acc, ...obj }), {})
}
console.log(mergeObjects(list))
// { a: 1, b: 3, c: 5, d: 6 }// Alternative — Object.assign
function mergeObjects(list) {
return Object.assign({}, ...list)
}Deep Merge
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function deepMerge(target, source) {
const result = { ...target }
for (const key of Object.keys(source)) {
const sourceVal = source[key]
const targetVal = result[key]
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
result[key] = deepMerge(targetVal, sourceVal)
} else {
result[key] = sourceVal
}
}
return result
}
function mergeObjectList(list) {
return list.reduce((acc, obj) => deepMerge(acc, obj), {})
}
const list = [
{ user: { name: 'Amit', age: 28 }, role: 'dev' },
{ user: { city: 'Mumbai' }, role: 'senior' },
{ tags: ['react', 'ts'] },
]
console.log(mergeObjectList(list))
// {
// user: { name: "Amit", age: 28, city: "Mumbai" },
// role: "senior",
// tags: ["react", "ts"]
// }Join/Group by Key (alternate interpretation)
// Merge array of { id, ...fields } into { [id]: fields }
const users = [
{ id: 'u1', name: 'Amit', role: 'dev' },
{ id: 'u2', name: 'Priya', role: 'qa' },
{ id: 'u1', team: 'frontend' }, // same id — merge fields
]
function joinById(list, key = 'id') {
return list.reduce((acc, item) => {
const id = item[key]
const { [key]: _, ...rest } = item
acc[id] = { ...acc[id], ...rest }
return acc
}, {})
}
console.log(joinById(users))
// { u1: { name: "Amit", role: "dev", team: "frontend" }, u2: { name: "Priya", role: "qa" } }React Usage
function MergedConfig({ configSources }) {
const merged = useMemo(() => configSources.reduce((acc, cfg) => ({ ...acc, ...cfg }), {}), [configSources])
return <AppSettings theme={merged.theme} apiUrl={merged.apiUrl} />
}322. Keep dependencies updated and remove unused packages
Theory
Outdated dependencies carry security vulnerabilities, miss performance improvements, and create compatibility issues. Unused packages bloat your bundle and increase attack surface.
Maintain a regular update cadence: weekly npm audit, monthly minor updates, careful major version upgrades with changelog review.
Pros & Cons
| Updated dependencies | Neglected dependencies |
|---|---|
| ✅ Security patches applied | ❌ Known CVEs in production |
| ✅ Smaller bundles (tree-shaking improvements) | ❌ Breaking changes pile up |
| ✅ New features and bug fixes | ❌ Harder to upgrade later |
| ❌ Update time / testing cost | — |
Real-Life Example
npm audit
npm audit fix
# Find outdated packages
npm outdated
# Remove unused dependencies
npx depcheck
# Analyze bundle size
npx source-map-explorer build/static/js/*.js
# Safe update workflow
npm update # minor/patch updates
npx npm-check-updates -u # see major updates available
npm install # apply
npm test # verify nothing broke// package.json — CI scripts
{
"scripts": {
"audit": "npm audit --audit-level=moderate",
"depcheck": "depcheck",
"analyze": "source-map-explorer build/static/js/*.js"
}
}Dependency hygiene checklist:
| Action | Frequency |
|---|---|
npm audit | Every PR / weekly |
| Remove unused packages | Monthly |
| Update patch/minor versions | Monthly |
| Review major version changelogs | Quarterly |
Lock file committed (package-lock.json) | Always |
| Dependabot / Renovate in CI | Automated |
323. Largest Frontend Systems Built
What they're assessing
Scale (users, teams, code volume), your role (lead vs contributor), and impact.
Framework
Situation — product, users, team size
Task — your ownership area
Action — architecture, migrations, standards you drove
Result — metrics (LCP, crash rate, delivery speed, revenue)Sample answer skeleton
Topics to mention if true for you
- Micro-frontends / monorepo (Nx, Turborepo)
- Design system at scale
- Auth (SSO, RBAC)
- Accessibility compliance (WCAG)
- Cross-browser / legacy IE migration
324. Lift state up only when required
Theory
Lifting state up means moving shared state to the closest common ancestor of components that need it. Only lift when siblings need to share data or a parent must coordinate children.
Don't lift state prematurely — colocate state as close as possible to where it's used. Local state that only one component needs should stay local.
Pros & Cons
| Lift when needed | Lift too early |
|---|---|
| ✅ Siblings stay in sync | ❌ Unnecessary re-renders of parent tree |
| ✅ Single source of truth | ❌ Props passed through components that don't care |
| ✅ Predictable data flow | ❌ Harder to refactor later |
Real-Life Example
// ❌ Over-lifted — theme in App when only Settings page needs it
function App() {
const [fontSize, setFontSize] = useState(16) // only Settings uses this
return (
<>
<Header fontSize={fontSize} />
<Settings fontSize={fontSize} setFontSize={setFontSize} />
</>
)
}
// ✅ Colocated — state lives where it's used
function Settings() {
const [fontSize, setFontSize] = useState(16)
return <FontSizeSlider value={fontSize} onChange={setFontSize} />
}
// ✅ Correctly lifted — two siblings share selected tab
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview')
return (
<>
<TabBar activeTab={activeTab} onTabChange={setActiveTab} />
<TabPanel activeTab={activeTab} />
</>
)
}325. Live search box — vanilla JavaScript
Theory
a fintech app explicitly tests vanilla JS — reaching for React is a red flag. Build a live search with:
- Debounced API calls — don't hit server on every keystroke
- Keyboard navigation — ArrowUp/Down, Enter, Escape
- Click outside to close — mousedown on document
- Accessibility — ARIA combobox pattern
Pros & Cons
| Vanilla JS | React for this task |
|---|---|
| ✅ Shows DOM fundamentals | ❌ a fintech app Round 2 red flag |
| ✅ No build step in interview | Overkill for 90-min test |
| ✅ Proves you understand events |
Real-Life Example — Full implementation
<div class="search" id="search-root">
<label for="search-input" class="sr-only">Search transactions</label>
<input
id="search-input"
type="search"
role="combobox"
aria-expanded="false"
aria-controls="search-results"
aria-autocomplete="list"
placeholder="Search transactions..."
autocomplete="off" />
<ul id="search-results" role="listbox" hidden></ul>
</div>function debounce(fn, delay) {
let timer
const debounced = (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
debounced.cancel = () => clearTimeout(timer)
return debounced
}
class LiveSearch {
constructor(root) {
this.root = root
this.input = root.querySelector('#search-input')
this.list = root.querySelector('#search-results')
this.items = []
this.activeIndex = -1
this.abortController = null
this.debouncedSearch = debounce(this.search.bind(this), 300)
this.bindEvents()
}
bindEvents() {
this.input.addEventListener('input', () => {
const q = this.input.value.trim()
if (!q) return this.close()
this.debouncedSearch(q)
})
this.input.addEventListener('keydown', (e) => this.handleKeydown(e))
// Click outside to close
document.addEventListener('mousedown', (e) => {
if (!this.root.contains(e.target)) this.close()
})
}
async search(query) {
if (this.abortController) this.abortController.abort()
this.abortController = new AbortController()
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: this.abortController.signal,
})
const data = await res.json()
this.items = data.results ?? []
this.render()
this.open()
} catch (err) {
if (err.name !== 'AbortError') console.error(err)
}
}
render() {
this.list.innerHTML = this.items
.map(
(item, i) =>
`<li role="option" id="option-${i}" aria-selected="${i === this.activeIndex}">
${item.label}
</li>`,
)
.join('')
this.list.querySelectorAll('li').forEach((li, i) => {
li.addEventListener('mousedown', (e) => {
e.preventDefault() // prevent input blur before click
this.select(i)
})
})
}
handleKeydown(e) {
if (!this.items.length) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
this.activeIndex = Math.min(this.activeIndex + 1, this.items.length - 1)
this.highlight()
break
case 'ArrowUp':
e.preventDefault()
this.activeIndex = Math.max(this.activeIndex - 1, 0)
this.highlight()
break
case 'Enter':
e.preventDefault()
if (this.activeIndex >= 0) this.select(this.activeIndex)
break
case 'Escape':
this.close()
break
}
}
highlight() {
this.list.querySelectorAll('li').forEach((li, i) => {
li.setAttribute('aria-selected', i === this.activeIndex)
li.classList.toggle('active', i === this.activeIndex)
})
this.input.setAttribute('aria-activedescendant', `option-${this.activeIndex}`)
}
select(index) {
this.input.value = this.items[index].label
this.close()
// navigate or callback
console.log('Selected:', this.items[index])
}
open() {
this.list.hidden = false
this.input.setAttribute('aria-expanded', 'true')
this.activeIndex = 0
this.highlight()
}
close() {
this.list.hidden = true
this.input.setAttribute('aria-expanded', 'false')
this.activeIndex = -1
this.debouncedSearch.cancel()
}
destroy() {
this.debouncedSearch.cancel()
if (this.abortController) this.abortController.abort()
}
}
new LiveSearch(document.getElementById('search-root'))What a fintech app evaluates: debounce + cancel, AbortController, keyboard UX, click-outside, no framework dependency.
326. Loading, Error & Empty States
Theory
Every data-fetching UI needs four states: loading, error, empty, success. Handle all explicitly — never show a blank screen.
Pattern: early returns or a unified AsyncBoundary component.
Real Example
function DataView({ query }) {
const { data, isLoading, error, isFetching } = query
if (isLoading) return <Skeleton rows={5} />
if (error) return <ErrorBanner message={error.message} onRetry={query.refetch} />
if (!data?.length) return <EmptyState title="No results" action={<CreateButton />} />
return (
<>
{isFetching && <RefreshIndicator />}
<ItemList items={data} />
</>
)
}327. Local Storage vs Cookies
Theory
Both store data in the browser, but with different capabilities and security models.
| localStorage | Cookies | |
|---|---|---|
| Capacity | ~5–10 MB | ~4 KB per cookie |
| Sent with requests | No | Yes (every HTTP request to domain) |
| Expiry | Never (until cleared) | Set via Expires / Max-Age |
| Accessible from JS | Yes | Yes (unless httpOnly) |
| Secure for tokens | ❌ XSS vulnerable | ✅ httpOnly + Secure + SameSite |
| API | Synchronous | Document.cookie or server Set-Cookie |
Also consider: sessionStorage (tab-scoped), IndexedDB (large structured data).
Pros & Cons
| localStorage | httpOnly Cookies |
|---|---|
| ✅ Large capacity | ✅ Not accessible to JS — XSS safe |
| ✅ Simple API | ✅ Auto-sent — good for auth |
| ❌ XSS can steal data | ❌ CSRF risk (mitigate with SameSite) |
| ❌ Synchronous — blocks main thread | ❌ 4KB limit |
Real-Life Example
// localStorage — user preferences (non-sensitive)
localStorage.setItem('theme', 'dark')
localStorage.setItem('recentSearches', JSON.stringify(['react', 'typescript']))
const theme = localStorage.getItem('theme')
// ❌ NEVER store auth tokens in localStorage
localStorage.setItem('token', jwt) // XSS can steal this
// ✅ Auth via httpOnly cookie (set by server)
// Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
// sessionStorage — form draft (cleared on tab close)
sessionStorage.setItem('draftOrder', JSON.stringify(cartItems))// Custom hook for localStorage
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initial
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}328. Map, Filter & Reduce
Theory
These are higher-order array methods that transform data without mutating the original array.
| Method | Returns | Purpose |
|---|---|---|
map | New array (same length) | Transform each element |
filter | New array (≤ length) | Keep elements matching condition |
reduce | Single value | Accumulate into one result |
All accept a callback (element, index, array) and optional thisArg.
Pros & Cons
| Pros | Cons |
|---|---|
| Declarative, readable | Can't short-circuit (use for loop with break) |
| Immutable — no side effects | Chaining creates intermediate arrays |
| Chainable | Slightly slower than manual loop for hot paths |
Real-Life Example
const products = [
{ id: 1, name: 'Biryani', price: 299, category: 'main', rating: 4.5 },
{ id: 2, name: 'Naan', price: 49, category: 'bread', rating: 4.0 },
{ id: 3, name: 'Lassi', price: 79, category: 'drink', rating: 4.2 },
{ id: 4, name: 'Kebab', price: 199, category: 'main', rating: 4.8 },
]
// map — transform for UI
const menuItems = products.map((p) => ({
label: `${p.name} — ₹${p.price}`,
value: p.id,
}))
// filter — show only highly rated mains under ₹300
const affordable = products.filter((p) => p.category === 'main' && p.rating >= 4.5 && p.price <= 300)
// reduce — calculate cart total
const cart = [
{ id: 1, qty: 2 },
{ id: 2, qty: 4 },
]
const total = cart.reduce((sum, item) => {
const product = products.find((p) => p.id === item.id)
return sum + product.price * item.qty
}, 0)
// 299*2 + 49*4 = 794
// Chaining
const summary = products
.filter((p) => p.rating >= 4.0)
.map((p) => p.name)
.reduce((acc, name) => acc + name + ', ', '')
// reduce can implement map and filter
const doubled = [1, 2, 3].reduce((acc, n) => [...acc, n * 2], [])// React — primary list rendering pattern
function ProductList({ products }) {
return (
<ul>
{products
.filter((p) => p.inStock)
.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</ul>
)
}329. Measure performance using React DevTools and Lighthouse
Theory
You can't optimize what you don't measure. Use React DevTools Profiler for component render performance and Lighthouse for overall web vitals (LCP, INP, CLS, TTI, bundle size).
Set performance budgets and enforce them in CI to prevent regressions.
Pros & Cons
| Measuring performance | Guessing performance |
|---|---|
| ✅ Data-driven optimization | ❌ Premature memoization everywhere |
| ✅ Catch regressions in CI | ❌ Users report slowness first |
| ✅ Prioritize biggest wins | ❌ Waste time on non-issues |
| ❌ Tooling setup time | — |
Real-Life Example
React DevTools Profiler
1. Install React DevTools browser extension
2. Open Profiler tab → click Record
3. Interact with your app (navigate, type, scroll)
4. Stop recording → analyze:
- Flame chart: which components rendered and how long
- Ranked chart: slowest components first
- "Why did this render?" (React 19+)// Programmatic profiling in development
import { Profiler } from 'react'
function onRenderCallback(id: string, phase: 'mount' | 'update', actualDuration: number) {
if (actualDuration > 16) {
// slower than 1 frame at 60fps
console.warn(`[Profiler] ${id} (${phase}): ${actualDuration.toFixed(1)}ms`)
}
}
;<Profiler id="ProductList" onRender={onRenderCallback}>
<ProductList products={products} />
</Profiler>Lighthouse
npx lighthouse https://your-app.com --view
# CI integration
npm install -D @lhci/cli
# lighthouserc.js — fail CI if scores drop below thresholdWeb Vitals in production
import { onLCP, onINP, onCLS } from 'web-vitals'
function sendToAnalytics(metric) {
// Google Analytics, Datadog, Sentry
analytics.track(metric.name, {
value: metric.value,
page: window.location.pathname,
})
}
onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)Performance budget example:
| Metric | Budget |
|---|---|
| Initial JS bundle | < 200 KB gzipped |
| LCP | < 2.5s |
| INP | < 200ms |
| CLS | < 0.1 |
| Component render | < 16ms (60fps) |
Quick Revision Cheat Sheet
| # | Practice | One-liner |
|---|---|---|
| 1 | Small components | One responsibility, ~150 lines max |
| 2 | Lift state wisely | Colocate first; lift only for shared siblings |
| 3 | Custom hooks | Extract duplicate stateful logic |
| 4 | Immutable state | Always new reference: spread, map, filter |
| 5 | Unique keys | Stable IDs from data, not index |
| 6 | Re-renders | State, parent, context trigger renders |
| 7 | Memoization | Profile first; useMemo/useCallback when needed |
| 8 | API states | Always handle loading, error, empty, success |
| 9 | Code splitting | React.lazy per route + Suspense fallback |
| 10 | Separate logic | utils + services + hooks + UI components |
| 11 | Folder structure | Feature-based, not type-based |
| 12 | Error Boundaries | Per-route fallback, log to Sentry |
| 13 | Prop drilling | Context/composition/Zustand when >2 levels |
| 14 | useEffect cleanup | Close WS, clear timers, abort fetch |
| 15 | Unit tests | Test behavior, critical paths, RTL |
| 16 | Accessibility | Semantic HTML, ARIA, keyboard, contrast |
| 17 | Responsive | Mobile-first, Grid/Flexbox, breakpoints |
| 18 | Env variables | VITE_ prefix, never secrets in frontend |
| 19 | Dependencies | npm audit, depcheck, regular updates |
| 20 | Performance | React Profiler + Lighthouse + Web Vitals RUM |
These 20 practices form the foundation of production-grade React development. Apply them from day one of a project — retrofitting is always harder than building right.
330. Multi-field form — PAN, Aadhaar
Theory
a fintech app Round 3 tests KYC-style forms — PAN, Aadhaar, with strict validation, error states, and state design before coding. They watch your architecture decisions before you write the first <input>.
Validation rules (typical):
- PAN:
AAAAA9999A— 5 letters, 4 digits, 1 letter - Aadhaar: 12 digits, Verhoeff checksum (or format-only in interviews)
Pros & Cons — form libraries vs custom
| React Hook Form | Manual useState |
|---|---|
| ✅ Less re-renders (uncontrolled) | ✅ Full control in interview |
| ✅ Built-in validation | ❌ More boilerplate |
| Interview: either works if state design is clean |
Real-Life Example
// validators.ts — pure functions, easy to test
const PAN_REGEX = /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/
const AADHAAR_REGEX = /^\d{12}$/
export function validatePAN(value: string): string | null {
if (!value) return 'PAN is required'
if (!PAN_REGEX.test(value.toUpperCase())) return 'Invalid PAN format (e.g. ABCDE1234F)'
return null
}
export function validateAadhaar(value: string): string | null {
const digits = value.replace(/\s/g, '')
if (!digits) return 'Aadhaar is required'
if (!AADHAAR_REGEX.test(digits)) return 'Aadhaar must be 12 digits'
return null
}
export function validateFullName(value: string): string | null {
if (!value.trim()) return 'Full name is required'
if (value.trim().length < 3) return 'Name too short'
return null
}// KYCForm.tsx — controlled fields with per-field errors
type FormState = {
fullName: string
pan: string
aadhaar: string
consent: boolean
}
type FormErrors = Partial<Record<keyof FormState, string>>
const INITIAL: FormState = {
fullName: '',
pan: '',
aadhaar: '',
consent: false,
}
function KYCForm() {
const [values, setValues] = useState<FormState>(INITIAL)
const [errors, setErrors] = useState<FormErrors>({})
const [touched, setTouched] = useState<Partial<Record<keyof FormState, boolean>>>({})
const validators: Record<string, (v: string) => string | null> = {
fullName: validateFullName,
pan: validatePAN,
aadhaar: validateAadhaar,
}
const setField = (field: keyof FormState, value: string | boolean) => {
setValues((prev) => ({ ...prev, [field]: value }))
if (touched[field] && typeof value === 'string') {
const error = validators[field]?.(value) ?? null
setErrors((prev) => ({ ...prev, [field]: error ?? undefined }))
}
}
const handleBlur = (field: keyof FormState) => {
setTouched((prev) => ({ ...prev, [field]: true }))
const val = values[field]
if (typeof val === 'string') {
const error = validators[field]?.(val)
setErrors((prev) => ({ ...prev, [field]: error ?? undefined }))
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const newErrors: FormErrors = {}
;(['fullName', 'pan', 'aadhaar'] as const).forEach((field) => {
const err = validators[field](values[field] as string)
if (err) newErrors[field] = err
})
if (!values.consent) newErrors.consent = 'Consent is required'
setErrors(newErrors)
setTouched({ fullName: true, pan: true, aadhaar: true, consent: true })
if (Object.keys(newErrors).length === 0) submitKYC(values)
}
return (
<form onSubmit={handleSubmit} noValidate>
<Field
label="Full Name"
id="fullName"
value={values.fullName}
error={touched.fullName ? errors.fullName : undefined}
onChange={(v) => setField('fullName', v)}
onBlur={() => handleBlur('fullName')}
/>
<Field
label="PAN"
id="pan"
value={values.pan}
error={touched.pan ? errors.pan : undefined}
onChange={(v) => setField('pan', v.toUpperCase())}
onBlur={() => handleBlur('pan')}
maxLength={10}
placeholder="ABCDE1234F"
/>
<Field
label="Aadhaar"
id="aadhaar"
value={values.aadhaar}
error={touched.aadhaar ? errors.aadhaar : undefined}
onChange={(v) => setField('aadhaar', v.replace(/\D/g, '').slice(0, 12))}
onBlur={() => handleBlur('aadhaar')}
inputMode="numeric"
/>
<button type="submit">Verify & Continue</button>
</form>
)
}331. Never mutate state directly
Theory
React detects state changes by reference comparison (Object.is). Mutating an object or array in place doesn't change its reference, so React won't re-render — and even if you force a re-render, other components holding the old reference see stale data.
Always create new objects/arrays when updating state. React 18's useState setter does not auto-clone your data.
Pros & Cons
| Immutable updates | Direct mutation |
|---|---|
| ✅ Predictable re-renders | ❌ Silent bugs — UI doesn't update |
| ✅ Time-travel debugging works | ❌ Stale references in child components |
| ✅ Easier change detection | ❌ Breaks React.memo comparisons |
| ✅ Safer concurrent rendering | — |
Real-Life Example
function Cart() {
const [items, setItems] = useState<CartItem[]>([])
// ❌ WRONG — mutates state directly
const addItemBad = (item: CartItem) => {
items.push(item) // mutates array in place
setItems(items) // same reference → React may skip re-render
}
// ✅ CORRECT — new array reference
const addItem = (item: CartItem) => {
setItems((prev) => [...prev, item])
}
// ✅ Update nested object
const updateQuantity = (id: string, qty: number) => {
setItems((prev) => prev.map((item) => (item.id === id ? { ...item, quantity: qty } : item)))
}
// ✅ Remove item
const removeItem = (id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id))
}
return <CartList items={items} onUpdate={updateQuantity} onRemove={removeItem} />
}// Redux Toolkit uses Immer internally — looks like mutation but is safe
const cartSlice = createSlice({
name: 'cart',
reducers: {
addItem: (state, action) => {
state.items.push(action.payload) // Immer creates draft — safe
},
},
})332. New features in latest React versions
Theory
Stay current with React 18 and React 19 features — Engineo asks this to verify you follow the ecosystem.
React 18 (2022)
| Feature | What it does |
|---|---|
| Concurrent rendering | Interruptible rendering, keeps UI responsive |
| Automatic batching | Batch state updates in timeouts, promises, native events |
| Transitions | useTransition, useDeferredValue — mark updates low priority |
| Suspense improvements | SSR streaming with renderToPipeableStream |
| Strict Mode changes | Double-invoke effects in dev |
createRoot | Replaces ReactDOM.render |
React 19 (2024–2025)
| Feature | What it does |
|---|---|
| Actions | useActionState, form actions with async |
| useOptimistic | Optimistic UI updates built-in |
use() hook | Read promises and context in render |
| Ref as prop | No more forwardRef needed |
| Document metadata | <title>, <meta> in components |
| Server Components | Stable in frameworks (Next.js) |
| Improved hydration | Better mismatch error messages |
Real Example
// React 18 — useTransition for non-urgent updates
function SearchPage() {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const [results, setResults] = useState([])
const handleChange = (value: string) => {
setQuery(value) // urgent — input stays responsive
startTransition(() => {
setResults(filterProducts(value)) // non-urgent — can be interrupted
})
}
return (
<>
<input value={query} onChange={(e) => handleChange(e.target.value)} />
{isPending && <Spinner />}
<ProductList products={results} />
</>
)
}
// React 19 — useOptimistic
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimistic] = useOptimistic(todos, (state, newTodo) => [
...state,
{ ...newTodo, pending: true },
])
async function handleAdd(text) {
addOptimistic({ id: crypto.randomUUID(), text })
await addTodo(text)
}
}333. Normal Functions vs Arrow Functions
Theory
| Feature | Normal | Arrow |
|---|---|---|
this | Dynamic (call site) | Lexical (enclosing scope) |
arguments | Yes | No — use rest ...args |
new / constructor | Yes | No |
prototype | Yes | No |
| Syntax | Verbose | Concise, implicit return |
Real Example
const paymentService = {
transactions: [100, 200, 300],
// ✅ Normal — this = paymentService
getTotal() {
return this.transactions.reduce((a, b) => a + b, 0)
},
// ❌ Arrow — this is NOT paymentService
brokenTotal: () => this?.transactions?.reduce((a, b) => a + b, 0),
// ✅ Arrow inside normal — inherits correct this
getFormatted() {
return this.transactions.map((t) => `₹${t}`)
},
}334. Normal vs Arrow Functions
Theory
| Normal | Arrow | |
|---|---|---|
this | Dynamic | Lexical |
arguments | Yes | No |
new / constructor | Yes | No |
| Syntax | Verbose | Concise |
Real Example
const service = {
items: [1, 2, 3],
sumRegular() {
return this.items.reduce((a, b) => a + b, 0)
}, // ✅
sumArrow: () => this?.items?.reduce((a, b) => a + b, 0), // ❌
}335. Object Methods
Theory
JavaScript provides built-in static methods on Object and instance methods on Object.prototype for creating, copying, comparing, and iterating objects.
Key methods
| Method | Purpose |
|---|---|
Object.keys(obj) | Array of own enumerable property names |
Object.values(obj) | Array of own enumerable property values |
Object.entries(obj) | Array of [key, value] pairs |
Object.assign(target, ...sources) | Shallow copy/merge |
Object.freeze(obj) | Prevent all mutations |
Object.seal(obj) | Prevent add/delete, allow modify |
Object.hasOwn(obj, key) | Safe own-property check |
Object.fromEntries(arr) | Inverse of entries |
Object.create(proto) | Create with specific prototype |
Pros & Cons
| Object static methods | Manual iteration |
|---|---|
| ✅ Standard, well-supported | ❌ Verbose |
| ✅ Composable (entries → map → fromEntries) | ❌ Easy to miss non-enumerable props |
Object.freeze for immutability | freeze is shallow only |
Real-Life Example
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
}
// Iterate
Object.keys(config).forEach((key) => console.log(key, config[key]))
Object.entries(config).map(([k, v]) => `${k}=${v}`)
// Merge configs
const defaults = { timeout: 3000, retries: 1 }
const finalConfig = { ...defaults, ...config }
// Transform object
const inverted = Object.fromEntries(Object.entries(config).map(([k, v]) => [k.toUpperCase(), v]))
// Safe property check
Object.hasOwn(config, 'apiUrl') // true
Object.hasOwn(config, 'toString') // false (inherited)
// Freeze for constants
const TAX_RATES = Object.freeze({
GST: 0.05,
SERVICE: 0.1,
})
// TAX_RATES.GST = 0.08; // silent fail (strict: TypeError)
// Clone
const shallow = { ...config }
const deep = structuredClone(config)// Real-life: Form state to API payload
const formState = { name: 'Amit', email: 'a@b.com', phone: '' }
const payload = Object.fromEntries(Object.entries(formState).filter(([_, v]) => v !== ''))
// { name: "Amit", email: "a@b.com" }336. Object vs Map differences in JavaScript
Theory
Both Object and Map store key-value pairs, but they differ in key types, iteration, performance, and API design. Object is the general-purpose data structure with prototype inheritance. Map is a dedicated hash map with any type of key and guaranteed insertion order.
Pros & Cons
| Object | Map | |
|---|---|---|
| Pros | JSON-serializable, familiar syntax, literal shorthand | Any key type, size property, better frequent add/delete perf |
| Cons | Keys only string/symbol, prototype pollution risk, no size | Not JSON-serializable, slightly more verbose |
Real-Life Example
// Object — config/settings (string keys, JSON-friendly)
const appConfig = {
apiUrl: 'https://api.a food-delivery platform.com',
timeout: 5000,
retries: 3,
}
// Map — caching DOM elements or object-keyed lookups
const elementCache = new Map()
function getCachedElement(node) {
if (!elementCache.has(node)) {
elementCache.set(node, {
height: node.offsetHeight,
width: node.offsetWidth,
})
}
return elementCache.get(node)
}
// Map — counting occurrences with non-string keys
const requestCounts = new Map()
function trackRequest(requestObj) {
requestCounts.set(requestObj, (requestCounts.get(requestObj) || 0) + 1)
}337. Objects in JavaScript
Theory
In JavaScript, almost everything is an object (or can behave like one). Objects are collections of key-value pairs — properties and methods. Keys are strings or symbols; values can be any type.
Objects are reference types — assigned and compared by reference, not value.
Real Example
const user = {
id: 'u_1',
name: 'Amit',
role: 'admin',
greet() {
return `Hello, ${this.name}`
},
}
user.greet() // "Hello, Amit"
typeof user // "object"338. Optimistic UI Updates
Theory
Optimistic UI updates the interface immediately before the server confirms the action. If the server request fails, the UI rolls back to the previous state.
Use for: likes, cart add/remove, todo toggle, message send — actions where success is likely and instant feedback matters.
Pros & Cons
| Pros | Cons |
|---|---|
| Instant perceived response | Rollback UX can confuse users |
| Better UX on slow networks | Complex state management |
| Feels native/app-like | Wrong for critical actions (payments) |
Real-Life Example
function LikeButton({ postId, initialLiked, initialCount }) {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (liked: boolean) =>
fetch(`/api/posts/${postId}/like`, {
method: liked ? 'POST' : 'DELETE',
}),
// Optimistic update — before server responds
onMutate: async (liked) => {
await queryClient.cancelQueries({ queryKey: ['post', postId] })
const previous = queryClient.getQueryData(['post', postId])
queryClient.setQueryData(['post', postId], (old: Post) => ({
...old,
liked,
likeCount: old.likeCount + (liked ? 1 : -1),
}))
return { previous } // snapshot for rollback
},
// Rollback on error
onError: (_err, _liked, context) => {
queryClient.setQueryData(['post', postId], context?.previous)
toast.error('Failed to update like')
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['post', postId] })
},
})
return (
<button onClick={() => mutation.mutate(!initialLiked)}>
{initialLiked ? '❤️' : '🤍'} {initialCount}
</button>
)
}339. Output prediction for tricky JavaScript snippets
Theory
Output prediction questions test your understanding of hoisting, closures, this binding, type coercion, event loop ordering, and reference vs value. Interviewers want you to explain why, not just guess the output.
Pros & Cons of these questions in interviews
| Pros | Cons |
|---|---|
| Reveals deep language understanding | Can feel academic vs real-world |
| Fast to ask in live coding | Some edge cases rarely appear in production |
Real-Life Example — 12 Tricky Snippets
Snippet 1 — Hoisting + TDZ
console.log(a)
var a = 1
console.log(b)
let b = 2Output: undefined → ReferenceError (b is in TDZ)
Why: var is hoisted and initialized as undefined. let is hoisted but not initialized.
Snippet 2 — Closure in loop
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}Output: 3, 3, 3
Why: var is function-scoped; all closures share the same i which is 3 after the loop.
Snippet 3 — Closure with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}Output: 0, 1, 2
Why: let creates a new binding per iteration.
Snippet 4 — Event loop order
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')Output: 1, 4, 3, 2
Why: Sync first → microtasks (promises) → macrotasks (setTimeout).
Snippet 5 — typeof null
console.log(typeof null)
console.log(null instanceof Object)Output: "object" → false
Why: Historical bug — typeof null returns "object". instanceof checks prototype chain.
Snippet 6 — Implicit coercion
console.log([] + [])
console.log([] + {})
console.log({} + [])Output: "" → "[object Object]" → "[object Object]"
Why: + with arrays/objects triggers toString().
Snippet 7 — this in arrow vs regular function
const obj = {
name: 'Amit',
regular() {
console.log(this.name)
},
arrow: () => console.log(this.name),
}
obj.regular()
obj.arrow()Output: "Amit" → undefined (or global name in non-strict)
Why: Arrow functions don't have their own this — they inherit from enclosing scope.
Snippet 8 — Promise chaining
Promise.resolve(1)
.then((v) => {
console.log(v)
return v + 1
})
.then((v) => {
console.log(v)
throw new Error('fail')
})
.then((v) => console.log(v))
.catch((e) => console.log('caught'))Output: 1 → 2 → caught
Why: Thrown error skips next .then, goes to .catch.
Snippet 9 — async/await order
async function foo() {
console.log('A')
await Promise.resolve()
console.log('B')
}
console.log('C')
foo()
console.log('D')Output: C, A, D, B
Why: await pauses foo and schedules resume as microtask after sync code.
Snippet 10 — Object reference
const a = { n: 1 }
const b = a
b.n = 2
console.log(a.n)Output: 2
Why: Objects are passed by reference — a and b point to the same object.
Snippet 11 — == vs ===
console.log(0 == false)
console.log(0 === false)
console.log('' == 0)
console.log(null == undefined)
console.log(null === undefined)Output: true, false, true, true, false
Why: == does type coercion; === does not.
Snippet 12 — IIFE + closure
;(function () {
try {
throw new Error('x')
} catch (e) {
var e = 2
console.log(e)
}
})()Output: 2
Why: var e in catch is function-scoped and hoisted, shadowing the catch parameter.
340. Output-based JavaScript Questions
Theory
Senior interviews test deep understanding through output prediction. Explain why, not just the answer. Topics: hoisting, closures, event loop, coercion, this, prototypes.
Q1 — Hoisting + var
console.log(a)
var a = 1
console.log(a)Output: undefined → 1
Q2 — TDZ
console.log(b)
let b = 2Output: ReferenceError
Q3 — Closure + var loop
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}Output: 3, 3, 3
Q4 — Closure + let loop
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}Output: 0, 1, 2
Q5 — Event loop
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')Output: 1 → 4 → 3 → 2
Q6 — async/await order
async function foo() {
console.log('A')
await Promise.resolve()
console.log('B')
}
console.log('C')
foo()
console.log('D')Output: C → A → D → B
Q7 — this + arrow
const obj = {
name: 'Senior',
regular() {
return this.name
},
arrow: () => this?.name,
}
console.log(obj.regular())
console.log(obj.arrow())Output: "Senior" → undefined
Q8 — Prototype
function Person(name) {
this.name = name
}
Person.prototype.greet = function () {
return this.name
}
const p = new Person('Amit')
console.log(p.greet())
console.log(p.hasOwnProperty('greet'))Output: "Amit" → false
Q9 — Coercion
console.log([] + [])
console.log([] + {})
console.log({} + [])Output: "" → "[object Object]" → "[object Object]"
Q10 — Promise chain
Promise.resolve(1)
.then((v) => v + 1)
.then((v) => {
throw new Error('fail')
})
.then((v) => console.log(v))
.catch((e) => console.log('caught'))
.then(() => console.log('done'))Output: caught → done
Q11 — typeof null
console.log(typeof null)
console.log(null instanceof Object)Output: "object" → false
Q12 — Currying output
function multiply(a) {
return function (b) {
return a * b
}
}
const double = multiply(2)
console.log(double(5))
console.log(multiply(3)(4))Output: 10 → 12
| Do | Don't |
|---|---|
| Explain why behind output | Guess without reasoning |
| Connect JS to React (closures → hooks, event loop → useEffect) | Treat JS and React as separate |
| Write code from scratch (debounce, curry, flatten) | Only know definitions |
| Mention edge cases (TDZ, shallow copy, arrow this) | Give textbook paragraphs |
Quick Revision — All 20
| # | Topic | One-liner |
|---|---|---|
| 1 | Closure | Function + remembered outer scope |
| 2 | Event loop | Sync → microtasks → macrotask |
| 3 | Micro vs macro | Promises first, setTimeout later |
| 4 | Hoisting | Declarations registered before run |
| 5 | TDZ | let/const inaccessible until declared |
| 6 | var/let/const | Block scope; const default |
| 7 | == vs === | Coercion vs strict — always === |
| 8 | call/apply/bind | Invoke now vs bind this for later |
| 9 | Arrow + bind | No — lexical this, bind ignored |
| 10 | Normal vs arrow | Dynamic vs lexical this |
| 11 | Debounce/throttle | Pause vs rate-limit |
| 12 | Currying | f(a)(b)(c) — partial application |
| 13 | Prototypes | Property lookup chain |
| 14 | Bubbling/capturing | Target → up vs window → down |
| 15 | GC | Mark-and-sweep unreachable objects |
| 16 | Deep vs shallow | Nested shared vs full clone |
| 17 | async/await | Promise sugar, microtask resume |
| 18 | map/filter/reduce | Transform, select, accumulate |
| 19 | Flatten | Recursive or flat(Infinity) |
| 20 | Output questions | Hoisting, loop, event loop, this |
Senior React interviews are won on JavaScript depth. Know the concept, explain it in 30 seconds, prove it with one example.
341. Output-based tricky JS questions (Bonus)
Theory
the company interviews also test output prediction — hoisting, TDZ, closures, and event loop. Explain why, not just the answer.
Real-Life Example — 8 snippets
Q1 — Hoisting + var
console.log(a)
var a = 10
console.log(a)Output: undefined → 10
Q2 — TDZ
console.log(b)
let b = 20Output: ReferenceError (TDZ — not undefined)
Q3 — Closure + var in loop
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}Output: 3, 3, 3
Q4 — Closure + let in loop
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}Output: 0, 1, 2
Q5 — Event loop
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')Output: A → D → C → B
Q6 — typeof undeclared vs TDZ
console.log(typeof notDeclared) // "undefined"
let x = 1
// console.log(typeof y); // ReferenceError — y in TDZ
let y = 2Q7 — undefined vs not defined
let user
console.log(user) // undefined
console.log(user.name) // TypeError (can't read property of undefined)
// console.log(profile); // ReferenceError (not defined)Q8 — this in arrow vs regular
const obj = {
name: 'the company',
regular() {
return this.name
},
arrow: () => this?.name,
}
console.log(obj.regular()) // "the company"
console.log(obj.arrow()) // undefined| Area | Topics in this guide |
|---|---|
| JavaScript core | Execution, hoisting, TDZ, undefined vs not defined |
| CSS layout | position, Flexbox, Grid |
| React patterns | HOC, traffic light state + timing |
| Array methods | map, filter, reduce |
| Accessibility | ARIA, semantic HTML, keyboard |
| Output questions | Hoisting, closure, event loop |
Quick Revision Cheat Sheet
| # | Topic | One-liner |
|---|---|---|
| 1 | Web Accessibility | WCAG, semantic HTML, keyboard, ARIA, contrast |
| 2 | CSS position | static=flow, relative=offset, absolute=ancestor, fixed=viewport |
| 3 | Flexbox vs Grid | 1D content-driven vs 2D layout-driven |
| 4 | JS execution | Parse → compile → execute context → event loop |
| 5 | undefined vs not defined | Exists/no value vs never declared (ReferenceError) |
| 6 | Hoisting | Declarations registered before execution; var=undefined |
| 7 | TDZ | let/const hoisted but inaccessible until declaration line |
| 8 | HOC | Function(Component) → EnhancedComponent; prefer hooks now |
| 9 | map/filter/reduce | Transform, select, accumulate — immutable |
| 10 | Traffic light | useState + useEffect + setTimeout + cleanup; 5s/3s/2s |
342. Principles of Functional Programming
Theory
Functional Programming (FP) treats computation as the evaluation of pure functions, avoiding mutable state and side effects. Core principles:
- Pure functions — same input always produces same output, no side effects
- Immutability — never mutate data; create new copies
- First-class functions — functions are values, can be passed/returned
- Higher-order functions — functions that take/return functions
- Function composition — combine small functions into pipelines
- Declarative style — describe what, not how
Pros & Cons
| Pros | Cons |
|---|---|
| Predictable, easier to test | Learning curve for OOP developers |
| Fewer bugs from shared mutable state | Can be verbose without language support |
| Enables parallelism | Performance cost of copying large data |
Real-Life Example
// Imperative (mutating)
function getActiveOrderTotals(orders) {
const totals = []
for (let i = 0; i < orders.length; i++) {
if (orders[i].status === 'active') {
orders[i].total = orders[i].items.reduce((s, item) => s + item.price, 0)
totals.push(orders[i].total)
}
}
return totals
}
// Functional (immutable, pure)
const getActiveOrderTotals = (orders) =>
orders
.filter((order) => order.status === 'active')
.map((order) => order.items.reduce((sum, item) => sum + item.price, 0))
// React embraces FP
const [orders, setOrders] = useState([])
setOrders((prev) => [...prev, newOrder]) // immutable update343. Projects deep dive
Theory
a fintech app Round 1 opens with a deep project walkthrough — not a resume summary. They probe: what you built, why you made specific technical choices, what broke, and what you'd do differently today.
Use STAR for features but add a technical decision layer: alternatives considered, trade-offs, metrics.
What interviewers probe
| Question | What they want |
|---|---|
| Why this architecture? | Trade-off thinking, not "it was trendy" |
| What broke in production? | Ownership, debugging skill |
| What would you redo? | Growth mindset, hindsight learning |
| How did you measure success? | Impact, not just code |
Real-Life Example — Answer skeleton
Pros & Cons framing (for "why X over Y")
| Choice | Pros | Cons | When a fintech app cares |
|---|---|---|---|
| React Query vs Redux for API | Less boilerplate, caching | Another dependency | Server vs client state split |
| WebSocket vs polling | Real-time | Connection management | Payment status UX |
| Monolith vs micro-frontend | Simpler | Team scaling | Only if you led the decision |
344. Quick Revision Cheat Sheet
| # | Scenario | Core answer |
|---|---|---|
| 1 | Slow multi-API page | Promise.all, Suspense streaming, cache with revalidate |
| 2 | Live stock data | Hybrid: dynamic SSR snapshot + WebSocket/polling on client — not SSG |
| 3 | Large bundle | Server Components, next/dynamic, bundle analyzer, optimizePackageImports |
| 4 | Server caching | fetch cache + tags, ISR, Redis for hot paths, revalidateTag |
| 5 | 10K blog posts | generateStaticParams + ISR + on-demand revalidation from CMS |
| 6 | Heavy chart on interaction | dynamic(..., { ssr: false }), import on click |
| 7 | Large file upload | Presigned URL → direct S3 upload → completion webhook |
| 8 | High-volume API routes | Rate limit, CDN cache, edge runtime, async queues |
| 9 | SPA → Next.js | Incremental routes, middleware auth, fix hydration, client islands |
| 10 | A/B testing | Middleware cookie assignment, server render variant, track in analytics |
- 10-frontend-concepts-checklist.md — CSR vs SSR vs SSG vs ISR
- 06-react-best-practices-project-guide.md — code splitting, performance
- 20-senior-frontend-real-world-interview.md — production engineering topics
345. React 18 Automatic Batching
Theory
React 18 batches all state updates into one re-render — inside events, setTimeout, promises, and native handlers. Previously only batched in React event handlers.
flushSync() forces synchronous render when needed (measuring DOM).
Real Example
function handleClick() {
setCount((c) => c + 1)
setFlag(true)
// React 18: ONE re-render
}
fetch('/api').then(() => {
setData(result)
setLoading(false)
// Also batched in React 18
})346. Render Phase vs Commit Phase
Theory
| Render Phase | Commit Phase | |
|---|---|---|
| Interruptible? | Yes (concurrent) | No — synchronous |
| What happens | Call components, run hooks, diff tree | Apply DOM mutations |
| Effects | Mark placement/update/deletion flags | Run useLayoutEffect |
| After | — | Browser paints → useEffect |
Real Example
User clicks → setState
↓
RENDER PHASE (interruptible)
- Call component function
- useState returns new value
- useMemo recalculates
- Diff old vs new tree
↓
COMMIT PHASE (sync)
- Apply DOM insert/update/delete
- useLayoutEffect fires
↓
Browser paints screen
↓
useEffect fires (passive)347. Round-by-round prep strategy
Theory
a fintech app spreads rounds across different days. Use the gap to fix what went wrong.
| Round | Focus | If you fail, fix before next |
|---|---|---|
| 1 | Browser internals, closures, projects | Write debounce from memory, CSS render blocking |
| 2 | Vanilla JS live search | Build autocomplete without React, event loop drills |
| 3 | React form state design | KYC form with PAN/Aadhaar validation, RHF |
| 4 | Cultural fit | Reflect on feedback, prepare honest weakness story |
| 5 | (Final) | Usually offer/higher-level — review all weak areas |
Checklist before a fintech app
- Write cancellable debounce from scratch in 5 minutes
- Build vanilla JS search with keyboard nav + click outside
- Explain defer vs async with timeline diagram
- Explain CSS render blocking + critical CSS
- KYC form with validation — state design on whiteboard first
- Context re-render problem + 3 fixes
- SSR vs ISR trade-offs for a fintech app product examples
- Closure leak example + fix ready
- "Why a fintech app" answer — specific, not generic
- 2 project deep-dive stories with "what I'd do differently"
| Topic | One-liner |
|---|---|
| Bubbling vs delegation | Mechanism vs pattern; delegation uses bubbling |
| defer vs async | Neither blocks download; async exec can interrupt parse; defer waits for parse end |
| CSS blocks render | Needs CSSOM for render tree — prevents FOUC |
| Cancellable debounce | clearTimeout + .cancel() + .flush() |
| Closure leak | Inner fn holds large outer ref — cleanup/remove listener |
| Vanilla search | Debounce + AbortController + keydown + mousedown outside |
| Reconciliation | Same type update; diff type replace; keys for lists |
| 50 + context | All consumers re-render — split context or selectors |
| Event loop | Sync → all microtasks → one macrotask |
| SSR/CSR/ISR | Fresh vs fast vs SEO — a fintech app uses all three |
| Form state | RHF for 10+ fields; reset dependents on parent change |
| Cultural fit | Honest weakness + action plan, no defensiveness |
348. Scope & Scope Chain
Theory
Scope determines where variables are accessible. JavaScript has:
- Global scope — accessible everywhere (
windowin browsers) - Function scope —
varis limited to the function - Block scope —
let/constlimited to{}blocks - Module scope — ES modules have their own scope
The scope chain is the path JavaScript follows to look up a variable: current scope → outer scope → ... → global. Inner scopes can access outer variables; outer scopes cannot access inner ones (lexical scoping).
Pros & Cons
| Block scope (let/const) | Function scope (var) |
|---|---|
| ✅ Variables contained to blocks | ❌ Variables leak from if/for blocks |
| ✅ Fewer naming collisions | ❌ Harder to reason about |
| ✅ TDZ catches early access | — |
Real-Life Example
const API_URL = 'https://api.example.com' // global/module scope
function fetchOrders(userId) {
const endpoint = `/users/${userId}/orders` // function scope
if (userId) {
let page = 1 // block scope
const limit = 20
while (page <= 5) {
let cursor = page * limit // block scope per iteration (let)
fetch(`${API_URL}${endpoint}?page=${page}&limit=${limit}`)
page++
}
// console.log(cursor); // ReferenceError — block scoped
}
// console.log(page); // ReferenceError
return endpoint
}
// Scope chain lookup
const tax = 0.05 // outer
function calculate(price) {
const discount = 0.1 // middle
function applyDiscount() {
return price - price * discount + price * tax
// applyDiscount looks up: own scope → calculate → global
}
return applyDiscount()
}// Module scope — variables don't pollute global
// orderService.js
const BASE_URL = import.meta.env.VITE_API_URL // module-private
export function getOrders() {
return fetch(`${BASE_URL}/orders`)
}349. Server-Side Rendering techniques to improve SEO
Theory
Search engine crawlers need meaningful HTML content to index pages. SSR techniques ensure crawlers see full content without executing JavaScript.
Key techniques: SSR, SSG (Static Site Generation), ISR (Incremental Static Regeneration), dynamic rendering, structured data, meta tags.
Pros & Cons
| Technique | Pros | Cons |
|---|---|---|
| SSR | Always fresh, SEO-friendly | Server load on every request |
| SSG | Fastest, cheapest hosting | Stale until rebuild |
| ISR | Fresh + fast | Complexity, cache invalidation |
| Dynamic rendering | Serves bots HTML, users JS | Cloaking risk if misused |
Real-Life Example
// Next.js — SSR with meta tags for SEO
export async function generateMetadata({ params }) {
const restaurant = await fetchRestaurant(params.id)
return {
title: `${restaurant.name} — Order Online | a food-delivery platform`,
description: restaurant.cuisine + ' restaurant in ' + restaurant.area,
openGraph: {
images: [restaurant.image],
},
}
}
// Structured data for Google rich results
function RestaurantPage({ restaurant }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Restaurant',
name: restaurant.name,
address: restaurant.address,
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: restaurant.rating,
reviewCount: restaurant.reviewCount,
},
}
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<h1>{restaurant.name}</h1>
{/* Full content in HTML — visible to crawlers */}
</>
)
}350. Session Storage vs Local Storage
Theory
Both are Web Storage APIs storing key-value pairs in the browser. Neither is sent with HTTP requests (unlike cookies).
| localStorage | sessionStorage | |
|---|---|---|
| Lifetime | Until explicitly cleared | Until tab/window closes |
| Scope | Same origin, all tabs | Same origin, single tab |
| Capacity | ~5–10 MB | ~5–10 MB |
| Shared across tabs | Yes | No |
| Survives refresh | Yes | Yes (same tab) |
| Use for | Preferences, drafts | Form data, single-session state |
Pros & Cons
| localStorage | sessionStorage |
|---|---|
| ✅ Persists across sessions | ✅ Auto-clears on tab close — safer for sensitive temp data |
| ❌ Stays until cleared — stale data risk | ❌ Lost when tab closes |
| ❌ XSS can read it | ❌ XSS can read it |
Never store auth tokens in either — use httpOnly cookies.
Real Example
// localStorage — user preferences (persist forever)
localStorage.setItem('theme', 'dark')
localStorage.setItem('language', 'en')
localStorage.setItem('recentSearches', JSON.stringify(['react', 'redux']))
// sessionStorage — multi-step form (cleared when tab closes)
sessionStorage.setItem('checkoutStep', '2')
sessionStorage.setItem('draftOrder', JSON.stringify(cartItems))
// Tab behavior demo
// Tab A: localStorage.setItem("x", "1") → Tab B can read "1"
// Tab A: sessionStorage.setItem("y", "2") → Tab B CANNOT read "y"// Custom hook
function useSessionStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const stored = sessionStorage.getItem(key)
return stored ? JSON.parse(stored) : initial
})
useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}351. Stopwatch Implementation
Theory
Build a stopwatch with Start, Stop, Reset and a live timer display. Use setInterval for ticking, store elapsed time, handle pause/resume by tracking offset.
Key: cleanup interval on stop/unmount; don't drift by recalculating from Date.now().
Vanilla JS Implementation
<div class="stopwatch">
<span id="display">00:00:00</span>
<button id="start">Start</button>
<button id="stop" disabled>Stop</button>
<button id="reset" disabled>Reset</button>
</div>class Stopwatch {
constructor(displayEl) {
this.display = displayEl
this.elapsed = 0
this.startTime = 0
this.timerId = null
this.running = false
}
format(ms) {
const totalSec = Math.floor(ms / 1000)
const h = String(Math.floor(totalSec / 3600)).padStart(2, '0')
const m = String(Math.floor((totalSec % 3600) / 60)).padStart(2, '0')
const s = String(totalSec % 60).padStart(2, '0')
return `${h}:${m}:${s}`
}
tick() {
const now = Date.now()
this.display.textContent = this.format(this.elapsed + (now - this.startTime))
}
start() {
if (this.running) return
this.running = true
this.startTime = Date.now()
this.timerId = setInterval(() => this.tick(), 10) // update every 10ms
}
stop() {
if (!this.running) return
this.running = false
this.elapsed += Date.now() - this.startTime
clearInterval(this.timerId)
this.timerId = null
this.tick() // final display
}
reset() {
this.stop()
this.elapsed = 0
this.display.textContent = '00:00:00'
}
}
const sw = new Stopwatch(document.getElementById('display'))
document.getElementById('start').onclick = () => sw.start()
document.getElementById('stop').onclick = () => sw.stop()
document.getElementById('reset').onclick = () => sw.reset()React Implementation
function Stopwatch() {
const [elapsed, setElapsed] = useState(0)
const [running, setRunning] = useState(false)
const startTimeRef = useRef(0)
const accumulatedRef = useRef(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const format = (ms: number) => {
const sec = Math.floor(ms / 1000)
const h = String(Math.floor(sec / 3600)).padStart(2, '0')
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0')
const s = String(sec % 60).padStart(2, '0')
return `${h}:${m}:${s}`
}
const start = () => {
if (running) return
setRunning(true)
startTimeRef.current = Date.now()
intervalRef.current = setInterval(() => {
setElapsed(accumulatedRef.current + (Date.now() - startTimeRef.current))
}, 10)
}
const stop = () => {
if (!running) return
accumulatedRef.current += Date.now() - startTimeRef.current
clearInterval(intervalRef.current!)
intervalRef.current = null
setRunning(false)
}
const reset = () => {
stop()
accumulatedRef.current = 0
setElapsed(0)
}
useEffect(() => () => clearInterval(intervalRef.current!), [])
return (
<div>
<span aria-live="polite">{format(elapsed)}</span>
<button onClick={start} disabled={running}>
Start
</button>
<button onClick={stop} disabled={!running}>
Stop
</button>
<button onClick={reset}>Reset</button>
</div>
)
}352. Strict Mode
Theory
<StrictMode> is a development-only wrapper that double-invokes certain functions (render, effects, state initializers) to expose side effects and impure code. It does not run in production.
Helps catch: missing cleanup, unsafe lifecycles, deprecated APIs.
Pros & Cons
| Pros | Cons |
|---|---|
| Catches bugs early | Double effects confuse beginners |
| Prepares for Concurrent React | Not a bug — intentional dev behavior |
Real Example
// main.jsx
ReactDOM.createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
// useEffect runs twice in dev — if you see duplicate API calls,
// your cleanup is missing or effect has no abort logic353. Sum of Numbers without a for Loop
Theory
Sum an array without for/while using:
reduce()— most idiomatic- Recursion — classic interview approach
eval+ join — trick (avoid in production)
Implementations
const nums = [1, 2, 3, 4, 5]
// Approach 1 — reduce (preferred)
const sumReduce = (arr) => arr.reduce((acc, n) => acc + n, 0)
console.log(sumReduce(nums)) // 15
// Approach 2 — recursion
function sumRecursive(arr) {
if (arr.length === 0) return 0
return arr[0] + sumRecursive(arr.slice(1))
}
console.log(sumRecursive(nums)) // 15
// Approach 3 — reduce without initial (only for non-empty)
const sumReduceNoInit = (arr) => arr.reduce((a, b) => a + b)
console.log(sumReduceNoInit(nums)) // 15
// Sum of arguments without loop
function sumArgs(...args) {
return args.reduce((a, b) => a + b, 0)
}
console.log(sumArgs(10, 20, 30)) // 60354. Team Collaboration & Conflict Resolution
Common questions
- Disagreement with a senior engineer on architecture?
- Missed deadline — what did you do?
- Code review feedback that felt unfair?
- Working with backend/design on conflicting priorities?
STAR example — Technical disagreement
Situation: Team split on Redux vs React Query for server state.
Task: Unblock sprint planning without endless debate.
Action: Proposed a 1-week spike — same feature both ways; measured bundle size,
lines of code, and cache behavior. Documented in a short ADR.
Team voted with data; chose React Query + Zustand for UI state.
Result: Decision in 3 days; team bought in; pattern documented for other squads.Conflict principles the organization cares about
- Professional services mindset — client delivery, clarity, documentation
- Escalate early with facts, not emotion
- Assume positive intent in code review
- Write it down — ADR, RFC, meeting notes
355. Technical weaknesses
Theory
a fintech app's Tech Lead opens with feedback from all 3 previous rounds and asks where you're weak. They watch for honesty, self-awareness, and action — not perfection.
Pros & Cons of answer approaches
| Do | Don't |
|---|---|
| Name a real gap with a learning plan | "I don't have weaknesses" |
| Reference something Round 1-3 exposed | Blame interviewers or questions |
| Show progress already made | Be defensive about feedback |
| Connect weakness to growth | List fake weaknesses ("I work too hard") |
Real-Life Example — Answer skeleton
356. Tree Shaking
Theory
Tree shaking is dead code elimination — the bundler removes exported modules that are never
imported. Requires ES modules (import/export) and side-effect-free
packages.
Works at build time. Mark packages as side-effect-free in package.json:
{ "sideEffects": false }Pros & Cons
| Pros | Cons |
|---|---|
| Automatic size reduction | Only works with ESM |
| No runtime cost | CommonJS modules can't be tree-shaken |
| Encourages modular libraries | Side effects prevent shaking |
Real-Life Example
// utils/math.js
export function add(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
export function divide(a, b) {
return a / b
}
// app.js — only imports add
import { add } from './utils/math'
console.log(add(2, 3))
// After tree shaking: multiply and divide removed from bundle// ❌ CommonJS — no tree shaking
const _ = require("lodash");
// ✅ ESM — tree shakeable
import debounce from "lodash-es/debounce";
// package.json of your library
{
"sideEffects": ["*.css"], // only CSS has side effects
"module": "dist/index.esm.js"
}357. Two Sum Problem
Theory
Given an array and a target, return indices of two numbers that add up to target.
Approach 1 — Brute force O(n²)
function twoSumBrute(nums, target) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] === target) return [i, j]
}
}
return []
}Approach 2 — Hash map O(n) ✅
function twoSum(nums, target) {
const seen = new Map() // value → index
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i]
if (seen.has(complement)) {
return [seen.get(complement), i]
}
seen.set(nums[i], i)
}
return []
}
console.log(twoSum([2, 7, 11, 15], 9)) // [0, 1]
console.log(twoSum([3, 2, 4], 6)) // [1, 2]C++ version
#include <vector>
#include <unordered_map>
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> seen;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (seen.count(complement)) {
return {seen[complement], i};
}
seen[nums[i]] = i;
}
return {};
}358. Type Coercion
Theory
Type coercion is JavaScript automatically converting one data type to another during operations.
It happens with ==, +, comparison operators, and logical operators.
- Implicit coercion — JS converts automatically (
"5" + 1→"51") - Explicit coercion — you convert intentionally (
Number("5"),String(42))
=== does not coerce — always prefer it over ==.
Pros & Cons
| Implicit coercion | Strict equality (===) |
|---|---|
| ✅ Flexible, forgiving | ✅ Predictable, no surprises |
❌ "0" == false is true | ❌ Must convert types yourself |
| ❌ Source of countless bugs | ✅ Industry standard |
Real-Life Example
// == coercion rules (avoid ==)
0 == false // true
'' == false // true
null == undefined // true
'5' == 5 // true
;[] == false // true
![] == false // false (quirky)
// === — no coercion
0 === false // false
'5' === 5 // false
// + operator coercion
'5' + 1 // "51" (string concat)
'5' - 1 // 4 (string → number)
true + true // 2
;[] + [] // "" (both toString to "")
// Explicit conversion — preferred
Number('42') // 42
Number('') // 0
Number('abc') // NaN
parseInt('42px') // 42
String(42) // "42"
Boolean('') // false
Boolean('hello') // true
!!value // common boolean cast
// Falsy values (all coerce to false)
// false, 0, -0, 0n, "", null, undefined, NaN
// Nullish coalescing — only null/undefined
const port = config.port ?? 3000 // 0 is valid, unlike ||
const name = user.name ?? 'Guest'// Real-life: Safe number from form input
function getQuantity(input) {
const qty = Number(input)
return Number.isFinite(qty) && qty > 0 ? qty : 1
}359. Types of Objects & How to Define Them
Theory
| Type | How to create | Example |
|---|---|---|
| Object literal | {} | { name: "Amit" } |
| Constructor | new Object() | Rarely used |
| Constructor function | new Person() | Pre-ES6 classes |
| Class | class Person {} | ES6 syntactic sugar |
| Object.create | Object.create(proto) | Prototypal inheritance |
| Factory function | Function returning object | Closure-based |
| Map | new Map() | Any key type |
| Set | new Set() | Unique values |
| Array | [] | typeof "object" |
| Function | function(){} | Callable object |
| Date, RegExp, Error | Built-in constructors | Specialised objects |
Real Example
// Literal
const config = { apiUrl: '/api', timeout: 5000 }
// Class
class Transaction {
constructor(amount, type) {
this.amount = amount
this.type = type
}
getFormatted() {
return `₹${this.amount}`
}
}
// Factory with closure
function createUser(name) {
let loginCount = 0
return {
name,
login() {
loginCount++
},
getLoginCount: () => loginCount,
}
}
// Object.create — inherit prototype
const animal = {
breathe() {
return 'breathing'
},
}
const dog = Object.create(animal)
dog.bark = () => 'woof'
// Map — non-string keys
const cache = new Map()
cache.set({ id: 1 }, { data: '...' })360. Unfamiliar codebase
Theory
They want your onboarding process — how you become productive without breaking things.
Real-Life Example — Answer skeleton
361. Use environment variables for configuration values
Theory
Environment variables store configuration that changes between environments (development, staging, production) — API URLs, feature flags, public keys. Never hardcode URLs or commit secrets to git.
In Vite: VITE_ prefix. In Create React App: REACT_APP_ prefix. Only variables with these prefixes are exposed to the browser bundle.
Pros & Cons
| Environment variables | Hardcoded values |
|---|---|
| ✅ Different config per environment | ❌ Must change code to deploy |
| ✅ No secrets in source code | ❌ Risk of committing API keys |
| ✅ CI/CD friendly | ❌ Same build can't serve multiple envs |
| ❌ Public in bundled JS (VITE_ vars) | — |
Real-Life Example
VITE_API_URL=http://localhost:3001/api
VITE_ENABLE_MOCK=true
VITE_APP_NAME=MyApp (Dev)
# .env.production
VITE_API_URL=https://api.production.com
VITE_ENABLE_MOCK=false
VITE_APP_NAME=MyApp// config/env.ts — typed, centralized
export const env = {
apiUrl: import.meta.env.VITE_API_URL as string,
enableMock: import.meta.env.VITE_ENABLE_MOCK === 'true',
appName: import.meta.env.VITE_APP_NAME as string,
} as const
// Usage
const apiClient = axios.create({ baseURL: env.apiUrl })# .gitignore — NEVER commit these
.env
.env.local
.env.productionRules:
- ✅
VITE_API_URL,VITE_GOOGLE_CLIENT_ID(public) - ❌
VITE_STRIPE_SECRET_KEY,VITE_DB_PASSWORD(secrets belong on server only)
362. useContext Hook
Theory
useContext reads the nearest value from a Context Provider. It avoids passing props through every level — any descendant can access shared data directly.
Pros & Cons
| Pros | Cons |
|---|---|
| No prop drilling | All consumers re-render on value change |
| Clean API | Not for frequently changing data |
Real Example
const ThemeContext = createContext('light')
function App() {
const [theme, setTheme] = useState('dark')
return (
<ThemeContext.Provider value={theme}>
<Header />
</ThemeContext.Provider>
)
}
function Header() {
const theme = useContext(ThemeContext)
return <header className={`header-${theme}`}>Logo</header>
}363. useDeferredValue Hook
Theory
useDeferredValue(value) returns a deferred version of a value that may lag behind during urgent updates. React treats updating the deferred value as low priority — keeps the UI responsive during expensive re-renders.
Part of Concurrent React (React 18+). Pairs with slow lists, search filters, heavy charts.
Pros & Cons
| Pros | Cons |
|---|---|
| Keeps input responsive | Shows stale data briefly |
| No manual transition API | Only helps render performance |
| Built-in, declarative | Needs expensive child to matter |
Real Example
import { useState, useDeferredValue, memo } from 'react'
const SlowList = memo(function SlowList({ query }) {
const items = filterHugeList(allItems, query) // expensive
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
})
function SearchPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search products..." />
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<SlowList query={deferredQuery} />
</div>
</>
)
}useDeferredValue vs useTransition
| useDeferredValue | useTransition | |
|---|---|---|
| Defer | A value | A state update |
| API | useDeferredValue(q) | startTransition(() => setQ()) |
| Pending flag | Compare value !== deferred | isPending boolean |
| Use when | Value passed to child | You control the setState |
// useTransition alternative
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
setInput(e.target.value) // urgent
startTransition(() => setQuery(e.target.value)) // deferred
}364. useLayoutEffect Hook
Theory
useLayoutEffect runs synchronously after DOM mutations but before the browser paints. Same signature as useEffect but fires earlier.
Use when: you need to measure DOM or mutate layout before user sees the screen — prevents visual flicker.
Use useEffect for: everything else — data fetching, subscriptions (doesn't block paint).
Comparison
| useEffect | useLayoutEffect | |
|---|---|---|
| Runs | After paint | Before paint |
| Blocks paint? | No | Yes |
| Use for | API calls, subscriptions | DOM measurements, sync layout |
| SSR warning? | No | Yes — server has no layout |
Real Example
// ❌ useEffect — tooltip flickers at (0,0) then jumps
function Tooltip({ targetRef }) {
const [pos, setPos] = useState({ x: 0, y: 0 })
useEffect(() => {
const rect = targetRef.current.getBoundingClientRect()
setPos({ x: rect.left, y: rect.bottom })
}, [targetRef])
return <div style={{ left: pos.x, top: pos.y }}>Tip</div>
}
// ✅ useLayoutEffect — position calculated before paint
function Tooltip({ targetRef }) {
const [pos, setPos] = useState({ x: 0, y: 0 })
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect()
setPos({ x: rect.left, y: rect.bottom })
}, [targetRef])
return <div style={{ left: pos.x, top: pos.y }}>Tip</div>
}// Other valid uses: focus on mount, scroll to element, animate from measured size
useLayoutEffect(() => {
inputRef.current?.focus()
}, [])365. useTransition & useDeferredValue
Theory
useTransition— mark state updates as non-urgent (low priority). Returns[isPending, startTransition].useDeferredValue— defer re-rendering a value until urgent updates finish.
Both keep UI responsive during heavy updates (filtering large lists, tab switches).
Real Example
function SearchResults({ query }) {
const [isPending, startTransition] = useTransition()
const [results, setResults] = useState(allItems)
const handleChange = (q) => {
setInput(q)
startTransition(() => setResults(filterItems(allItems, q)))
}
return (
<>
<input value={input} onChange={(e) => handleChange(e.target.value)} />
{isPending && <Spinner />}
<List items={results} />
</>
)
}366. Web Accessibility and Semantic Elements
Theory
Web Accessibility (a11y) ensures apps are usable by everyone — screen readers, keyboard users, motor impairments. Semantic HTML uses meaningful tags (<nav>, <main>, <article>) instead of generic <div>.
WCAG principles: Perceivable, Operable, Understandable, Robust.
Semantic elements
| Element | Purpose |
|---|---|
<header> | Page/section header |
<nav> | Navigation links |
<main> | Primary content (one per page) |
<article> | Self-contained content |
<section> | Thematic grouping |
<aside> | Sidebar, related content |
<footer> | Footer info |
<button> | Actions (not <div onClick>) |
Real Example
// ❌ Inaccessible
<div onClick={submit}>Submit</div>
<div className="nav">...</div>
// ✅ Accessible
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/dashboard">Dashboard</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<article>
<h1>Transaction History</h1>
<button type="submit" onClick={submit}>Submit</button>
</article>
</main>367. What are microservices?
Theory
Microservices is an architectural style where an application is built as a collection of small, independently deployable services, each owning a specific business capability. Services communicate via HTTP/REST, gRPC, or message queues.
Each service has its own database, codebase, and deployment pipeline.
Pros & Cons
| Pros | Cons |
|---|---|
| Independent scaling per service | Distributed system complexity |
| Team autonomy per service | Network latency between services |
| Technology diversity | Data consistency challenges |
| Fault isolation | Operational overhead (monitoring, logging) |
Real-Life Example
Food delivery platform microservices:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Restaurant │ │ Order │ │ Payment │
│ Service │ │ Service │ │ Service │
│ (menus, │ │ (cart, │ │ (Razorpay, │
│ search) │ │ tracking) │ │ refunds) │
└──────┬───────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└──────── API Gateway ─────────────┘
│
Frontend App
(calls gateway,
not individual services)Frontend impact: BFF (Backend for Frontend) aggregates multiple microservice calls into one API tailored for the UI.
368. What are Microtasks and Macrotasks?
Theory
The event loop processes two types of async callbacks in strict priority:
Microtasks (higher priority — all drained before next macrotask):
Promise.then/catch/finallyqueueMicrotask()MutationObserverawait(resumes as microtask)
Macrotasks (lower priority — one per loop iteration):
setTimeout/setIntervalsetImmediate(Node.js)- I/O callbacks
- UI rendering events (click, scroll)
requestAnimationFrame(before paint, separate queue)
Pros & Cons
| Microtasks | Macrotasks |
|---|---|
| ✅ Run immediately after current sync code | ✅ Yield to browser rendering |
| ✅ Fast promise resolution | ✅ Better for deferring heavy work |
| ❌ Can starve macrotasks if infinite chain | ❌ Slower than microtasks |
Real-Life Example
console.log('1: sync start')
setTimeout(() => console.log('5: macrotask — setTimeout'), 0)
Promise.resolve()
.then(() => console.log('3: microtask 1'))
.then(() => console.log('4: microtask 2'))
queueMicrotask(() => console.log('3b: microtask 3'))
console.log('2: sync end')
// Output: 1 → 2 → 3 → 3b → 4 → 5// Microtask starvation — avoid in production
function poisonLoop() {
Promise.resolve().then(poisonLoop) // blocks setTimeout and UI updates forever
}
// poisonLoop(); // never do this// Practical React pattern
function CheckoutButton() {
const handleClick = async () => {
console.log('1: click handler sync')
await processPayment() // yields — microtask resumes after promise resolves
console.log('3: after await')
setTimeout(() => console.log('5: redirect'), 1000)
}
return <button onClick={handleClick}>Pay Now</button>
}
// Click output: 1 → (payment API) → 3 → (1 second) → 5Microtask vs Macrotask — when to use which
Use microtask (Promise, queueMicrotask) | Use macrotask (setTimeout, requestAnimationFrame) |
|---|---|
| Chain async operations | Defer work to next frame |
| Process promise results | Allow browser to paint between chunks |
| DOM measurements after state update | Break up long computations |
// Break heavy work across macrotasks to keep UI responsive
function processInChunks(items, chunkSize = 100) {
let index = 0
function processChunk() {
const end = Math.min(index + chunkSize, items.length)
for (; index < end; index++) processItem(items[index])
if (index < items.length) {
setTimeout(processChunk, 0) // macrotask — browser can paint between chunks
}
}
processChunk()
}| Area | Key Topics in This Guide |
|---|---|
| JavaScript core | Event Loop, Hoisting, Micro/Macrotasks, map, bind, second largest |
| React Hooks & patterns | useEffect, useCallback, useMemo, HOC, Props Drilling |
| Routing & API | React Router v6, Axios interceptors, protected routes |
| Frontend security | XSS, token storage, CSP, CSRF, env vars, npm audit |
| Performance | Debounce, Throttle, useMemo, chunked processing |
| Logic questions | Second largest element, event loop output prediction |
Quick Revision Cheat Sheet
| # | Topic | One-liner |
|---|---|---|
| 1 | Event Loop | Sync → all microtasks → one macrotask → repeat |
| 2 | Second largest | One-pass with largest + secondLargest variables, O(n) |
| 3 | Debounce vs Throttle | Wait for pause vs once per interval |
| 4 | map vs bind | Transform array vs fix this on function |
| 5 | Axios | HTTP client with interceptors, JSON, cancellation |
| 6 | React Router | BrowserRouter → Routes → Route; useParams, protected routes |
| 7 | Frontend security | XSS sanitize, httpOnly cookies, no secrets in env, HTTPS |
| 8 | Hooks | useEffect = side effects; useMemo = value; useCallback = function |
| 9 | HOC | Wrap component for reuse; prefer custom hooks today |
| 10 | Hoisting | var/function hoisted; let/const in TDZ until declared |
| 11 | Props Drilling | Pass through unused levels; fix with Context/composition |
| 12 | Event Bubbling | Target → parents; enables delegation |
| 13 | Micro vs Macro | Promises first, then setTimeout; one macro per loop |
369. What are Smart Pointers?
Theory
Smart pointers are RAII wrappers around raw pointers that automatically manage memory — they delete the object when no longer needed.
| Type | Ownership | Copyable |
|---|---|---|
unique_ptr | Exclusive | No |
shared_ptr | Shared (ref count) | Yes |
weak_ptr | Non-owning observer | Yes |
#include <memory>
void demo() {
// Raw pointer — manual delete, leak-prone
int* raw = new int(42);
delete raw;
// Smart pointer — automatic cleanup
auto smart = std::make_unique<int>(42);
// deleted when `smart` goes out of scope
}370. What do you do for security purposes in frontend applications?
Theory
Frontend security protects users and data in the browser. While true security enforcement happens on the server, the frontend must prevent common attacks and never trust client-side code alone.
Key areas: XSS, CSRF, authentication, data exposure, dependency security, and secure communication.
Pros & Cons of frontend security layers
| Practice | Benefit | Limitation |
|---|---|---|
| Input sanitization | Prevents XSS | Server must also validate |
| httpOnly cookies | Tokens not accessible to JS | Requires backend support |
| CSP headers | Blocks inline script injection | Can break third-party scripts |
| HTTPS only | Encrypts data in transit | Doesn't protect against XSS |
Real-Life Example — Security checklist
1. Prevent XSS (Cross-Site Scripting)
// ❌ Dangerous — renders raw HTML from user/API
<div dangerouslySetInnerHTML={{ __html: userComment }} />
// ✅ Safe — React escapes by default
<div>{userComment}</div>
// ✅ If HTML needed — sanitize first
import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userComment) }} />2. Secure token storage
// ❌ Never store tokens in localStorage (accessible to any JS/XSS)
localStorage.setItem('token', accessToken)
// ✅ Use httpOnly, Secure, SameSite cookies (set by backend)
// Frontend cannot read it — immune to XSS token theft
// Backend sets: Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict3. Authentication & authorization
// Frontend checks are UX only — backend MUST enforce
function AdminPanel() {
const { user } = useAuth()
// UI guard
if (!user?.roles.includes('admin')) {
return <Navigate to="/unauthorized" />
}
return <AdminDashboard />
}
// API calls always include auth — backend validates
apiClient.get('/api/admin/users') // 403 if not admin, regardless of UI4. Environment variables
// ✅ Public keys only in frontend env
VITE_API_URL=https://api.example.com
VITE_GOOGLE_CLIENT_ID=abc123 // public OAuth client ID — OK
// ❌ NEVER expose secrets in frontend
VITE_STRIPE_SECRET_KEY=sk_live_xxx // visible in bundled JS!5. Content Security Policy (CSP)
<!-- Set via server headers -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" />6. CSRF protection
// Include CSRF token in state-changing requests
apiClient.post('/api/orders', orderData, {
headers: { 'X-CSRF-Token': getCsrfTokenFromCookie() },
})7. Dependency security
npm audit
npm audit fix8. Other practices
| Practice | Why |
|---|---|
| HTTPS everywhere | Encrypt data in transit |
| Validate/sanitize all inputs | Client + server validation |
| Rate limit sensitive actions | Prevent brute force on login |
| Logout clears all session data | Prevent session fixation |
| Subresource Integrity (SRI) | Verify CDN scripts haven't been tampered |
| Don't log sensitive data | Console logs visible in production tools |
Interview answer (concise)
371. What happens during object construction and destruction?
Construction order
1. Base class constructors (in declaration order)
2. Member variables (in declaration order, not initializer list order)
3. Constructor body executesclass Base {
public:
Base() { std::cout << "Base ctor\n"; }
~Base() { std::cout << "Base dtor\n"; }
};
class Derived : public Base {
int a;
std::string b;
public:
Derived() : a(1), b("hello") {
std::cout << "Derived ctor body\n";
}
~Derived() { std::cout << "Derived dtor\n"; }
};
// Output on creation:
// Base ctor
// Derived ctor body
// Output on destruction (reverse order):
// Derived dtor
// Base dtorDestruction order (reverse of construction)
- Derived destructor body
- Derived member destructors (reverse declaration order)
- Base class destructor
Virtual destructor rule
class Base {
public:
virtual ~Base() = default; // REQUIRED if deleting via base pointer
};
class Derived : public Base { /* ... */ };
Base* ptr = new Derived();
delete ptr; // calls Derived::~Derived then Base::~Base
// Without virtual ~Base, only Base destructor runs → leak372. What happens internally when a state update is triggered?
Step-by-step internals
// You write:
setCount(count + 1)
// Internally (simplified):- Update object created —
{ lane: SyncLane, action: (c) => c + 1, eagerState: ... } - Enqueued on Fiber — attached to
fiber.updateQueue(circular linked list) - Eager evaluation — React may compute new state immediately for bailout check
- Bailout? — If
Object.is(oldState, newState), skip scheduling - Schedule work —
scheduleUpdateOnFiber(root, fiber, lane) - Render scheduled —
requestUpdateLane→ensureRootIsScheduled - Work loop runs —
workLoopConcurrentorworkLoopSync - Component re-executed — hooks read updated
memoizedState - Reconciliation — diff old vs new child elements
- Commit — DOM patched if diff has changes
Batching behavior
function handleClick() {
setCount((c) => c + 1)
setFlag((f) => !f)
// React 18+: both batched → single re-render
}
// Also batched in timeouts and native events (React 18+)
setTimeout(() => {
setCount(1)
setFlag(true)
}, 1000) // single re-renderflushSync — opt out of batching
import { flushSync } from 'react-dom'
flushSync(() => {
setCount(1)
})
// DOM updated synchronously here
setFlag(true) // separate render373. What is providesTags and invalidatesTags?
Answer
providesTags: Used in queries to tag cached data.invalidatesTags: Used in mutations to tell RTK Query to invalidate and refetch related data.
374. What is an event in React?
Answer
Events in React are handled using camelCase syntax and JavaScript functions. They are used to respond to user actions like clicks, typing, or form submissions.
<button onClick={handleClick}>Click Me</button>375. What is defaultProps?
Answer
defaultProps allows you to set default values for props in class components. It ensures props have a fallback value if not provided.
MyComponent.defaultProps = {
name: 'Guest',
}376. What is Event Bubbling?
Theory
When an event fires on a DOM element, it goes through three phases:
- Capturing — travels from
windowdown to the target - Target — reaches the element that triggered the event
- Bubbling — travels from target back up to
window
By default, event listeners run in the bubbling phase. This enables event delegation — attaching one listener on a parent instead of many on children.
event.stopPropagation() stops the event from bubbling further. event.stopImmediatePropagation() also prevents other listeners on the same element.
Pros & Cons
| Event Bubbling | Event Capturing |
|---|---|
| ✅ Enables delegation — fewer listeners | ✅ Intercept before children handle |
| ✅ Natural default behavior | ❌ Less intuitive |
| ❌ Children can unintentionally block parents | Used with { capture: true } |
Real-Life Example
<div id="card">
<button id="add-to-cart">Add to Cart</button>
<button id="favorite">♥</button>
</div>// Bubbling order when "Add to Cart" is clicked:
// 1. button (target)
// 2. div#card (bubbling)
// 3. body → html → window
document.getElementById('card').addEventListener('click', (e) => {
const button = e.target.closest('button')
if (!button) return
if (button.id === 'add-to-cart') addToCart()
if (button.id === 'favorite') toggleFavorite()
})
// One listener handles all buttons — works for dynamically added buttons too// React — bubbling works the same
function ProductCard({ onAddToCart }) {
return (
<div className="card" onClick={() => console.log('card clicked')}>
<button
onClick={(e) => {
e.stopPropagation() // prevent card click from firing
onAddToCart()
}}>
Add to Cart
</button>
</div>
)
}
// Modal — stop propagation on content to prevent overlay close
function Modal({ onClose, children }) {
return (
<div className="overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
)
}377. What is Event Capturing and Bubbling
Theory
When an event fires on a DOM element, it travels in three phases:
- Capturing phase — event travels from
window→ target (top down) - Target phase — event reaches the target element
- Bubbling phase — event travels from target →
window(bottom up)
By default, event listeners run in the bubbling phase. Use { capture: true } or addEventListener(el, fn, true) for capturing.
event.stopPropagation() stops further propagation. event.stopImmediatePropagation() also prevents other listeners on the same element.
Pros & Cons
| Capturing | Bubbling | |
|---|---|---|
| Pros | Intercept events before children handle them | Natural default, event delegation |
| Cons | Less intuitive, rarely needed | Children can block parent handlers |
| Use case | Focus management, analytics wrappers | Delegated click handlers on lists |
Real-Life Example
<div id="grandparent">
<div id="parent">
<button id="child">Click me</button>
</div>
</div>document.getElementById('grandparent').addEventListener('click', () => console.log('grandparent bubble'))
document.getElementById('parent').addEventListener('click', () => console.log('parent bubble'))
document.getElementById('child').addEventListener('click', () => console.log('child target'))
// Click button → Output: child target → parent bubble → grandparent bubble
// Event delegation (bubbling) — real-life pattern
document.getElementById('menu').addEventListener('click', (e) => {
const item = e.target.closest('[data-action]')
if (!item) return
handleAction(item.dataset.action) // one listener for 100 menu items
})378. What is forwardRef?
Answer
forwardRef is used to pass a ref through a component to one of its children.
const FancyInput = forwardRef((props, ref) => <input ref={ref} {...props} />)379. What is Props Drilling?
Theory
Props drilling is passing data through multiple intermediate components that don't need the data themselves, just to reach a deeply nested child. It makes components tightly coupled and hard to maintain.
App (user)
└── Header (passes user)
└── Navbar (passes user)
└── UserMenu (passes user)
└── Avatar (finally uses user) ← 4 levels deepPros & Cons of solutions
| Solution | Pros | Cons |
|---|---|---|
| Props drilling | Explicit, easy to trace | Unmaintainable at depth |
| Context API | No intermediate passing | All consumers re-render on change |
| State library (Zustand/Redux) | Scalable, devtools | Setup overhead |
| Component composition | Clean, no extra libraries | Only works for specific slots |
Real-Life Example
The problem
// ❌ Props drilling — theme passed through 4 levels
function App() {
const [theme, setTheme] = useState('dark')
return <Layout theme={theme} setTheme={setTheme} />
}
function Layout({ theme, setTheme }) {
return <Sidebar theme={theme} setTheme={setTheme} />
}
function Sidebar({ theme, setTheme }) {
return <ThemeToggle theme={theme} setTheme={setTheme} />
}
function ThemeToggle({ theme, setTheme }) {
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme}</button>
}Solution 1 — Context API
const ThemeContext = createContext()
function App() {
const [theme, setTheme] = useState('dark')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
)
}
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext)
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme}</button>
}Solution 2 — Component composition
function App() {
const [theme, setTheme] = useState('dark')
return <Layout sidebar={<ThemeToggle theme={theme} setTheme={setTheme} />} />
}
function Layout({ sidebar }) {
return (
<div>
<aside>{sidebar}</aside> {/* no drilling — direct slot */}
<main>...</main>
</div>
)
}Solution 3 — Zustand (scalable)
import { create } from 'zustand'
const useThemeStore = create((set) => ({
theme: 'dark',
toggle: () => set((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' })),
}))
function ThemeToggle() {
const { theme, toggle } = useThemeStore()
return <button onClick={toggle}>{theme}</button>
}Interview answer
380. What is RAII?
Theory
Resource Acquisition Is Initialization — bind resource lifetime to object lifetime. Acquire in constructor, release in destructor.
class FileHandle {
FILE* file;
public:
FileHandle(const char* path) : file(fopen(path, "r")) {
if (!file) throw std::runtime_error("Cannot open file");
}
~FileHandle() {
if (file) fclose(file); // always called, even on exception
}
// delete copy, allow move...
};
void readConfig() {
FileHandle f("config.txt");
// use f...
} // destructor closes file automatically — no leakRAII in standard library
| Resource | RAII wrapper |
|---|---|
| Memory | unique_ptr, shared_ptr |
| File | fstream |
| Mutex | lock_guard, unique_lock |
| Socket | custom RAII class |
Interview answer
381. What is Temporal Dead Zone (TDZ)?
Theory
The Temporal Dead Zone is the period between entering a scope and the line where a let or const variable is declared. During TDZ, the variable exists in the lexical environment but is not initialized — accessing it throws ReferenceError.
var does not have a TDZ — it's initialized as undefined when hoisted.
TDZ exists to:
- Catch errors from using variables before declaration
- Make
let/constbehave more predictably thanvar - Prevent issues with
classandconstbefore initialization
Pros & Cons
| TDZ (let/const) | No TDZ (var) |
|---|---|
| ✅ Catches bugs early | ❌ Returns undefined silently |
| ✅ Enforces declare-before-use | ❌ Hoisting confusion |
| ❌ ReferenceError if accessed early | ✅ "Forgiving" but dangerous |
Real-Life Example
// TDZ demonstration
{
// TDZ for `value` starts here (scope entry)
// console.log(value); // ReferenceError — in TDZ
let value = 42 // TDZ ends here
console.log(value) // 42
}
// typeof and TDZ
typeof undeclaredVar // "undefined" (safe — no ReferenceError)
// typeof tdzVar; // ReferenceError — tdzVar is in TDZ
let tdzVar = 10
// TDZ with const
const API_KEY = import.meta.env.VITE_API_KEY
// Must be declared before use — cannot access before line
// Class TDZ
// const instance = new Service(); // ReferenceError — class in TDZ
class Service {
connect() {
return 'connected'
}
}
const instance = new Service() // OK// Why TDZ matters — prevents this bug with var
function init() {
console.log(config) // undefined (var) — silent bug
var config = { debug: true }
}
function initSafe() {
// console.log(config); // ReferenceError (let) — caught immediately
let config = { debug: true }
}Interview answer (concise)
382. What is the difference between var, let, and const?
Theory
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function-scoped | Block-scoped | Block-scoped |
| Hoisting | Hoisted, initialized as undefined | Hoisted but in TDZ until declaration | Hoisted but in TDZ until declaration |
| Reassignment | Allowed | Allowed | Not allowed (binding is constant) |
| Redeclaration | Allowed in same scope | Not allowed | Not allowed |
| Global object property | window.x in browsers | No | No |
TDZ (Temporal Dead Zone): The period between entering a scope and the line where let/const is declared. Accessing the variable in TDZ throws ReferenceError.
Important nuance for const: const prevents rebinding, not mutation. Objects and arrays can still be mutated.
Practical Example
// --- Scope ---
function scopeDemo() {
if (true) {
var a = 1 // function-scoped
let b = 2 // block-scoped
const c = 3 // block-scoped
}
console.log(a) // 1
// console.log(b); // ReferenceError
// console.log(c); // ReferenceError
}
// --- Hoisting ---
console.log(x) // undefined (var is hoisted)
var x = 10
// console.log(y); // ReferenceError (TDZ)
let y = 20
// --- const mutation vs reassignment ---
const user = { name: 'Rahul' }
user.name = 'Amit' // ✅ OK — mutating object
// user = {}; // ❌ TypeError — rebinding
// --- Classic var loop bug ---
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100) // 0, 1, 2
}383. What is the difference between children and props?
Answer
propsare custom attributes passed to a component.childrenis a special prop that includes any nested elements or components passed between the component's opening and closing tags.
<MyComponent>Hello</MyComponent> // "Hello" is passed as childrenTop 15 Redux Toolkit + RTK Query Interview Questions (With Answers)
These are essential interview questions and answers focused on Redux Toolkit and RTK Query, ideal for beginner to intermediate developers.
384. What is the most challenging task you handled in your project?
Theory
This is a behavioral question. Interviewers assess problem-solving, ownership, technical depth, and communication. Use the STAR method:
- Situation — project context and constraints
- Task — your specific responsibility
- Action — technical decisions and steps you took
- Result — measurable outcome
the company values practical experience, so anchor your answer in real technical work — performance, legacy migration, production bugs, or cross-team coordination.
Pros & Cons of answer strategies
| Do | Avoid |
|---|---|
| Quantify impact (load time, error rate, users affected) | Vague answers without technical detail |
| Explain trade-offs you considered | Blaming teammates |
| Show what you learned | Picking a trivial task |
| Mention tools (React Profiler, Redux DevTools) | Rambling over 3 minutes |
Real-Life Example — Sample answer skeleton
Tips for your own answer
Pick a story where you can naturally mention React, TypeScript, or Redux — the company's focus areas. Have 2–3 stories ready and choose based on the conversation flow.
385. What is Web Accessibility?
Theory
Web Accessibility (a11y) means designing and building websites so that everyone can use them — including people with visual, auditory, motor, cognitive, and neurological disabilities. It also benefits users on slow networks, mobile devices, and older browsers.
Standards are defined by WCAG (Web Content Accessibility Guidelines) with levels A, AA, and AAA. Most companies target WCAG 2.1 AA.
Core principles (POUR):
- Perceivable — content available to all senses (alt text, captions)
- Operable — keyboard navigation, no seizure triggers
- Understandable — clear language, predictable behavior
- Robust — works with assistive technologies (screen readers)
Pros & Cons
| Accessible apps | Ignoring accessibility |
|---|---|
| ✅ Larger user base (15%+ have disabilities) | ❌ Legal risk (ADA, EAA, RPwD Act) |
| ✅ Better SEO (semantic HTML) | ❌ Lost enterprise contracts |
| ✅ Better UX for everyone | ❌ Fails Lighthouse a11y audits |
| ✅ Required by many clients | ❌ Keyboard users can't navigate |
Real-Life Example
// ❌ Inaccessible
<div onClick={submitOrder}>Place Order</div>
<input placeholder="Email" />
<div className="error">Invalid email</div>
// ✅ Accessible
<button type="submit" onClick={submitOrder}>
Place Order
</button>
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
aria-describedby="email-hint email-error"
aria-invalid={hasError}
autoComplete="email"
/>
<span id="email-hint" className="sr-only">
Enter your registered email address
</span>
{hasError && (
<p id="email-error" role="alert">
Please enter a valid email address
</p>
)}// Modal — focus trap + ARIA
function ConfirmModal({ isOpen, onClose, onConfirm, title, children }) {
const modalRef = useRef(null)
useEffect(() => {
if (!isOpen) return
modalRef.current?.focus()
const handleEsc = (e) => e.key === 'Escape' && onClose()
document.addEventListener('keydown', handleEsc)
return () => document.removeEventListener('keydown', handleEsc)
}, [isOpen, onClose])
if (!isOpen) return null
return (
<div className="overlay" role="presentation" onClick={onClose}>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
onClick={(e) => e.stopPropagation()}>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onConfirm}>Confirm</button>
<button onClick={onClose}>Cancel</button>
</div>
</div>
)
}Quick checklist:
| Practice | Example |
|---|---|
| Semantic HTML | <nav>, <main>, <button>, not <div onClick> |
| Alt text | <img alt="Chicken Biryani from Spice Garden" /> |
| Keyboard | Tab through all interactive elements |
| Focus visible | :focus-visible outline styles |
| Color contrast | 4.5:1 for normal text (WCAG AA) |
| Skip link | "Skip to main content" |
| Live regions | aria-live="polite" for status updates |
npm install -D eslint-plugin-jsx-a11y @axe-core/reactInterview answer (concise)
386. What was your role in your last project?
Theory
Use STAR method — Situation, Task, Action, Result. For senior roles, emphasize ownership, decisions, collaboration, and measurable impact.
Real Example — Sample answer
387. Why did you choose React?
Theory
Interviewers want genuine reasoning — not "because it's popular." Connect React's strengths to real problems you've solved.
Pros & Cons (why React vs alternatives)
| React | Angular | Vue |
|---|---|---|
| ✅ Flexible — library not framework | ✅ Full framework, opinionated | ✅ Gentle learning curve |
| ✅ Huge ecosystem and hiring pool | ❌ Steeper learning curve | ❌ Smaller enterprise adoption in India |
| ✅ Component reusability | ||
| ✅ Virtual DOM performance | ||
| ❌ Needs choices (router, state) |
Real Example — Strong answer
388. Why does CSS block rendering?
Theory
Browsers build the render tree by combining the DOM and CSSOM (CSS Object Model). The browser cannot render until it knows how elements look — so CSS is a render-blocking resource.
Pipeline:
HTML → DOM tree
CSS → CSSOM tree
DOM + CSSOM → Render tree → Layout → Paint → CompositeIf CSS hasn't loaded, the browser would show unstyled content (FOUC — Flash of Unstyled Content), then repaint when CSS arrives — causing layout shift and wasted work. So browsers block rendering until critical CSS is parsed.
JavaScript can also block rendering because document.write and synchronous scripts can modify
DOM/CSSOM. CSS blocking is about the render tree dependency, not JS execution per se.
Pros & Cons of render-blocking CSS
| Blocking (default) | Non-blocking (media trick / async) |
|---|---|
| ✅ No FOUC | ✅ Faster first paint |
| ✅ Correct layout on first paint | ❌ Flash of unstyled content |
| ❌ Delays first render | ❌ CLS risk |
Real-Life Example
<!-- Render-blocking — browser waits before painting -->
<link rel="stylesheet" href="styles.css" />
<!-- Non-render-blocking — loads but doesn't block render -->
<link rel="stylesheet" href="print.css" media="print" />
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)" />
<!-- Critical CSS — inline above-the-fold styles -->
<style>
.header {
background: #0d0d0d;
height: 64px;
}
.hero {
min-height: 400px;
}
</style>
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'" />Optimization strategies a fintech app expects you to know:
- Inline critical CSS for above-the-fold
mediaattribute for non-critical stylesheetspreloadfor important CSS- Avoid
@importin CSS (serial loading) - Minimize CSS size — remove unused (PurgeCSS)
Interview answer (concise)
389. Why use React?
Answer
React offers several benefits:
- Declarative syntax makes code more predictable and easier to debug.
- Component-based architecture encourages reusable, maintainable code.
- It uses a Virtual DOM for faster rendering.
- It's backed by a strong community and ecosystem.
390. Write unit tests for critical functionality
Theory
Test behavior, not implementation. Focus on critical paths: authentication, payments, form validation, data transformations, and user-facing interactions. Use React Testing Library — query by role/label (how users see the UI), not by CSS class or internal state.
Testing trophy: More integration tests, fewer shallow unit tests, some E2E (Playwright/Cypress).
Pros & Cons
| Testing critical paths | No tests |
|---|---|
| ✅ Catch regressions before deploy | ❌ Production bugs cost more to fix |
| ✅ Confidence to refactor | ❌ Fear of changing code |
| ✅ Living documentation | ❌ Manual QA only — slow and incomplete |
| ❌ Initial time investment | — |
Real-Life Example
// utils/pricing.test.ts — pure logic, fast tests
import { calculateOrderTotal } from './pricing'
describe('calculateOrderTotal', () => {
it('applies free delivery above ₹500', () => {
const items = [{ price: 300, quantity: 2 }] // subtotal = 600
const result = calculateOrderTotal(items)
expect(result.deliveryFee).toBe(0)
})
it('charges ₹40 delivery below ₹500', () => {
const items = [{ price: 200, quantity: 1 }]
const result = calculateOrderTotal(items)
expect(result.deliveryFee).toBe(40)
})
})// components/LoginForm.test.tsx — user behavior
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LoginForm } from './LoginForm'
describe('LoginForm', () => {
it('shows error on invalid credentials', async () => {
const user = userEvent.setup()
render(<LoginForm onSubmit={jest.fn()} />)
await user.type(screen.getByLabelText(/email/i), 'wrong@test.com')
await user.type(screen.getByLabelText(/password/i), 'wrongpass')
await user.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/invalid credentials/i)
})
})
it('disables submit while loading', async () => {
render(<LoginForm onSubmit={() => new Promise(() => {})} />)
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled()
})
})What to test vs skip:
| Test ✅ | Skip ❌ |
|---|---|
| Payment/checkout flow | Third-party library internals |
| Form validation rules | CSS styling (use visual regression) |
| Auth guards / protected routes | Every trivial render |
| Business logic (utils) | Implementation details (state variable names) |
391. Your application has 10,000 blog posts. How would you generate and serve these pages efficiently?
Description
Rendering 10,000 pages at every build can be slow. You need a strategy that balances SEO, build time, and freshness — typically SSG + ISR or dynamic SSR with caching, not 10K blocking builds on every deploy.
Theory
| Approach | When to use |
|---|---|
| SSG all 10K at build | OK if build pipeline handles it (~minutes) |
ISR + generateStaticParams | Pre-render popular posts; generate rest on first visit |
| On-demand ISR | CMS webhook triggers revalidatePath |
| Pagination + SSG listing | List pages static; detail on demand |
generateStaticParams — pre-build top N; others generated at runtime.
Pros & Cons
| ISR (incremental) | Full SSG every deploy |
|---|---|
| ✅ Fast deploys | ✅ All pages instantly cached at edge |
| ✅ Scales to millions of pages | ❌ Build time grows linearly |
| ❌ First visitor pays generation cost | ❌ Rebuild all 10K for one typo fix |
Real Example — Blog with 10,000 posts
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export const revalidate = 3600 // ISR — revalidate every hour
// Pre-build top 500 posts; rest on-demand
export async function generateStaticParams() {
const posts = await fetch(`${process.env.API_URL}/posts?top=500`).then((r) => r.json())
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
async function getPost(slug: string) {
const res = await fetch(`${process.env.API_URL}/posts/${slug}`, {
next: { tags: [`post-${slug}`] },
})
if (res.status === 404) return null
return res.json()
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
)
}// app/blog/page.tsx — paginated listing (SSG per page)
export async function generateStaticParams() {
const { totalPages } = await getPostCount() // e.g. 400 pages × 25 posts
return Array.from({ length: totalPages }, (_, i) => ({
page: String(i + 1),
}))
}// CMS webhook — revalidate single post
// app/api/revalidate/post/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
export async function POST(req: Request) {
const { slug, secret } = await req.json()
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
revalidateTag(`post-${slug}`)
revalidatePath('/blog')
return Response.json({ ok: true })
}Serving: Deploy to Vercel/Cloudflare — static HTML at edge CDN; ISR regenerates in background.
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime