React is used by 40.6% of all developers. Master all 50 interview questions - hooks, Virtual DOM, Server Components, and performance patterns - asked at top product companies in 2026.
React is the most-used frontend library for five consecutive years, relied on by 40.6% of professional developers. This guide covers all 50 interview questions junior and mid-level React developers face at product companies - fundamentals, hooks, patterns, performance, Router, and modern React 18/19 features - each with a concise answer and a runnable code snippet.
React is the most-used frontend library for five consecutive years - 40.6% of all professional developers rely on it daily (Stack Overflow Developer Survey 2025). That staying power converts directly into interview demand: React developer roles consistently rank among the highest-paying frontend positions, with average total compensation exceeding $120,000 in the US (Built In, 2026).
The challenge isn't finding React interview questions - it's finding a single resource that covers concept, code, and interview nuance in one place. This guide answers all 50 questions you're likely to face at product companies and startups, organized by topic, each with a clean code snippet and an interview tip where it counts.
Key Takeaways
React is used by 40.6% of developers - the #1 frontend library for five straight years (Stack Overflow 2025)
Hooks (useState, useEffect, useCallback, useMemo) appear in virtually every React interview - know them cold
React 18 introduced Concurrent Mode,
useTransition, anduseId; React 19 stabilized Server ComponentsThis guide covers all 6 interview buckets: fundamentals, hooks, patterns, performance, Router, and modern React
frontend interview prep roadmap → comprehensive guide to preparing for frontend developer interviews
React was created by Jordan Walke at Meta and open-sourced in 2013. It's used by 40.6% of professional developers today, making it the most popular frontend library for five years running (Stack Overflow, 2025). Fundamentals questions test whether you understand React's core model - not just its API.
React is a JavaScript library for building user interfaces - specifically, for building component trees that render to the DOM. It handles the view layer only. Unlike full frameworks such as Angular or Vue, React doesn't include a router, form library, or HTTP client out of the box. You compose those yourself from the ecosystem (React Router, React Hook Form, etc.).
// React's job: describe what the UI should look like
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// ReactDOM's job: put it on the actual page
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")).render(<Greeting name="Abhijeet" />);Interview tip: Interviewers often ask this to check if you understand why "React" and "React ecosystem" are different conversations.
JSX (JavaScript XML) is a syntax extension that lets you write HTML-like markup inside JavaScript. Browsers don't understand JSX - a build tool (Babel or the new React 17+ JSX transform) compiles it into React.createElement() calls before the browser ever sees it.
// What you write (JSX)
const element = <button className="btn">Click me</button>;
// What Babel compiles it to (React 17+ automatic runtime)
import { jsx as _jsx } from "react/jsx-runtime";
const element = _jsx("button", { className: "btn", children: "Click me" });Since React 17, you no longer need import React from 'react' at the top of every file - the JSX transform handles it automatically.
The Virtual DOM is a lightweight JavaScript object representation of the real DOM tree. When state changes, React creates a new virtual DOM tree, diffs it against the previous one (reconciliation), and applies only the minimal set of changes to the real DOM.
State change → New Virtual DOM → Diff (reconciliation) → Real DOM patch// React batches multiple setState calls and computes the minimum DOM ops
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>{count}</p>
{/* Only the <p> text node is updated in the real DOM - not the entire div */}
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}Interview tip: The Virtual DOM doesn't make React faster than direct DOM manipulation - it makes React fast enough while keeping code declarative and predictable.
Functional components are plain JavaScript functions that return JSX. They use hooks for state and side effects. Class components extend React.Component, use this.state, and manage lifecycle in methods like componentDidMount. Since React 16.8 (hooks), functional components can do everything class components can - and are the modern standard.
// Class component (legacy pattern)
class Welcome extends React.Component {
state = { count: 0 };
render() {
return <h1>Count: {this.state.count}</h1>;
}
}
// Functional component (modern standard)
function Welcome() {
const [count, setCount] = React.useState(0);
return <h1>Count: {count}</h1>;
}Interview tip: If asked why hooks were introduced, the answer is: to share stateful logic between components without class boilerplate or HOC nesting.
Props (properties) are read-only inputs passed from a parent component to a child. They're the primary mechanism for component communication in React. A child must never mutate its props - it should treat them as immutable.
// Parent passes props
function App() {
return <UserCard name="Abhijeet" role="Frontend Dev" isActive={true} />;
}
// Child receives and uses props
function UserCard({ name, role, isActive }) {
return (
<div>
<h2>{name}</h2>
<p>{role}</p>
<span>{isActive ? "Online" : "Offline"}</span>
</div>
);
}Props can be any JavaScript value: strings, numbers, booleans, arrays, objects, or functions (callbacks).
State is mutable data managed inside a component. When state changes, React re-renders that component and its children. Props are passed in from outside and are read-only. The key rule: props come from the parent, state lives inside the component.
function Toggle() {
const [isOn, setIsOn] = React.useState(false); // state - owned here
return (
<button onClick={() => setIsOn(prev => !prev)}>
{isOn ? "ON" : "OFF"}
</button>
);
}A common interview mistake is describing state as "private props" - state is actually data the component manages itself and can change over time. Props are external inputs that don't change unless the parent re-renders with new values.
Functional components with hooks map to lifecycle phases:
This is when a component is created and inserted into the DOM for the first time.
Logic: This is where you typically initialize state, set up subscriptions, or fetch initial data.
Hook Equivalent: useEffect(() => { ... }, [])
The empty dependency array [] ensures the code inside runs only once, immediately after the initial render.
Legacy Class Method: componentDidMount()
This occurs whenever a component's props or state change, causing a re-render to keep the UI in sync with the data.
Logic: You use this phase to perform actions in response to specific data changes (e.g., re-fetching a user profile when a userId prop changes).
Hook Equivalent: useEffect(() => { ... }, [dependency])
By adding variables to the dependency array, the effect runs on mount and every time those specific variables change.
Legacy Class Method: componentDidUpdate()
This is the final stage when a component is being removed from the DOM.
Logic: Crucial for "cleaning up" to prevent memory leaks. This includes clearing timers (setInterval), cancelling network requests, or removing event listeners.
Hook Equivalent: The Cleanup Function inside useEffect.
JavaScript
useEffect(() => {
const timer = setInterval(() => console.log('Tick'), 1000);
// Cleanup function
return () => clearInterval(timer);
}, []);
Legacy Class Method: componentWillUnmount()
function DataLoader({ id }) {
const [data, setData] = React.useState(null);
React.useEffect(() => {
// componentDidMount + componentDidUpdate (on id change)
let cancelled = false;
fetch(`/api/items/${id}`)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d); });
return () => { cancelled = true; }; // componentWillUnmount
}, [id]);
return <div>{data ? data.name : "Loading..."}</div>;
}Reconciliation is React's algorithm for computing the minimal DOM updates needed after a state change. React diffs the new Virtual DOM tree against the old one using two heuristics: (1) elements of different types produce different trees, and (2) the key prop tells React which list items are stable across renders.
// Without key - React re-renders all list items on any change
<ul>
{items.map(item => <li>{item.name}</li>)} // ⚠️ Missing key
</ul>
// With key - React reuses existing DOM nodes efficiently
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>Keys are stable, unique identifiers that tell React how to match items across renders during reconciliation. Without proper keys, React may reorder the wrong DOM nodes, causing subtle UI bugs (form input values in the wrong row, animations on the wrong element).
// Bad - index as key breaks when items are added/removed/reordered
{todos.map((todo, index) => <TodoItem key={index} todo={todo} />)}
// Good - stable unique id as key
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}Interview tip: Always use a stable data ID as key. Index is acceptable only for static, non-reorderable lists.
A controlled component stores form input value in React state - React is the source of truth. An uncontrolled component stores value in the DOM itself, accessed via a ref. Controlled components are easier to validate and synchronize; uncontrolled components are simpler for file inputs or when integrating with non-React code.
// Controlled - React owns the value
function ControlledInput() {
const [value, setValue] = React.useState("");
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
// Uncontrolled - DOM owns the value
function UncontrolledInput() {
const inputRef = React.useRef(null);
const handleSubmit = () => console.log(inputRef.current.value);
return <input ref={inputRef} defaultValue="" />;
}React Hooks were introduced in React 16.8 (February 2019) and have since become the standard way to write React. According to the React team, hooks eliminated the need for class components while making stateful logic shareable across components without render props or HOC nesting.
Hooks are functions that let you "hook into" React state and lifecycle from functional components. They were introduced to solve three problems with class components: difficult reuse of stateful logic, complex components becoming hard to understand, and confusing this binding.
// Before hooks: HOC nesting to share stateful logic (confusing)
<AuthProvider>
<ThemeProvider>
<DataProvider>
<MyComponent />
</DataProvider>
</ThemeProvider>
</AuthProvider>
// After hooks: compose logic directly in the component
function MyComponent() {
const { user } = useAuth();
const { theme } = useTheme();
const { data } = useData();
return <div style={theme}>{user.name}: {data.title}</div>;
}useState adds local state to a functional component. It returns a tuple: the current value and a setter function. React preserves state across re-renders and re-renders the component whenever the setter is called with a new value.
function Counter() {
const [count, setCount] = React.useState(0);
// Functional update - safe when new state depends on old state
const increment = () => setCount(prev => prev + 1);
// Direct update - fine when new state is independent
const reset = () => setCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={reset}>Reset</button>
</div>
);
}Interview tip: Always use the functional form setCount(prev => prev + 1) when the new state depends on the old value - especially in async callbacks.
useEffect runs a side effect after the component renders. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount from class components. The effect runs after every render by default, or conditionally based on its dependency array.
function WindowWidth() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
// Cleanup: remove listener when component unmounts
return () => window.removeEventListener("resize", handler);
}, []); // Empty array = run once on mount
return <p>Window width: {width}px</p>;
}The dependency array controls when the effect re-runs. No array = runs after every render. Empty array [] = runs once on mount. Array with values = runs when any listed value changes between renders.
React.useEffect(() => {
// Runs after every render
});
React.useEffect(() => {
// Runs once on mount (like componentDidMount)
}, []);
React.useEffect(() => {
// Runs on mount AND whenever userId changes
fetchUser(userId);
}, [userId]);Interview tip: ESLint's react-hooks/exhaustive-deps rule enforces correct dependencies. Don't suppress it - fix the underlying issue.
Return a cleanup function from useEffect. React calls it before the component unmounts and before re-running the effect on the next render cycle.
function Chat({ roomId }) {
React.useEffect(() => {
const socket = createSocket(roomId);
socket.connect();
return () => {
socket.disconnect(); // cleanup runs before roomId changes or unmount
};
}, [roomId]);
return <div>Chat room: {roomId}</div>;
}Common cleanup targets: event listeners, timers (clearTimeout/clearInterval), WebSocket connections, and AbortControllers for cancelled fetch requests.
useRef returns a mutable object { current: initialValue } that persists across renders without triggering re-renders when changed. Use it to: (1) access a DOM node directly, (2) store a mutable value that doesn't affect the UI, or (3) hold the previous value of a state variable.
function FocusInput() {
const inputRef = React.useRef(null);
const focusIt = () => inputRef.current.focus(); // direct DOM access
return (
<>
<input ref={inputRef} placeholder="Type here..." />
<button onClick={focusIt}>Focus</button>
</>
);
}
// Storing a mutable value (timer ID) without causing re-renders
function Timer() {
const timerRef = React.useRef(null);
const start = () => {
timerRef.current = setInterval(() => console.log("tick"), 1000);
};
const stop = () => clearInterval(timerRef.current);
return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}useMemo memoizes the return value of a function. It recomputes only when its dependencies change. Use it for expensive computations that would otherwise run on every render.
function ProductList({ products, filterText }) {
// Without useMemo: filters the entire array on every render
// With useMemo: only recomputes when products or filterText changes
const filtered = React.useMemo(
() => products.filter(p => p.name.toLowerCase().includes(filterText)),
[products, filterText]
);
return (
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}Interview tip: Don't over-use useMemo. It has overhead of its own (memory + comparison). Only apply it when you've profiled and identified an actual performance problem.
useCallback memoizes a function reference so it doesn't get recreated on every render. useMemo memoizes the return value of a function. The key use case for useCallback is preventing child components wrapped in React.memo from re-rendering unnecessarily when a callback prop is passed.
function Parent() {
const [count, setCount] = React.useState(0);
// Without useCallback: new function reference every render → Child re-renders
// With useCallback: same reference until deps change → Child stays memoized
const handleClick = React.useCallback(() => {
console.log("clicked");
}, []); // no deps = stable reference forever
return (
<>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>Re-render parent</button>
<MemoizedChild onClick={handleClick} />
</>
);
}
const MemoizedChild = React.memo(function Child({ onClick }) {
console.log("Child rendered"); // only logs when onClick reference changes
return <button onClick={onClick}>Click</button>;
});useContext reads the value of a React Context without needing to pass props through every intermediate component. It's the solution to prop drilling - when a prop is threaded through several layers of components just to reach a deeply nested one.
// Create context
const ThemeContext = React.createContext("light");
// Provide it high in the tree
function App() {
return (
<ThemeContext.Provider value="dark">
<Layout />
</ThemeContext.Provider>
);
}
// Consume it anywhere in the tree - no prop drilling needed
function Button() {
const theme = React.useContext(ThemeContext);
return <button className={`btn-${theme}`}>Click</button>;
}useReducer manages state through a reducer function - a pure function that takes current state and an action, returns next state. Use it when: state logic is complex, the next state depends on multiple sub-values, or actions have names that make debugging easier.
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT": return { ...state, count: state.count + state.step };
case "DECREMENT": return { ...state, count: state.count - state.step };
case "SET_STEP": return { ...state, step: action.payload };
default: throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count} (step: {state.step})</p>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: "SET_STEP", payload: +e.target.value })}
/>
</div>
);
}useLayoutEffect fires synchronously after DOM mutations but before the browser paints. useEffect fires asynchronously after the paint. Use useLayoutEffect when you need to measure DOM layout (element sizes, positions) and apply changes before the user sees the initial render - otherwise use useEffect.
function Tooltip({ targetRef, text }) {
const tooltipRef = React.useRef(null);
// useLayoutEffect: measure and position before browser paints (no flicker)
React.useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
tooltipRef.current.style.top = `${rect.bottom + 8}px`;
tooltipRef.current.style.left = `${rect.left}px`;
}, []);
return <div ref={tooltipRef} className="tooltip">{text}</div>;
}Custom hooks are functions starting with use that encapsulate and share stateful logic between components. They're the primary way to extract and reuse React logic without HOCs or render props.
// Custom hook: useFetch
function useFetch(url) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
.then(d => { setData(d); setLoading(false); })
.catch(e => { if (e.name !== "AbortError") { setError(e); setLoading(false); } });
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage - reusable across any component
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <h2>{data.name}</h2>;
}PERSONAL EXPERIENCE - Custom hooks are the most underutilized pattern by junior developers. Every time you write the same useEffect+useState pattern twice, it should become a custom hook.
useId generates a stable, unique ID that is consistent between server and client renders - solving the hydration mismatch problem that occurred when generating IDs with Math.random() or counters. Use it for accessibility attributes like htmlFor/id pairs.
function PasswordInput() {
const id = React.useId(); // e.g. ":r0:" - stable across SSR and hydration
return (
<div>
<label htmlFor={id}>Password</label>
<input id={id} type="password" />
</div>
);
}useTransition marks state updates as non-urgent, allowing React to keep the UI responsive by prioritizing urgent updates (like typing) over slow ones (like filtering a large list). It returns [isPending, startTransition].
function SearchPage() {
const [query, setQuery] = React.useState("");
const [results, setResults] = React.useState([]);
const [isPending, startTransition] = React.useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // urgent: update the input immediately
startTransition(() => {
setResults(expensiveFilter(value)); // non-urgent: can be deferred
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <p>Updating results...</p>}
<ResultsList items={results} />
</>
);
}There are two rules enforced by the eslint-plugin-react-hooks linter:
1. Only call hooks at the top level - Never inside loops, conditions, or nested functions. React relies on the call order being the same every render. 2. Only call hooks from React functions - Call them from functional components or other custom hooks, never from plain JS functions.
// ❌ Wrong - hook inside a condition (breaks call order)
function Bad({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = React.useState(null); // violates rule 1
}
}
// ✅ Correct - condition inside the hook/component body
function Good({ isLoggedIn }) {
const [user, setUser] = React.useState(null);
if (!isLoggedIn) return null; // condition after hooks
return <p>{user?.name}</p>;
}Understanding component design patterns separates candidates who can build features from those who can build scalable applications. These patterns appear frequently in system-design and architecture questions at senior-adjacent roles.
Prop drilling happens when a prop is passed through multiple intermediate components that don't use it themselves - just to get it to a deeply nested child. It creates tight coupling and makes refactoring painful.
Solutions: React Context for read-mostly global data, component composition (passing components as children), or a state management library like Zustand or Redux Toolkit.
// Prop drilling (fragile - every level must pass userId down)
<App userId={userId}>
<Layout userId={userId}>
<Sidebar userId={userId}>
<UserAvatar userId={userId} /> {/* only this component uses it */}
</Sidebar>
</Layout>
</App>
// Context (clean - UserAvatar reads it directly)
const UserContext = React.createContext(null);
<UserContext.Provider value={{ userId }}>
<Layout> {/* no prop needed */}
<Sidebar> {/* no prop needed */}
<UserAvatar /> {/* reads from context */}
</Sidebar>
</Layout>
</UserContext.Provider>A Higher-Order Component is a function that takes a component and returns a new component with added behaviour. It's a pattern for cross-cutting concerns like authentication guards, analytics tracking, or data fetching before hooks existed.
// HOC: adds auth guard to any component
function withAuth(WrappedComponent) {
return function AuthGuard(props) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) return <Redirect to="/login" />;
return <WrappedComponent {...props} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);Interview tip: HOCs are still valid (especially in libraries), but custom hooks have replaced them for most new code. Be ready to discuss both.
Render props is a pattern where a component accepts a function as a prop and calls it to render its children, sharing state or behaviour without hard-coding the UI.
// MouseTracker shares position via render prop
function MouseTracker({ render }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
return (
<div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
{render(pos)} {/* caller decides what to render with the position */}
</div>
);
}
// Usage
<MouseTracker
render={({ x, y }) => <p>Mouse at {x}, {y}</p>}
/>Like HOCs, custom hooks have largely replaced render props for new code - but knowing the pattern is expected in interviews.
React strongly favors composition over inheritance. Composition means building complex UIs by combining smaller components - often via children or specialized props - rather than extending base classes. React's children prop is the simplest form of composition.
// Composition via children - Card is reusable with any content
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
function App() {
return (
<Card title="User Profile">
<Avatar src="/user.png" />
<p>Abhijeet Kushwaha - Frontend Dev</p>
</Card>
);
}The React docs explicitly state: "We haven't found any use cases where we'd recommend creating component inheritance hierarchies."
React.memo is a higher-order component that memoizes a functional component's render output. React skips re-rendering a memoized component if its props haven't changed (shallow comparison). Pair it with useCallback on parent callbacks to get the full benefit.
// Without React.memo: re-renders every time Parent re-renders
function ExpensiveChild({ data }) {
console.log("Child rendered");
return <ul>{data.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
}
// With React.memo: skips render if `data` reference hasn't changed
const ExpensiveChild = React.memo(function({ data }) {
console.log("Child rendered");
return <ul>{data.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
});
// Custom comparison (optional)
const OptimizedChild = React.memo(ExpensiveChild, (prevProps, nextProps) => {
return prevProps.data.length === nextProps.data.length; // custom equality
});Error Boundaries are class components that catch JavaScript errors in their child tree during rendering, in lifecycle methods, and in constructors of the whole tree below them. They render a fallback UI instead of crashing the entire app. Functional components can't be error boundaries (yet) - you need a class component or a library like react-error-boundary.
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
// Log to error tracking service (Sentry, etc.)
console.error("Boundary caught:", error, info.componentStack);
}
render() {
if (this.state.hasError) {
return <h2>Something went wrong. <button onClick={() => this.setState({ hasError: false })}>Retry</button></h2>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<FeatureThatMightCrash />
</ErrorBoundary>The Context API provides a way to share data across the component tree without prop drilling. It has three parts: Lifting state up means moving state to the nearest common ancestor of components that need to share it. The parent owns the state and passes it down as props, ensuring a single source of truth. Presentational components (also called "dumb" components) deal only with how things look. They receive all data via props and don't manage state or fetch data. Container components (or "smart" components) handle data fetching, state management, and pass data down to presentational components. This pattern improves testability - you can test React 18 ships with automatic batching, concurrent rendering, and Suspense for data - dramatically improving performance without any code changes. Still, knowing how to measure and fix bottlenecks is a core skill interviewers probe. Code splitting breaks your JS bundle into smaller chunks downloaded on demand. Without it, the entire app ships in one JS file - users wait for code they may never use. React supports code splitting via Interview tip: Route-based splitting is the highest-ROI form. Component-level splitting (for modals, charts, heavy editors) is the next step. Three techniques: (1) always provide stable Common causes: parent re-renders (child re-renders by default), unstable function references passed as props, and Context value changing on every render. The React DevTools Profiler records component render times and lets you identify which components render, how often, and why. It shows flame charts, ranked charts, and the reason for each render. The Virtualization (or windowing) renders only the rows visible in the viewport, not the entire list. For a 10,000-item list, it might render 20 DOM nodes regardless of list length. Use it when lists exceed ~200 items and you notice scroll jank or long initial renders. Libraries: React Router v6 (released 2021, now at v6.x) was a significant rewrite that introduced React Router v6 replaced Nest Use the React 19 (December 2024) stabilized Server Components and introduced the React Server Components render on the server at request time (or build time) and send HTML + a serialized component description to the client. They have zero JS bundle impact, can directly access databases and secrets, but can't use state, effects, or browser APIs. Client Components still handle interactivity. Server Components don't replace client components - they complement them. The mental model is: push data fetching and static rendering to the server, keep interactivity on the client. This split is what Next.js App Router is built around. Concurrent Mode is React 18's rendering model that allows React to interrupt, pause, and resume rendering work. Instead of blocking the main thread for a long render, React can pause mid-render to handle a higher-priority update (like a user keystroke), then resume. It's enabled by default when you use Concurrent features built on top of this: Both mark updates as non-urgent (concurrent transitions), but they serve different purposes: - React 18 is the primary focus - it introduced Concurrent Mode, automatic batching, React 18 vs React 19 differences → detailed comparison of React 18 and 19 features Context API is fine for low-frequency updates (auth, theme, locale). Redux (or Zustand) is better when: multiple components update state frequently, you need DevTools time-travel debugging, or state mutations require strict action-based tracing. According to the 2025 State of JS survey, Zustand has overtaken Redux in developer satisfaction for new projects. Yes, but only in context. Interviewers at companies with legacy codebases ask about lifecycle methods and React 18 introduced automatic batching: multiple state updates in any async context (setTimeout, Promises, native event handlers) are batched into a single re-render. In React 17, only updates inside React event handlers were batched. To opt out, use React 18 automatic batching deep dive → detailed guide on React 18 batching behavior React interview questions in 2026 span six layers: fundamentals, hooks, architecture patterns, performance, routing, and modern concurrent features. Hooks dominate - The questions that separate strong candidates aren't recall questions - they're the ones that combine concepts: "why does React.memo not help if you pass a new callback prop?" or "how would you share state between sibling components without prop drilling?". Work through the code examples in this guide until you can write them from memory, and you'll be ready. Key Takeaways React fundamentals (Virtual DOM, reconciliation, controlled components) are table stakes - know them cold Hooks are the heart of modern React - master the rules, the dependency array, and custom hooks Performance: React 18's JavaScript interview questions 2026 → complete guide to JavaScript fundamentals asked in interviews Author: Abhijeet Kushwaha | Last updated: April 2026React.Fragment lets you group multiple elements without adding an extra DOM node. It's useful when a component must return multiple elements but a wrapper would break the layout (e.g., inside a or flexbox).
// With wrapper div - adds unnecessary DOM node, can break CSS
function Bad() {
return (
<div>
<dt>Term</dt>
<dd>Definition</dd>
</div>
);
}
// With Fragment - no extra DOM node
function Good() {
return (
<React.Fragment>
<dt>Term</dt>
<dd>Definition</dd>
</React.Fragment>
);
}
// Shorthand syntax (most common in practice)
function Better() {
return (
<>
<dt>Term</dt>
<dd>Definition</dd>
</>
);
}Q33. What is the Context API?
React.createContext() (creates the context), Context.Provider (supplies the value), and useContext() (consumes the value).// auth-context.js
const AuthContext = React.createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = React.useState(null);
const login = (userData) => setUser(userData);
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = React.useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
return ctx;
}
// Any component in the tree
function NavBar() {
const { user, logout } = useAuth();
return <nav>{user ? <button onClick={logout}>Logout</button> : "Guest"}</nav>;
}Q34. What is lifting state up?
// Both inputs need to share the same temperature value
function TemperatureConverter() {
const [celsius, setCelsius] = React.useState("");
const toCelsius = (f) => ((+f - 32) * 5) / 9;
const toFahrenheit = (c) => (+c * 9) / 5 + 32;
return (
<div>
<TemperatureInput
scale="Celsius"
value={celsius}
onChange={setCelsius}
/>
<TemperatureInput
scale="Fahrenheit"
value={celsius !== "" ? toFahrenheit(celsius).toFixed(1) : ""}
onChange={f => setCelsius(toCelsius(f).toFixed(1))}
/>
</div>
);
}Q35. What is the difference between presentational and container components?
// Presentational - pure UI, no side effects
function UserCard({ name, avatar, role }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{role}</p>
</div>
);
}
// Container - manages data, renders the presentational component
function UserCardContainer({ userId }) {
const { data, loading } = useFetch(`/api/users/${userId}`);
if (loading) return <Skeleton />;
return <UserCard name={data.name} avatar={data.avatar} role={data.role} />;
}UserCard with static props, and test UserCardContainer for correct data fetching.Section 4 - Performance Optimization (Q36–Q42)
Q36. How does `React.lazy` and `Suspense` work?
React.lazy lets you dynamically import a component - deferring its bundle download until it's first needed. Suspense provides a fallback UI while the lazy component loads. Together they implement code splitting at the component level.import React, { Suspense, lazy } from "react";
// Component is loaded only when this route is first visited
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
function App() {
return (
<Suspense fallback={<div>Loading page...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}Q37. What is code splitting in React?
React.lazy + dynamic import(), and build tools (Webpack, Vite) handle the chunking.// Dynamic import - Webpack / Vite automatically create a separate chunk
const HeavyChart = lazy(() => import("./components/HeavyChart"));
// Route-based splitting: most impactful strategy
// Each page route loads its own chunk only when visited
const routes = [
{ path: "/", component: lazy(() => import("./pages/Home")) },
{ path: "/analytics", component: lazy(() => import("./pages/Analytics")) },
];Q38. How do you optimize list rendering in React?
key props, (2) wrap list items in React.memo to prevent unnecessary re-renders, (3) use virtualization for large lists to render only visible rows.import { FixedSizeList as List } from "react-window";
// react-window only renders visible rows - handles 100,000 item lists smoothly
function VirtualList({ items }) {
const Row = React.memo(({ index, style }) => (
<div style={style}>{items[index].name}</div>
));
return (
<List height={600} itemCount={items.length} itemSize={50} width="100%">
{Row}
</List>
);
}Q39. What causes unnecessary re-renders and how do you prevent them?
// Problem: new object reference every render → MemoizedChild always re-renders
function Parent() {
const config = { theme: "dark" }; // new object every render!
return <MemoizedChild config={config} />;
}
// Fix: useMemo stabilizes the reference
function Parent() {
const config = React.useMemo(() => ({ theme: "dark" }), []);
return <MemoizedChild config={config} />;
}
// Context problem: new value object every render → all consumers re-render
<ThemeContext.Provider value={{ theme, setTheme }}> {/* new obj! */}
// Fix: memoize the context value
const value = React.useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>Q40. What is the React Profiler?
// Profiler API (programmatic, for CI or logging)
import { Profiler } from "react";
function onRenderCallback(id, phase, actualDuration, baseDuration) {
// id: component tree name
// phase: "mount" | "update"
// actualDuration: time spent rendering this update
// baseDuration: estimated time without memoization
if (actualDuration > 16) { // flag renders > 1 frame (60fps)
console.warn(`Slow render in ${id}: ${actualDuration.toFixed(2)}ms`);
}
}
<Profiler id="Dashboard" onRender={onRenderCallback}>
<Dashboard />
</Profiler>Q41. What is the role of the `key` prop in performance?
key prop is React's hint for reconciliation. A stable, unique key lets React reuse the existing DOM node when a list item's position changes. An unstable key (like Math.random() or array index in a reorderable list) forces React to unmount and remount the component - destroying local state and causing layout thrash.// ❌ Math.random() key - remounts the component every render
{items.map(item => <Row key={Math.random()} item={item} />)}
// ❌ Index key in sorted/filtered list - wrong DOM reuse on reorder
{items.map((item, i) => <Row key={i} item={item} />)}
// ✅ Stable unique ID - React reuses the DOM node correctly
{items.map(item => <Row key={item.id} item={item} />)}Q42. What is windowing/virtualization and when should you use it?
react-window (lightweight) or react-virtual (TanStack).import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualTable({ rows }) {
const parentRef = React.useRef(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 45,
});
return (
<div ref={parentRef} style={{ height: "500px", overflow: "auto" }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
{virtualizer.getVirtualItems().map(item => (
<div key={item.key} style={{ position: "absolute", top: item.start, height: item.size, width: "100%" }}>
{rows[item.index].name}
</div>
))}
</div>
</div>
);
}Section 5 - React Router (Q43–Q46)
, nested route layouts, and the useNavigate hook. It's the standard routing solution and appears in virtually every frontend interview.Q43. What is React Router v6? How does it differ from v5?
with (which always picks the best match), introduced relative paths, replaced useHistory with useNavigate, and added the component for nested layouts.// React Router v5 (legacy)
<Switch>
<Route exact path="/" component={Home} />
<Route path="/users/:id" component={UserDetail} />
</Switch>
// React Router v6 (current)
import { Routes, Route } from "react-router-dom";
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>Q44. How do you implement nested routes in React Router v6?
elements and use in the parent component where children should render.// Route config
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
// DashboardLayout.jsx - renders shared UI + child route at <Outlet>
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar /> {/* always visible */}
<main>
<Outlet /> {/* DashboardHome | Analytics | Settings renders here */}
</main>
</div>
);
}Q45. How do you perform programmatic navigation in React Router v6?
useNavigate hook (replaces useHistory from v5).import { useNavigate } from "react-router-dom";
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
await login(credentials);
navigate("/dashboard"); // basic navigation
navigate("/profile", { replace: true }); // replace history entry (no back button)
navigate(-1); // go back one step in history
}
return <form onSubmit={handleSubmit}>...</form>;
}Q46. What is the difference between `<Link>` and `<NavLink>`?
renders an anchor tag that navigates without a full page reload. does the same but also automatically applies an active class (and optional style) when its to path matches the current URL - making it ideal for navigation menus.import { Link, NavLink } from "react-router-dom";
function Nav() {
return (
<nav>
{/* Link - just navigates */}
<Link to="/">Home</Link>
{/* NavLink - adds active class when route matches */}
<NavLink to="/dashboard" className={({ isActive }) => isActive ? "nav-active" : ""}>
Dashboard
</NavLink>
{/* NavLink with inline style */}
<NavLink to="/settings" style={({ isActive }) => ({ fontWeight: isActive ? "bold" : "normal" })}>
Settings
</NavLink>
</nav>
);
}Section 6 - Modern React Patterns (Q47–Q50)
use hook, Actions, and new form handling APIs. These questions appear in interviews at companies already on Next.js 13+ or Remix.Q47. What are React Server Components (RSC)?
// app/page.jsx - Server Component (no "use client" directive)
// Runs on the server - can fetch data directly, no useEffect needed
async function ProductPage({ params }) {
const product = await db.products.findById(params.id); // direct DB access
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client Component for interactivity */}
<AddToCartButton productId={product.id} />
</div>
);
}
// components/AddToCartButton.jsx - Client Component
"use client";
function AddToCartButton({ productId }) {
const [added, setAdded] = React.useState(false); // state OK here
return <button onClick={() => setAdded(true)}>{added ? "Added!" : "Add to Cart"}</button>;
}Q48. What is Suspense in React and how does it work with data fetching?
catches "pending" signals from its children and shows a fallback until the data (or lazy component) is ready. React 18 extended Suspense to work with data fetching when using Suspense-compatible data sources (React Query, Relay, Next.js App Router, or the use() hook in React 19).// React 19 - use() hook unwraps a promise and integrates with Suspense
import { use, Suspense } from "react";
function UserProfile({ userPromise }) {
const user = use(userPromise); // suspends until resolved
return <h2>{user.name}</h2>;
}
function App() {
const userPromise = fetch("/api/user").then(r => r.json());
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}Q49. What is Concurrent Mode in React 18?
createRoot.// React 17 - legacy render (synchronous, blocking)
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
// React 18 - concurrent root (non-blocking, opt-in to all concurrent features)
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")).render(<App />);useTransition, useDeferredValue, Suspense for data, automatic batching, and Selective Hydration.Q50. What is the difference between `startTransition` and `useTransition`?
startTransition - a static function import. Use when you don't need the isPending flag. - useTransition - a hook. Returns [isPending, startTransition]. Use when you need to show a loading indicator during the transition.import { startTransition } from "react";
// startTransition: simpler, no pending state
function SearchBox({ onSearch }) {
return (
<input onChange={e => {
startTransition(() => onSearch(e.target.value));
}} />
);
}
// useTransition: with pending state for loading indicator
function SearchBoxWithIndicator({ onSearch }) {
const [isPending, startTransition] = React.useTransition();
return (
<>
<input onChange={e => startTransition(() => onSearch(e.target.value))} />
{isPending && <span className="spinner" />}
</>
);
}Frequently Asked Questions
What React version should I focus on for 2026 interviews?
useTransition, useId, and the startTransition API, all of which appear in interviews. React 19 (stable, December 2024) introduced Server Components and the use() hook; expect questions on RSC if you're interviewing at Next.js or Remix shops.How do I answer "when would you use Redux vs Context API"?
Are class components still asked in interviews in 2026?
this binding. The consensus in new projects is functional components + hooks. Know class components conceptually (especially Error Boundaries), but lead answers with functional equivalents.What is the difference between `useEffect` and `useLayoutEffect`?
useEffect runs after the browser paints - fire-and-forget for data fetching, subscriptions, and logging. useLayoutEffect runs before the browser paints - use it for DOM measurements and position corrections that must be applied before the user sees the first frame. Defaulting to useEffect is almost always correct.How does React handle batching in React 18?
ReactDOM.flushSync().Conclusion
useState, useEffect, useCallback, useMemo, and useContext appear in virtually every interview, with useTransition and custom hooks increasingly common at senior levels.React.memo + useCallback + useMemo only help together - profile before you optimizeuseTransition and RSC in React 19 are the differentiating questions at senior-adjacent levelsMore from React & Frontend