React —
Zero to Interview
Ready
Every topic asked in Indian interviews — from 1-year to 10-year experience. Fundamentals, hooks, performance, code splitting, polyfills, Web Vitals, SSR, Concurrent Mode, and every pattern that gets asked in Flipkart, Swiggy, Razorpay, Google, Amazon India rounds.
What is React? + JSX
React is a JavaScript library (not a framework) by Meta for building UIs from composable components. It maintains a Virtual DOM, diffs it against the real DOM, and surgically updates only what changed. This is the answer to "Why React?" in every interview.
// JSX = JavaScript + XML syntax. Babel transforms it to React.createElement() // These two are IDENTICAL: // JSX way (what you write): const el = <h1 className="title">Hello, {name}!</h1>; // What Babel compiles it to: const el = React.createElement('h1', { className: 'title' }, `Hello, ${name}!`); // ─── JSX Rules ───────────────────────────────────────────── // 1. class → className (class is reserved in JS) // 2. for → htmlFor // 3. Self-close all empty tags: <img /> <input /> <br /> // 4. One root element (or use Fragment <></>) // 5. Expressions in {}, NOT statements function UserCard({ name, age, isOnline }) { return ( <> {/* Fragment — no extra div */} <h2>{name}</h2> <p>Age: {age}</p> <span className={isOnline ? 'online' : 'offline'}> {isOnline ? '● Online' : '○ Offline'} </span> {age >= 18 && <button>Adult Features</button>} {/* conditional render */} </> ); }
Components & Props
Components are the building blocks of React. Props are the inputs — read-only data passed from parent to child. Understanding prop types, default props, children, and prop drilling lays the foundation for everything that follows.
// ─── Functional Component (modern, preferred) ────────────── function Button({ label, onClick, variant = 'primary', disabled = false }) { return ( <button onClick={onClick} disabled={disabled} className={`btn btn-${variant}`} > {label} </button> ); } // ─── Children prop ───────────────────────────────────────── function Card({ title, children }) { return ( <div className="card"> <h3>{title}</h3> <div className="card-body">{children}</div> {/* slot pattern */} </div> ); } // Usage: <Card title="Profile"><UserCard /></Card> // ─── Props spreading (careful — don't overuse) ───────────── function Input({ label, ...rest }) { // rest = remaining props return ( <label> {label} <input {...rest} /> {/* spread all HTML attrs */} </label> ); } // ─── Prop types of data you can pass ────────────────────── <MyComponent str="hello" // string num={42} // number (curly braces!) bool={true} // boolean (or just write: bool) arr={[1, 2, 3]} // array obj={{ key: 'val' }} // object fn={() => {}} // function jsx={<Icon />} // JSX element />
Context API for global/shared state. (2) Component composition — pass the child component directly instead of its data. (3) State management libraries (Redux, Zustand). Prefer composition first, then Context, then Redux.State & Event Handling
State is private, mutable data owned by a component. When state changes, React re-renders the component. Understanding when to use state vs props, and how events work, is fundamental for every interview.
import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); // [value, setter] // ─── NEVER mutate state directly ────────────────────── // ❌ count++ (mutation — won't trigger re-render) // ✅ setCount(count + 1) (creates new value, triggers re-render) // ─── Functional update (when new state depends on old) ─ const increment = () => setCount(prev => prev + 1); // safe // ─── State batching (React 18 batches everything) ────── const handleClick = () => { setCount(c => c + 1); // batched — only ONE re-render total setCount(c => c + 1); // functional updates still stack correctly }; // count increases by 2, but 1 re-render // ─── Object state — spread to preserve other keys ───── const [form, setForm] = useState({ name: '', email: '' }); const handleChange = (e) => setForm(prev => ({ ...prev, // keep other fields [e.target.name]: e.target.value // update only this field })); return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> {/* Synthetic events — React wraps native events */} <input onChange={handleChange} name="name" /> <form onSubmit={(e) => { e.preventDefault(); }}> {/* prevent default! */} </div> ); }
setState, React schedules a re-render — it doesn't happen synchronously. After the function finishes, React batches all state updates and does one re-render. If you need the new state value immediately, use a useEffect with the state as a dependency, or use functional updates like setState(prev => prev + 1).Virtual DOM & Reconciliation
The Virtual DOM is React's secret weapon. Understanding it — and the diffing algorithm — is asked in almost every interview in India, from startups to Google. This is where React's performance story begins.
React keeps two VDOMs: previous and next. It diffs them (O(n) with heuristics), finds the minimal set of DOM operations, then batches and applies them to the real DOM.
Component Lifecycle & Hooks Overview
Class components had lifecycle methods. Hooks replaced them for functional components. You must know both — class components still exist in old codebases and lifecycle questions still appear in interviews.
| Class Lifecycle | Hook Equivalent | When It Runs |
|---|---|---|
| constructor() | useState / useReducer | Component created — initialize state |
| componentDidMount() | useEffect(() => {}, []) | After first render — fetch data, subscriptions |
| componentDidUpdate() | useEffect(() => {}, [deps]) | After every render when deps changed |
| componentWillUnmount() | useEffect cleanup (return fn) | Before component removed — cleanup timers, listeners |
| shouldComponentUpdate() | React.memo / useMemo | Decide whether to re-render |
| render() | return JSX from function | Every render |
| componentDidCatch() | No hook equivalent | Error boundary — still needs class component |
Error Boundaries cannot be implemented with hooks — they still require class components with componentDidCatch and getDerivedStateFromError. This is the one remaining reason you'd write a class component today.
useState & useReducer
useState is for simple state. useReducer is for complex state with multiple related updates — the same pattern as Redux but local to one component. Choosing between them correctly shows seniority.
// ─── useState — for simple, independent pieces of state ──── const [count, setCount] = useState(0); const [user, setUser] = useState(null); const [items, setItems] = useState([]); // Lazy initializer — function runs ONCE (avoids expensive recompute) const [data, setData] = useState(() => JSON.parse(localStorage.getItem('data'))); // ─── useReducer — for complex state logic ────────────────── const initialState = { count: 0, loading: false, error: null }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'SET_LOADING': return { ...state, loading: action.payload }; case 'SET_ERROR': return { ...state, error: action.payload, loading: false }; default: return state; } } function MyComponent() { const [state, dispatch] = useReducer(reducer, initialState); const fetchData = async () => { dispatch({ type: 'SET_LOADING', payload: true }); try { const data = await api.fetch(); dispatch({ type: 'INCREMENT' }); } catch (e) { dispatch({ type: 'SET_ERROR', payload: e.message }); } }; }
useReducer when: (1) Next state depends on previous state in complex ways. (2) Multiple pieces of state update together (loading, error, data — the "fetch" pattern). (3) State transitions have named actions — makes debugging much easier. (4) You have 3+ related useState calls that always update together. Rule of thumb: if your setState logic is complex enough to have bugs, move it to a reducer.useEffect — Deep Dive
useEffect is the most misunderstood hook. It's NOT a lifecycle method replacement — it's a synchronization tool. Getting the dependency array right, cleanup functions, and avoiding race conditions are key interview topics.
import { useState, useEffect } from 'react'; // ─── Three forms of useEffect ───────────────────────────── useEffect(() => {}); // runs after EVERY render (rarely want this) useEffect(() => {}, []); // runs ONCE after mount (componentDidMount) useEffect(() => {}, [userId]); // runs when userId changes // ─── Data fetching with cleanup (race condition fix) ─────── function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let cancelled = false; // ← race condition fix async function fetchUser() { const data = await fetch(`/api/users/${userId}`).then(r => r.json()); if (!cancelled) setUser(data); // only set if component still mounted } fetchUser(); return () => { cancelled = true; }; // cleanup: cancel stale request }, [userId]); // re-fetch when userId changes return user ? <div>{user.name}</div> : <div>Loading...</div>; } // ─── Event listeners with cleanup ───────────────────────── useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); // ← critical! }, []);
1. Missing dependency: using a variable in the effect but not listing it → stale closure bug. ESLint exhaustive-deps catches this.
2. Object/array in deps: {a:1} !== {a:1} in JS → infinite loop. Use useMemo or move object inside effect.
3. No cleanup: event listeners, timers, subscriptions leak memory → always return cleanup function.
4. Async function directly: useEffect callback can't be async — create an inner async function.
useEffect(() => { setInterval(() => console.log(count), 1000) }, []) — count will always log 0 because the effect closed over the initial value. Fix: add count to deps array, or use useRef to store the latest value, or use functional state updates.useRef, useContext & useMemo
// ─── useRef — persists value without re-render ───────────── const inputRef = useRef(null); // DOM reference const timerRef = useRef(null); // mutable value, NO re-render on change const prevCount = useRef(count); // track previous value // DOM access: <input ref={inputRef} /> const focus = () => inputRef.current.focus(); // imperatively focus input // ─── useContext — consume context without Consumer wrapper ─ const ThemeContext = React.createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Child /> </ThemeContext.Provider> ); } function Child() { const theme = useContext(ThemeContext); // → "dark" return <div className={theme}>...</div>; } // ─── useMemo — memoize expensive computation ─────────────── const sortedList = useMemo(() => { return [...items].sort((a, b) => a.price - b.price); // expensive sort }, [items]); // only re-sort when items changes // ─── useCallback — memoize function reference ────────────── const handleDelete = useCallback((id) => { setItems(prev => prev.filter(item => item.id !== id)); }, []); // stable reference — won't cause child re-renders // Pass to React.memo children to prevent unnecessary re-renders const MemoizedList = React.memo(ExpensiveList);
useMemo memoizes a computed value (returns the value). useCallback memoizes a function reference (returns the function). useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useMemo to avoid expensive recalculation. Use useCallback to give stable function references to memoized child components so they don't re-render when the parent re-renders.Custom Hooks
Custom hooks are functions starting with "use" that encapsulate and reuse stateful logic across components. They're the modern replacement for HOCs and render props. Building one from scratch is a common interview task.
// ─── useFetch — data fetching hook ──────────────────────── function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); fetch(url) .then(r => r.json()) .then(d => { if(!cancelled) { setData(d); setLoading(false); }}) .catch(e => { if(!cancelled) setError(e); }); return () => { cancelled = true; }; }, [url]); return { data, loading, error }; } // Usage: const { data, loading } = useFetch('/api/users'); // ─── useDebounce — search input optimization ─────────────── function useDebounce(value, delay = 300) { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(timer); // cancel on every keystroke }, [value, delay]); return debounced; } // API call only fires 300ms after user stops typing! // ─── useLocalStorage ─────────────────────────────────────── function useLocalStorage(key, initialValue) { const [stored, setStored] = useState(() => { try { return JSON.parse(localStorage.getItem(key)) ?? initialValue; } catch { return initialValue; } }); const setValue = (value) => { setStored(value); localStorage.setItem(key, JSON.stringify(value)); }; return [stored, setValue]; } // ─── useOnClickOutside — dropdown/modal close ───────────── function useOnClickOutside(ref, handler) { useEffect(() => { const listener = (e) => { if (!ref.current?.contains(e.target)) handler(e); }; document.addEventListener('mousedown', listener); return () => document.removeEventListener('mousedown', listener); }, [ref, handler]); }
HOC, Render Props & Compound Components
Advanced component patterns asked in senior rounds. HOCs and Render Props are older patterns — important to know conceptually. Compound components are still actively used (Radix UI, Headless UI use them).
// ─── HOC — wraps a component, adds behavior ──────────────── function withAuth(WrappedComponent) { return function AuthGuard(props) { const { isLoggedIn } = useAuth(); if (!isLoggedIn) return <Navigate to="/login" />; return <WrappedComponent {...props} />; }; } const ProtectedDashboard = withAuth(Dashboard); // HOC composed // ─── Render Props — pass render function as prop ─────────── function DataProvider({ url, render }) { const { data, loading } = useFetch(url); return render({ data, loading }); // caller controls the UI } // Usage: <DataProvider url="/api/users" render={({ data, loading }) => loading ? <Spinner/> : <UserList data={data} />} /> // ─── Compound Components — Tab/Accordion pattern ─────────── const TabContext = React.createContext(); function Tabs({ children }) { const [active, setActive] = useState(0); return <TabContext.Provider value={{ active, setActive }}>{children}</TabContext.Provider>; } function TabList({ children }) { return <div role="tablist">{children}</div>; } function Tab({ index, children }) { const { active, setActive } = useContext(TabContext); return <button onClick={() => setActive(index)} aria-selected={active===index}>{children}</button>; } Tabs.List = TabList; Tabs.Tab = Tab; // Usage: <Tabs><Tabs.List><Tabs.Tab index={0}>...</Tabs.Tab></Tabs.List></Tabs>
State Management
The state management landscape has evolved dramatically. Context API, Redux Toolkit, Zustand, and React Query each solve different problems. Knowing which to use when is a top senior interview question.
| Tool | Best For | When to Use | Drawback |
|---|---|---|---|
| useState | Local UI state | Form, toggle, modal open/close | Can't share across tree |
| Context API | Low-frequency global state | Theme, auth, locale | Re-renders all consumers |
| Redux Toolkit | Complex global state | Large app, many features sharing state, time-travel debug | Boilerplate, learning curve |
| Zustand | Simple global state | Startup/mid app, replaces Context + Redux | Less structured than Redux |
| React Query / TanStack | Server state (API data) | Always — handles cache, loading, refetch, stale | Only for server data |
| Jotai / Recoil | Atomic state | Fine-grained updates, avoids re-render problems | Less mature ecosystem |
Server state ≠ Client state. Use React Query / TanStack Query for any data from an API — it handles caching, background refetching, loading/error states automatically. Use Zustand or Context for pure UI state (sidebar open, selected filters). Mixing them into one Redux store is the old way.
// Zustand — simplest global state (no Provider needed!) import { create } from 'zustand'; const useCartStore = create((set, get) => ({ items: [], total: 0, addItem: (item) => set(state => ({ items: [...state.items, item] })), removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })), clearCart: () => set({ items: [], total: 0 }), })); // In any component — no Provider, no connect(): function Cart() { const { items, addItem, removeItem } = useCartStore(); return <div>{items.length} items in cart</div>; }
React Router
React Router v6 is the standard for client-side routing. Nested routes, protected routes, dynamic params, and lazy-loaded routes are common interview topics and real-world patterns.
import { BrowserRouter, Routes, Route, useParams, useNavigate, useSearchParams, Navigate, Outlet } from 'react-router-dom'; function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/user/:id" element={<UserPage />} /> {/* Protected route pattern */} <Route element={<ProtectedLayout />}> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Route> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); } // Protected Layout using Outlet function ProtectedLayout() { const { user } = useAuth(); if (!user) return <Navigate to="/login" replace />; return <Outlet />; {/* renders child routes */} } // Hooks const { id } = useParams(); // /user/123 → id = "123" const navigate = useNavigate(); // navigate('/home') const [params, setParams] = useSearchParams(); // ?page=2 → params.get('page')
Re-renders, Memoization & Optimization
A component re-renders when: state changes, props change, parent re-renders, or context value changes. Senior interviews at Swiggy, Razorpay, and FAANG dig deep into when to prevent re-renders and how.
// ─── React.memo — skip re-render if props unchanged ──────── const ExpensiveChild = React.memo(({ items, onDelete }) => { return items.map(item => <Item key={item.id} {...item} onDelete={onDelete} />); }); function Parent() { const [count, setCount] = useState(0); const [items] = useState([...]); // ❌ Without useCallback: new function ref on every parent render // → ExpensiveChild re-renders even when count changes (not items) // ✅ With useCallback: stable reference, ExpensiveChild skips re-render const handleDelete = useCallback((id) => { setItems(prev => prev.filter(i => i.id !== id)); }, []); return ( <> <button onClick={() => setCount(c => c + 1)}>{count}</button> <ExpensiveChild items={items} onDelete={handleDelete} /> </> ); } // ─── Virtualization — for huge lists ────────────────────── import { FixedSizeList } from 'react-window'; const Row = ({ index, style }) => <div style={style}>Row {index}</div>; // Only renders visible rows — 10,000 items, no lag <FixedSizeList height={600} itemCount={10000} itemSize={35} width="100%"> {Row} </FixedSizeList>
Code Splitting & Lazy Loading
Your entire React app ships as one JS bundle by default. Code splitting breaks it into smaller chunks loaded on demand. This is one of the highest-impact optimizations — asked in every senior interview in India.
import { lazy, Suspense } from 'react'; // ─── Route-based splitting (biggest impact) ──────────────── const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); const Reports = lazy(() => import('./pages/Reports')); function App() { return ( <Suspense fallback={<LoadingSpinner />}> {/* shown while chunk loads */} <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); } // Users only download Dashboard.chunk.js when they visit /dashboard // ─── Component-based splitting (heavy components) ────────── const HeavyChart = lazy(() => import('./components/HeavyChart')); const RichEditor = lazy(() => import('./components/RichEditor')); // ─── Preloading — start loading before user navigates ────── const preloadDashboard = () => import('./pages/Dashboard'); // fire early <NavLink onMouseEnter={preloadDashboard} to="/dashboard">Dashboard</NavLink>; // By the time they click, chunk is already loaded! // ─── Named chunks (webpack magic comment) ────────────────── import(/* webpackChunkName: "analytics" */ './Analytics');
React.lazy() + Suspense for component-level splitting. (2) Dynamic import() for any module. (3) Webpack/Vite automatically creates separate chunks. The biggest win is route-based splitting — users only download code for pages they visit. For a 2MB app, this can reduce initial load to 200KB.Core Web Vitals & React Profiling
Google's Core Web Vitals directly affect SEO rankings and user experience. Understanding them and how React affects each metric is now a standard senior-level question in India.
| Metric | What it measures | Good threshold | React impact |
|---|---|---|---|
| LCP | Largest Contentful Paint — when main content loads | < 2.5s | SSR, lazy images, code splitting, preloading |
| INP | Interaction to Next Paint — how fast UI responds to clicks | < 200ms | Avoid long tasks, use startTransition, defer non-critical work |
| CLS | Cumulative Layout Shift — elements moving unexpectedly | < 0.1 | Set image dimensions, avoid injecting content above fold |
| FCP | First Contentful Paint — any content visible | < 1.8s | SSR, skeleton screens, reduce blocking JS |
| TTFB | Time To First Byte — server response speed | < 800ms | SSR/SSG, CDN, caching |
// ─── React DevTools Profiler (in code) ──────────────────── import { Profiler } from 'react'; function onRenderCallback(id, phase, actualDuration) { console.log(`${id} (${phase}): ${actualDuration.toFixed(2)}ms`); } // Wrap expensive components to measure render time: <Profiler id="ProductList" onRender={onRenderCallback}> <ProductList /> </Profiler> // ─── Web Vitals measurement ──────────────────────────────── import { onLCP, onINP, onCLS, onFCP } from 'web-vitals'; onLCP(metric => sendToAnalytics(metric)); onINP(metric => sendToAnalytics(metric)); onCLS(metric => sendToAnalytics(metric)); // ─── startTransition — mark non-urgent updates ──────────── import { startTransition } from 'react'; const handleSearch = (query) => { setSearchInput(query); // urgent — update input immediately startTransition(() => { setFilteredResults(filter(query)); // non-urgent — can be interrupted }); // typing stays responsive! };
SSR, SSG, CSR & Hydration
Rendering strategy is a major architect-level question. SSR, SSG, CSR — knowing trade-offs and how React handles hydration is essential for senior roles at product companies.
suppressHydrationWarning for intentional mismatches, or useEffect for client-only code.React 18 & Concurrent Mode
React 18 is the current version with major new features. Understanding Automatic Batching, startTransition, Suspense for data fetching, and React Server Components is now expected in senior Indian interviews.
import { startTransition, useDeferredValue, useTransition, useId } from 'react'; // ─── useTransition — shows pending state ────────────────── function SearchPage() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition(); const handleSearch = (q) => { setQuery(q); // urgent: update input startTransition(() => { setResults(heavyFilter(q)); // non-urgent: filter results }); }; return ( <> <input value={query} onChange={e => handleSearch(e.target.value)} /> {isPending ? <Spinner/> : <ResultList items={results} />} </> ); } // ─── createRoot — React 18 required ─────────────────────── import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render(<App />); // react-dom/client (NOT react-dom) enables concurrent features
Polyfills, Browser Compatibility & Web APIs
Polyfills provide modern functionality to older browsers. Understanding what needs polyfilling, how Babel targets work, and which browser APIs need fallbacks is a senior-level concept asked in companies building for broad user bases (like Jio, BSNL network users with older phones).
// ─── browserslist (.browserslistrc) ─────────────────────── // > 0.5% - browsers with >0.5% market share // last 2 versions - last 2 major versions // not dead - not discontinued // not IE 11 - exclude IE11 (most apps now) // ─── babel.config.js with automatic polyfilling ─────────── module.exports = { presets: [['@babel/preset-env', { useBuiltIns: 'usage', // auto-import only used polyfills corejs: 3, // core-js version targets: "> 0.5%, last 2 versions, not dead" }]] }; // ─── Manual polyfill pattern ────────────────────────────── if (!window.IntersectionObserver) { require('intersection-observer'); // load polyfill if needed } // ─── fetch polyfill for older browsers ─────────────────── import 'whatwg-fetch'; // polyfills window.fetch // ─── Feature detection (better than browser detection) ──── if ('IntersectionObserver' in window) { // use modern API } else { // fallback } // ─── Common APIs needing polyfills ──────────────────────── // Promise.allSettled → core-js // Array.flat/flatMap → core-js // Object.fromEntries → core-js // ResizeObserver → resize-observer-polyfill // URL API → core-js // AbortController → abortcontroller-polyfill
Testing in React
Testing knowledge is expected at senior levels. React Testing Library (RTL) is the industry standard — it tests behavior, not implementation. Understanding unit tests, integration tests, and mocking is commonly asked.
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from 'vitest'; // ─── Basic component test ───────────────────────────────── test('renders user name', () => { render(<UserCard name="Rahul" age={25} />); expect(screen.getByText('Rahul')).toBeInTheDocument(); expect(screen.getByText('Age: 25')).toBeInTheDocument(); }); // ─── User interaction test ──────────────────────────────── test('counter increments on click', async () => { const user = userEvent.setup(); render(<Counter />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: /increment/i })); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); // ─── Async test with API mock ───────────────────────────── vi.mock('../api', () => ({ fetchUser: vi.fn().mockResolvedValue({ name: 'Priya', email: 'p@co.in' }) })); test('displays fetched user', async () => { render(<UserProfile userId="1" />); expect(screen.getByText(/loading/i)).toBeInTheDocument(); await waitFor(() => expect(screen.getByText('Priya')).toBeInTheDocument()); }); // ─── Query priority (RTL guideline) ────────────────────── // 1. getByRole - accessibility-based (best) // 2. getByLabelText - form labels // 3. getByPlaceholder // 4. getByText - visible text // 5. getByTestId - last resort (not user-visible)
Complete Interview Question Bank
Every question that gets asked in Indian tech interviews — from 1-year experience startups to 5+ year FAANG rounds. Grouped by difficulty and company level.
You're ready when you can answer all of these without hesitation:
- Explain Virtual DOM, reconciliation, and the key prop from scratch
- Write a custom hook (useFetch, useDebounce, useLocalStorage) in a live coding round
- Explain exactly when a component re-renders and how to prevent unnecessary ones
- Describe code splitting strategy for a large e-commerce app
- Explain SSR hydration — what it is, why it matters, what causes errors
- Compare Context API, Redux Toolkit, and Zustand — when to use each
- Explain what startTransition does and when you'd use it
- Describe Core Web Vitals and how React choices affect each metric
- Build a protected route with React Router v6
- Explain React Server Components and how they differ from Client Components
- Describe what polyfills are and how Babel + browserslist automate them