React Interview Questions and Answers: 60 Questions from Easy to Hard
This page is for Indian job seekers preparing for React developer interviews, whether you are a fresher looking for your first front-end role or an experienced engineer aiming for a senior position. It covers 60 questions in three levels: easy questions on JSX, props, state and basic hooks, medium questions on effects, memoisation, context, forms and testing, and hard questions on rendering internals, concurrent features, React 19 and architecture. Every answer uses modern function components and hooks. Read each answer, run the code in a small Vite project, then practise explaining it aloud.
What React interviews cover
- Fundamentals: JSX, components, props versus state, lists and keys, conditional rendering, events and controlled forms, usually asked as short questions or small coding tasks.
- Hooks in depth: useState updates and batching, useEffect dependencies and cleanup, useRef, useMemo, useCallback, useReducer, context and writing your own custom hooks.
- Rendering behaviour: what triggers a re-render, how reconciliation and keys work, React.memo, and finding and fixing performance problems with the Profiler.
- Modern React: React 18 concurrent features such as transitions and Suspense, React 19 actions, the use API, ref as a prop, and Server Components basics.
- Practical skills: fetching data safely, state management choices, testing with React Testing Library, accessibility, and a live machine-coding round.
How freshers should prepare
- Make sure your JavaScript basics are solid first, especially closures, array methods, destructuring, spread and promises, because React questions build on all of them.
- Create a Vite project and rebuild every example on this page, then change it, break it and fix it until you can explain what each hook does.
- Build two small projects end to end, such as a job search page with filters and a form with validation, and host them so you can share links.
- Practise explaining useEffect clearly: when it runs, what the dependency array does and why cleanup matters, since nearly every fresher interview asks about it.
- Prepare for a 45-minute coding task like a counter, a to-do list, a search with debounce or pagination, and talk through your choices while typing.
How experienced candidates should prepare
- Be ready to discuss real performance work: what you measured, which components re-rendered and why, and how memo, splitting state or virtualisation changed the numbers.
- Explain architecture decisions from your projects, such as where state lives, why you chose TanStack Query, Redux Toolkit or Zustand, and how you organised folders.
- Revise React 18 and 19 changes in depth: automatic batching, transitions, Suspense, actions, useActionState, use, ref as a prop, and Server Components with Next.js.
- Practise senior machine-coding rounds, such as an autocomplete with keyboard support, an infinite feed or a reusable modal, with tests and accessibility included.
- Prepare to review code: spot stale closures, missing cleanups, index keys, mutated state and unnecessary effects, and explain the fix and the trade-off.
Common mistakes to avoid
- Describing class components and lifecycle methods as the main way to write React; answer with function components and hooks, and mention classes only for legacy code.
- Using useEffect for everything, including derived values and user actions; explain that effects are for syncing with external systems and handlers handle events.
- Claiming the virtual DOM makes React fast by itself, or that setState updates the value immediately; both answers show shallow understanding.
- Wrapping every value in useMemo and useCallback without measuring, or not knowing why memo fails when a new object or function is passed as a prop.
- Ignoring accessibility and testing in coding rounds; using proper labels, roles and a quick React Testing Library test makes a strong impression.
React interview questions: Easy questions (freshers)
1. What is React, and why do teams use it?
React is a JavaScript library for building user interfaces out of components. You describe what the UI should look like for the current data, and React works out how to update the DOM when that data changes. Teams use it because components are reusable and easy to reason about, data flows one way from parent to child, the ecosystem is huge, and the same skills carry over to React Native for mobile apps. It is a library, not a full framework, so routing and data fetching come from tools like React Router or Next.js.
2. What is JSX?
JSX is a syntax extension that lets you write HTML-like markup inside JavaScript. It is not understood by browsers; a compiler such as Babel or esbuild turns each tag into a function call that creates a React element. Inside curly braces you can put any JavaScript expression. Differences from HTML include className instead of class, htmlFor instead of for, camelCase event names like onClick, and every tag must be closed. A component must return a single root, so use a fragment when you need siblings.
export default function Greeting({ name }) {
const isMorning = new Date().getHours() < 12;
return (
<>
<h1 className="title">Hello, {name}</h1>
<p>{isMorning ? "Good morning" : "Good day"}</p>
</>
);
} 3. What is the difference between props and state?
Props are inputs passed from a parent to a child component, like function arguments. The child must treat them as read-only. State is data a component owns and manages itself with hooks such as useState, and changing it with the setter makes React re-render that component. If a child needs to change data that the parent owns, the parent passes down a callback as a prop. A good rule: if a value can be computed from props or other state, it should not be stored as state at all.
4. How does useState work?
useState adds a state variable to a function component. It returns the current value and a setter function. Calling the setter schedules a re-render; the variable in the current render does not change immediately, so reading it right after setting still gives the old value. When the new value depends on the old one, pass an updater function such as setCount(c => c + 1), which always receives the latest value. For an expensive initial value, pass a function so it runs only on the first render.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Clicked {count} times
</button>
);
} 5. Why does React need keys when rendering a list?
Keys tell React which item is which between renders. With stable keys, React can match each element to its previous version, so it moves, adds or removes only what changed and keeps each item's state, such as text typed in an input, attached to the right item. Use a unique, stable id from your data. Array indexes are only safe for static lists that never reorder or change, and random values such as Math.random() break matching completely because they change every render. Keys must be unique among siblings, not across the whole app.
export default function TaskList({ tasks }) {
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
} 6. What is useEffect used for?
useEffect synchronises a component with something outside React, such as a timer, a WebSocket, a browser API or a manual DOM library. It runs after React has updated the screen. The dependency array controls when it runs again: after every render with no array, once after mount with an empty array, or whenever a listed value changes. If the effect returns a function, React calls it as cleanup before the next run and on unmount, which is where you clear timers and remove listeners.
import { useEffect, useState } from "react";
export default function Clock() {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id); // cleanup on unmount
}, []);
return <p>{now.toLocaleTimeString("en-IN")}</p>;
} 7. What is the difference between controlled and uncontrolled components?
In a controlled input, React state is the single source of truth: the value prop comes from state and onChange updates it, so you can validate or format on every keystroke. In an uncontrolled input, the DOM keeps the value and you read it when needed with a ref or from FormData on submit, giving the starting value with defaultValue. Controlled inputs suit live validation and dependent fields; uncontrolled ones are simpler and cheaper for large forms. Do not switch one input between the two modes.
import { useRef, useState } from "react";
export function ControlledEmail() {
const [email, setEmail] = useState("");
return <input value={email} onChange={(e) => setEmail(e.target.value.trim())} />;
}
export function UncontrolledEmail() {
const ref = useRef(null);
return <input ref={ref} defaultValue="" onBlur={() => console.log(ref.current.value)} />;
} 8. How do you render something conditionally in React?
Use plain JavaScript inside JSX: a ternary for either/or, && to show something only when a condition is true, or an early return before the main JSX. Returning null renders nothing. Watch the && trap with numbers: {count && <Badge />} renders the text 0 when count is 0, because 0 is falsy but is still a renderable value. Write {count > 0 && <Badge />} instead. For complex branches, move the logic into a variable or a small component to keep the JSX readable.
export default function Inbox({ count }) {
if (count == null) return null; // nothing to show yet
return (
<div>
{count > 0 && <span>{count} new messages</span>}
{count === 0 ? <p>All caught up</p> : null}
</div>
);
} 9. What is the virtual DOM, and how does React update the page?
When state changes, React calls your components again to get a new tree of React elements, which are plain JavaScript objects describing the UI. This in-memory tree is often called the virtual DOM. React compares it with the previous tree, a process called reconciliation, and then applies only the necessary changes to the real DOM. The benefit is not that the virtual DOM is magically fast, but that you write simple declarative code while React batches and minimises the expensive DOM work.
10. What are the rules of hooks?
There are two rules. First, call hooks only at the top level of a component or custom hook, never inside conditions, loops, nested functions or after an early return. Second, call them only from function components or custom hooks, not from regular functions or class components. React identifies each hook by the order in which it is called on every render, so changing that order mixes up their state. The eslint-plugin-react-hooks package catches violations, and its exhaustive-deps rule checks effect dependencies.
11. How do you handle events in React?
You pass a function to a camelCase prop such as onClick, onChange or onSubmit. Pass the function itself, not the result of calling it: onClick={handleClick} is correct, while onClick={handleClick()} runs during render. To pass arguments, wrap it in an arrow function. React gives the handler a synthetic event that works the same across browsers and has the usual preventDefault and stopPropagation methods. Handlers are the right place for side effects caused by a user action, rather than useEffect.
export default function SaveButton({ onSave }) {
function handleClick(e) {
e.preventDefault();
onSave("draft");
}
// Pass the function itself; onClick={handleClick()} would run during render
return <button onClick={handleClick}>Save draft</button>;
} 12. What does lifting state up mean?
When two sibling components need the same data, you move that state to their closest common parent and pass it down as props, along with a callback to change it. Then there is a single source of truth and the siblings stay in sync. In the example, the search box and the list both depend on the query, so the parent owns it. Lift state only as high as needed: putting everything at the top causes unnecessary re-renders and prop drilling.
import { useState } from "react";
function Filter({ value, onChange }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
function CityList({ items }) {
return <ul>{items.map((c) => <li key={c}>{c}</li>)}</ul>;
}
export default function CitySearch({ cities }) {
const [query, setQuery] = useState("");
const shown = cities.filter((c) => c.toLowerCase().includes(query.toLowerCase()));
return <><Filter value={query} onChange={setQuery} /><CityList items={shown} /></>;
} 13. What is prop drilling, and how can you avoid it?
Prop drilling is passing a prop through several layers of components that do not use it, only so that a deeply nested child can reach it. It makes components harder to change and move. The first fix to try is composition: pass ready-made JSX as children so the intermediate components never touch the data. For truly global values such as the logged-in user, theme or language, use Context. For complex shared app state, a store library such as Zustand or Redux Toolkit can help, but do not reach for it first.
14. What is useRef used for?
useRef returns an object with a current property that persists for the lifetime of the component. It has two main uses. First, holding a reference to a DOM node, for example to focus an input, measure size or scroll into view. Second, keeping a mutable value between renders, such as a timer id or the previous value of a prop. Unlike state, changing ref.current does not trigger a re-render, so do not use a ref for data that should appear on screen. Avoid reading or writing refs during render.
import { useRef } from "react";
export default function SearchBox() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} placeholder="Search jobs" />
<button onClick={() => inputRef.current.focus()}>Focus search</button>
</>
);
} 15. What is the children prop?
children holds whatever you put between a component's opening and closing tags: text, elements or other components. It lets you build wrapper components such as cards, modals and layouts that do not need to know what they contain. This kind of composition is React's main way to reuse layout and avoid prop drilling. You can also pass JSX through named props, for example header and footer, when a component has several slots.
function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
export default function Profile() {
return <Card title="Skills"><p>React, TypeScript</p></Card>;
} 16. Why should you not mutate state directly?
React decides whether state changed by comparing the old and new values with Object.is. If you push into an array or change an object property and then pass the same reference to the setter, React sees no change and may skip the re-render, and other code holding the old reference also sees unexpected changes. Always create a new array or object: spread, map, filter and the copying array methods such as toSorted work well. Immutable updates also make memo and useMemo comparisons reliable.
import { useState } from "react";
export default function Tags() {
const [tags, setTags] = useState(["react"]);
function add(tag) {
// Wrong: tags.push(tag); setTags(tags); -> same array, React may skip the render
setTags([...tags, tag]);
}
return <button onClick={() => add("hooks")}>{tags.join(", ")}</button>;
} 17. Should you write function components or class components today?
Write function components with hooks. They are what the official documentation teaches, they need less code, and hooks let you share stateful logic through custom hooks instead of patterns like higher-order components. Newer features such as Server Components and the React 19 hooks only work with functions. Class components are still supported, so you should be able to read them in older codebases, and an error boundary still has to be a class, or you can use a small library that wraps one.
18. What is StrictMode, and why do my effects run twice in development?
StrictMode is a development-only wrapper that helps find bugs early. Inside it, React renders components twice to expose impure rendering, and mounts each component, unmounts it and mounts it again, so every effect runs setup, cleanup and setup. If your effect breaks after this, for example a duplicated subscription or two open connections, it is missing a proper cleanup. None of this happens in a production build. Do not remove StrictMode to hide the double call; fix the cleanup instead.
import { StrictMode, useEffect } from "react";
function Chat({ roomId }) {
useEffect(() => {
console.log("connect", roomId);
return () => console.log("disconnect", roomId); // makes the double run harmless
}, [roomId]);
return <p>Room {roomId}</p>;
}
export default function App() {
return <StrictMode><Chat roomId="general" /></StrictMode>;
} 19. What are the common ways to style React components?
You can use plain CSS files with className, CSS Modules that scope class names to one component, utility-first CSS such as Tailwind, or CSS-in-JS libraries. Inline styles take an object with camelCase property names, and numbers become pixel values for most properties. Inline styles are fine for dynamic values like a progress width, but they cannot express hover states or media queries, so use classes for most styling. Choose one approach per project and stay consistent.
export default function Progress({ percent }) {
return (
<div className="progress">
<div
className="progress-bar"
style={{ width: `${percent}%`, backgroundColor: "#2e7d32", height: 8 }}
/>
</div>
);
} 20. How is a React app mounted into the page?
react is the core library for components and hooks, while react-dom connects them to the browser DOM. In React 18 and later you call createRoot from react-dom/client with a DOM container and then call render with your top-level component. createRoot enables concurrent features such as automatic batching and transitions. The old ReactDOM.render API was removed in React 19. For server-rendered HTML, you use hydrateRoot instead so React attaches to the existing markup rather than recreating it.
import { createRoot } from "react-dom/client";
import App from "./App";
const root = createRoot(document.getElementById("root"));
root.render(<App />); React interview questions: Medium questions (1-3 years)
21. How do you fetch data in useEffect without race conditions?
Put the fetch in an effect that depends on the input, such as city. If the city changes quickly, an older request can finish after a newer one and show the wrong data. To prevent that, keep an ignore flag in the effect and set it in the cleanup; React runs the cleanup before the next effect, so stale responses are dropped. AbortController achieves the same and also cancels the request. In larger apps, a data library like TanStack Query or a framework loader handles caching, retries and deduplication for you.
import { useEffect, useState } from "react";
export default function Jobs({ city }) {
const [jobs, setJobs] = useState([]);
useEffect(() => {
let ignore = false;
fetch(`/api/jobs?city=${encodeURIComponent(city)}`)
.then((res) => res.json())
.then((data) => { if (!ignore) setJobs(data); });
return () => { ignore = true; };
}, [city]);
return <ul>{jobs.map((j) => <li key={j.id}>{j.title}</li>)}</ul>;
} 22. What is a stale closure in useEffect, and how do you fix it?
Each render creates new functions that capture the props and state of that render. If an effect with an empty dependency array starts an interval that calls setSeconds(seconds + 1), the callback keeps seeing seconds as 0 forever, so the timer stops at 1. Fixes: use an updater function, setSeconds(s => s + 1), which always gets the latest value; or list the value in the dependency array so the effect restarts with fresh values. Do not silence the exhaustive-deps lint rule; it usually points at exactly this bug.
import { useEffect, useState } from "react";
export default function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Bug: setSeconds(seconds + 1) would always read 0 from the first render
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(id);
}, []);
return <p>{seconds}s</p>;
} 23. What is the difference between useMemo and useCallback, and when should you use them?
useMemo caches the result of a calculation between renders and recomputes only when its dependencies change. useCallback caches a function definition itself; it is the same as useMemo returning a function. Use them for expensive calculations, for values passed to a memo-wrapped child so its props stay equal, and for values used as effect dependencies. They are not free, since React still compares dependencies, so do not wrap everything. Measure first. If your project uses the React Compiler, it adds most of this memoisation automatically.
import { memo, useCallback, useMemo, useState } from "react";
const Row = memo(function Row({ job, onApply }) {
return <li>{job.title} <button onClick={() => onApply(job.id)}>Apply</button></li>;
});
export default function JobList({ jobs }) {
const [query, setQuery] = useState("");
const visible = useMemo(() => jobs.filter((j) => j.title.includes(query)), [jobs, query]);
const onApply = useCallback((id) => console.log("apply", id), []);
return (<><input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{visible.map((j) => <Row key={j.id} job={j} onApply={onApply} />)}</ul></>);
} 24. When does React.memo help, and when is it useless?
memo skips re-rendering a component when its props are shallowly equal to the previous props. It helps when a component is expensive to render and its parent re-renders often with the same props. It is useless if you pass a new object, array or inline function on every render, because shallow comparison sees a new reference each time. In the example, user is recreated on every click, so Avatar re-renders anyway. Fix it by moving the constant outside the component or wrapping it in useMemo. memo also does not stop re-renders caused by the component's own state or context.
import { memo, useState } from "react";
const Avatar = memo(function Avatar({ user }) {
console.log("Avatar render");
return <img src={user.photo} alt={user.name} />;
});
export default function Header() {
const [count, setCount] = useState(0);
const user = { name: "Dev", photo: "/dev.png" }; // new object each render: memo can't help
return <><Avatar user={user} /><button onClick={() => setCount(count + 1)}>{count}</button></>;
} 25. How do you use the Context API?
Create a context with createContext and a default value, wrap part of the tree in a provider with a value, and read it in any descendant with useContext. In React 19 you can render the context itself as the provider; in React 18 you write ThemeContext.Provider. Context is ideal for values many components need, such as theme, language or the current user. Every component that reads the context re-renders when the provider's value changes, so keep frequently changing data out of a widely used context.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function Toolbar() {
return <p>Theme: {useContext(ThemeContext)}</p>;
}
export default function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext value={theme}>
<Toolbar />
<button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>Toggle</button>
</ThemeContext>
);
} 26. What is a custom hook? Write one.
A custom hook is a function whose name starts with use and that calls other hooks. It lets you reuse stateful logic, such as debouncing, form handling or subscriptions, across components. Each component that calls the hook gets its own independent state; hooks share logic, not data. The use prefix tells React and the lint rules to apply the rules of hooks. This hook returns a value only after it has stopped changing for the given delay, which is useful before calling a search API.
import { useEffect, useState } from "react";
export function useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
// Usage: const q = useDebouncedValue(searchText); then fetch with q 27. When would you choose useReducer over useState?
Choose useReducer when state has several related fields, when the next state depends on the previous one in many ways, or when many event handlers update the same state. The reducer is a pure function that takes the current state and an action and returns the next state, which keeps all update logic in one place and is easy to unit test without rendering anything. useState is simpler for independent values like a toggle or an input. dispatch has a stable identity, so it is safe to pass down or use in effects.
import { useReducer } from "react";
function cartReducer(state, action) {
switch (action.type) {
case "add": return { ...state, items: [...state.items, action.item] };
case "clear": return { ...state, items: [] };
default: throw new Error("Unknown action: " + action.type);
}
}
export default function Cart() {
const [cart, dispatch] = useReducer(cartReducer, { items: [] });
return (<button onClick={() => dispatch({ type: "add", item: "Book" })}>
Items: {cart.items.length}</button>);
} 28. How can you use a key to reset a component's state?
React keeps a component's state as long as the same component type renders at the same position with the same key. If you change the key, React treats it as a different component: it unmounts the old one, throwing away its state, and mounts a fresh one. This is the clean way to reset a form when the selected record changes, for example clearing a message draft when you switch chat contacts, instead of writing an effect that resets every field. The same rule explains why index keys can attach state to the wrong list item.
import { useState } from "react";
function Draft({ to }) {
const [text, setText] = useState("");
return <textarea value={text} onChange={(e) => setText(e.target.value)} placeholder={"Message " + to} />;
}
export default function Chat({ contact }) {
// A new key makes React unmount the old Draft and mount a fresh one
return <Draft key={contact.id} to={contact.name} />;
} 29. If you call setCount(count + 1) three times in one click, what happens?
The count goes up by one, not three. count is a constant inside that render, say 0, so all three calls ask React to set the value to 1. React also batches state updates in an event handler into a single re-render; since React 18 this automatic batching applies inside promises, timeouts and native handlers too. If each update should build on the previous one, pass an updater function: setCount(c => c + 1) three times queues three updates and the result is 3.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
function addThreeWrong() {
setCount(count + 1); setCount(count + 1); setCount(count + 1); // ends at 1
}
function addThree() {
setCount((c) => c + 1); setCount((c) => c + 1); setCount((c) => c + 1); // ends at 3
}
return (<><p>{count}</p><button onClick={addThreeWrong}>Wrong</button>
<button onClick={addThree}>Right</button></>);
} 30. How do you build a form with several fields and validation?
Keep the field values in one state object and use a single change handler that reads the input's name attribute, so adding a field does not need a new handler. On submit, call preventDefault, validate, and either show errors or send the data. Show errors next to the field and mark them with role="alert" or aria-describedby so screen readers announce them. Always validate again on the server. For large forms, libraries like React Hook Form with a schema validator such as Zod reduce re-renders and boilerplate.
import { useState } from "react";
export default function SignupForm() {
const [form, setForm] = useState({ name: "", phone: "" });
const [error, setError] = useState("");
const update = (e) => setForm({ ...form, [e.target.name]: e.target.value });
function submit(e) {
e.preventDefault();
if (!/^[6-9]\d{9}$/.test(form.phone)) return setError("Enter a valid 10-digit mobile number");
setError(""); console.log("submit", form); // send to the API here
}
return (<form onSubmit={submit}><input name="name" value={form.name} onChange={update} />
<input name="phone" value={form.phone} onChange={update} />{error && <p role="alert">{error}</p>}
<button>Sign up</button></form>);
} 31. What is an error boundary, and what errors does it not catch?
An error boundary is a component that catches errors thrown while rendering its children, shows a fallback UI instead of a blank screen, and can log the error. It must be a class component with static getDerivedStateFromError, and optionally componentDidCatch for logging. It does not catch errors in event handlers, in asynchronous code such as setTimeout or a fetch callback, in server-side rendering, or in the boundary itself. Handle those with try/catch or by storing the error in state. Place boundaries around independent sections so one broken widget does not take down the page.
import { Component } from "react";
export class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error("Logged:", error.message, info.componentStack);
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
} 32. How do you code-split a React app?
Use React.lazy with a dynamic import so a component's code is downloaded only when it is first rendered, and wrap it in Suspense to show a fallback while it loads. The imported module must have a default export. Split at the route level first, then at heavy features such as charts, editors or admin panels, which keeps the first load small on slower mobile networks. Declare lazy components at the top level of a module, never inside another component, or their state resets on every render.
import { lazy, Suspense } from "react";
const Reports = lazy(() => import("./Reports"));
export default function Dashboard() {
return (
<Suspense fallback={<p>Loading reports...</p>}>
<Reports />
</Suspense>
);
} 33. What is the difference between useEffect and useLayoutEffect?
useEffect runs after the browser has painted, so it does not block what the user sees, and it is the right default. useLayoutEffect runs after React has updated the DOM but before the browser paints, and React processes any state update inside it before painting. Use it only when you must measure layout and adjust immediately, such as positioning a tooltip, otherwise the user sees a flicker. Because it blocks painting, heavy work inside it makes the app feel slow.
import { useLayoutEffect, useRef, useState } from "react";
export default function Tooltip({ text }) {
const ref = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
setHeight(ref.current.getBoundingClientRect().height); // measured before paint
}, [text]);
return <div ref={ref} style={{ marginTop: -height }}>{text}</div>;
} 34. How do you test a React component with React Testing Library?
Render the component, find elements the way a user would, by role, label or visible text, interact with user-event, and assert on what is shown on screen. Avoid testing internal state or implementation details, so tests survive refactoring. Prefer getByRole because it also checks accessibility. userEvent.setup() simulates real typing and clicking more faithfully than fireEvent. This test runs under Jest or Vitest with a jsdom environment; with the jest-dom matchers installed you could write toHaveTextContent instead.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { test, expect } from "vitest";
import Counter from "./Counter";
test("increments on click", async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole("button", { name: "Clicked 0 times" }));
expect(screen.getByRole("button").textContent).toBe("Clicked 1 times");
}); 35. What is derived state, and why should you avoid syncing it with useEffect?
Derived state is any value you can compute from existing props or state, such as a total, a filtered list or a full name. Calculate it directly during render instead of storing it in separate state and updating it in an effect. The effect approach causes an extra render with stale values first, and the two pieces of state can drift out of sync. If the calculation is expensive, wrap it in useMemo. Keep only the minimal source data in state.
import { useState } from "react";
export default function Cart({ items }) {
const [coupon, setCoupon] = useState("");
// Avoid: a total state plus useEffect(() => setTotal(...), [items])
const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const total = coupon === "SAVE10" ? subtotal * 0.9 : subtotal;
return (<><input value={coupon} onChange={(e) => setCoupon(e.target.value)} />
<p>Total: Rs {total.toFixed(2)}</p></>);
} 36. How do you pass a ref to a child component in React 19, and how was it done before?
In React 19, ref is an ordinary prop for function components: the child accepts ref in its props and attaches it to the DOM element it wants to expose. In React 18 and earlier, function components could not receive ref as a prop, so you wrapped the child in forwardRef, which passed the ref as a second argument. forwardRef still works in React 19 but is no longer needed and is planned for deprecation. Expose a ref only when the parent truly needs DOM access, such as focusing an input.
import { useRef } from "react";
// React 19: ref arrives as a normal prop
function TextField({ label, ref, ...rest }) {
return <label>{label} <input ref={ref} {...rest} /></label>;
}
export default function LoginForm() {
const phoneRef = useRef(null);
return (<><TextField label="Mobile" ref={phoneRef} />
<button onClick={() => phoneRef.current.focus()}>Edit number</button></>);
}
// React 18: const TextField = forwardRef(function TextField(props, ref) { ... }); 37. How do you decide where a piece of state should live?
Start as local as possible: state used by one component stays in it. If siblings need it, lift it to the nearest common parent. Values many distant components read, such as the user or theme, go in context. Filters, tabs and search queries that users should be able to bookmark or share belong in the URL. Data that comes from the server is a cache, best handled by TanStack Query, SWR or framework loaders rather than copied into global state. Use a global store such as Zustand or Redux Toolkit only for complex client-side state that truly is shared.
38. What causes a component to re-render?
A component re-renders when its own state changes, when a context it reads changes, or when its parent re-renders. The last point surprises people: by default a child re-renders whenever its parent does, even if its props did not change, unless it is wrapped in memo and its props are shallowly equal. Props changing is not a separate trigger, because props only change when the parent renders. A re-render is just calling the function again; React then updates only the DOM nodes whose output actually changed.
39. What are portals, and when would you use one?
createPortal renders children into a different DOM node, usually document.body, while keeping them in the same place in the React tree. Use it for modals, tooltips, dropdowns and toasts that must escape a parent with overflow: hidden or a stacking context with a low z-index. Even though the DOM is elsewhere, context still works and events bubble through the React tree, so a click inside the modal reaches React handlers on its React parents. For a modal, still handle focus trapping, the Escape key and aria-modal.
import { createPortal } from "react-dom";
export default function Modal({ open, children }) {
if (!open) return null;
return createPortal(
<div role="dialog" aria-modal="true" className="modal">{children}</div>,
document.body
);
} 40. How do you test a component that fetches data?
Do not hit the real API. Mock the network: spy on fetch and return a fake Response, or, better for larger suites, use Mock Service Worker to intercept requests at the network level so the component code stays unchanged. Then render the component and use an async findBy query, which retries until the element appears or a timeout passes. Also assert on the request made and test the loading and error states, not just the happy path. Restore mocks after each test so they do not leak.
import { render, screen } from "@testing-library/react";
import { afterEach, expect, test, vi } from "vitest";
import Jobs from "./Jobs";
afterEach(() => vi.restoreAllMocks());
test("shows jobs from the API", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify([{ id: 1, title: "React Developer" }]))
);
render(<Jobs city="Pune" />);
expect(await screen.findByText("React Developer")).toBeTruthy();
expect(fetch).toHaveBeenCalledWith("/api/jobs?city=Pune");
}); React interview questions: Hard questions (experienced)
41. Explain reconciliation and the render and commit phases.
On an update React first runs the render phase: it calls components to build the new element tree and compares it with the current one. The comparison uses heuristics that make it O(n): a different element type at a position means the old subtree is destroyed and rebuilt, the same type means props are updated, and keys match list items. Rendering must be pure because, with concurrent features, React can pause, restart or discard it. Then the commit phase applies DOM changes synchronously, runs layout effects, and later runs passive effects. Fiber is the internal data structure that makes this work interruptible and prioritised.
42. How does useTransition keep typing responsive during a heavy update?
useTransition lets you mark some state updates as non-urgent. Updates inside startTransition are rendered in the background and can be interrupted: if the user types again, React drops the half-finished render and starts with the newest value, while urgent updates like the input text are applied immediately. isPending lets you show a subtle indicator. Transitions do not make the work itself faster and do not debounce network calls; they only change priority. In React 19, startTransition also accepts async functions, called actions.
import { useState, useTransition } from "react";
export default function SearchPage({ allJobs }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState(allJobs);
const [isPending, startTransition] = useTransition();
function onChange(e) {
const value = e.target.value;
setQuery(value); // urgent: the input updates immediately
const q = value.toLowerCase();
startTransition(() => setResults(allJobs.filter((j) => j.title.toLowerCase().includes(q))));
}
return (<><input value={query} onChange={onChange} />{isPending && <span>Updating...</span>}
<ul>{results.map((j) => <li key={j.id}>{j.title}</li>)}</ul></>);
} 43. What is useDeferredValue, and how is it different from debouncing?
useDeferredValue gives you a copy of a value that lags behind during heavy renders. React first re-renders with the old deferred value, so the input stays responsive, then renders with the new value in the background and abandons that render if the value changes again. Unlike a debounce, there is no fixed delay: on a fast laptop the lag is almost zero, and on a slow phone it adapts. It only helps if the slow child is wrapped in memo, so the urgent render can skip it. It does not reduce network requests, so still debounce API calls.
import { memo, useDeferredValue, useState } from "react";
const SlowList = memo(function SlowList({ text }) {
const items = Array.from({ length: 300 }, (_, i) => `${text} result ${i}`);
return <ul>{items.map((t) => <li key={t}>{t}</li>)}</ul>;
});
export default function Search() {
const [text, setText] = useState("");
const deferred = useDeferredValue(text);
return (<><input value={text} onChange={(e) => setText(e.target.value)} />
<div style={{ opacity: text !== deferred ? 0.5 : 1 }}><SlowList text={deferred} /></div></>);
} 44. How do Suspense and the use API work for data loading in React 19?
use reads the value of a promise or a context during render. If the promise is still pending, the component suspends, and the nearest Suspense boundary shows its fallback until it resolves; if it rejects, the nearest error boundary takes over. Unlike other hooks, use can be called inside conditions. The key rule is that the promise must be created outside render, for example in a Server Component, a router loader or a cache, and passed down; creating a fresh promise during render would suspend again on every render.
import { Suspense, use } from "react";
function JobDetails({ jobPromise }) {
const job = use(jobPromise); // suspends until the promise resolves
return <h2>{job.title}</h2>;
}
export default function JobPage({ jobPromise }) {
return (
<Suspense fallback={<p>Loading job...</p>}>
<JobDetails jobPromise={jobPromise} />
</Suspense>
);
} 45. How do React 19 actions and useActionState simplify form handling?
In React 19 you can pass a function to a form's action prop. React calls it with the form's FormData inside a transition, so you do not need preventDefault or manual pending flags. useActionState wraps that function: it receives the previous state and the FormData, its return value becomes the new state, and the hook also returns isPending. This is ideal for showing server validation errors and disabling the button while submitting. useFormStatus gives nested buttons the pending state, and useOptimistic shows the expected result before the server confirms.
import { useActionState } from "react";
async function applyAction(prevState, formData) {
const email = formData.get("email");
if (!email.includes("@")) return { error: "Enter a valid email" };
await new Promise((r) => setTimeout(r, 500)); // stand-in for an API call
return { error: null, done: true };
}
export default function ApplyForm() {
const [state, formAction, isPending] = useActionState(applyAction, { error: null });
return (<form action={formAction}><input name="email" />
<button disabled={isPending}>{isPending ? "Sending..." : "Apply"}</button>
{state.error && <p role="alert">{state.error}</p>}{state.done && <p>Applied!</p>}</form>);
} 46. Can useMemo and useCallback make performance worse? What does the React Compiler change?
Yes. Every memo hook adds work on each render: storing values and comparing dependencies. Wrapping cheap calculations, or callbacks passed to children that are not memoised, adds cost and clutter with no benefit, and wrong dependency arrays cause stale-data bugs. Memoise when profiling shows a real cost or when referential stability matters for memo children or effect dependencies. The React Compiler, now stable, analyses components at build time and inserts fine-grained memoisation automatically, so new code needs far fewer manual useMemo and useCallback calls, provided components follow the rules of React.
47. A React page feels slow. How do you find and fix the cause?
Measure before changing anything. Use the React DevTools Profiler to see which components render, how often and how long, and turn on highlighting of updates. Use the Chrome Performance panel to find long tasks, and check Interaction to Next Paint in field data. Typical causes are state placed too high so a whole page re-renders on each keystroke, unstable props that defeat memo, large lists rendered at once, heavy synchronous calculations, and big bundles. Fixes include moving state down, memo with stable props, list virtualisation, transitions, code splitting and moving work to a Web Worker. Re-measure after each fix.
48. What are React Server Components, and how do they differ from Client Components?
Server Components run only on the server, at build or request time, and send rendered output to the client instead of their JavaScript, so their code and dependencies add nothing to the bundle. They can be async and read databases or files directly, but cannot use state, effects, browser APIs or event handlers. Client Components are the normal interactive components; the "use client" directive marks the boundary where client code begins. Props passed across that boundary must be serialisable. Note that "use server" marks Server Functions that the client can call, not Server Components. They need a framework such as the Next.js App Router.
49. How do you choose between Context, Redux Toolkit, Zustand and TanStack Query?
First separate server state from client state. Server state, meaning data fetched from APIs, needs caching, deduplication, background refresh and invalidation, which TanStack Query or SWR does well; putting it in Redux by hand recreates that work badly. For client state, Context suits values that change rarely, such as theme or the logged-in user, because every reader re-renders on change. Zustand is a light store with selector-based subscriptions for shared, frequently changing UI state. Redux Toolkit suits large teams that want strict conventions, middleware and time-travel debugging. Many apps need only Query plus a little Context.
50. How do you stop a context from re-rendering too many components?
Every consumer re-renders when the provider's value changes by reference. Three fixes help. Memoise the value so it changes only when its data changes, not on every provider render. Split one large context into several by how often each part changes, for example user data separate from stable actions, so components that only call logout never re-render when the user changes. And keep fast-changing values, such as a mouse position, out of context, or move them to a store with selectors. Here the actions object is created once, so components reading only actions stay quiet.
import { createContext, useMemo, useState } from "react";
export const UserContext = createContext(null);
export const UserActionsContext = createContext(null);
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
const actions = useMemo(() => ({
login: (u) => setUser(u),
logout: () => setUser(null),
}), []);
return (<UserActionsContext value={actions}>
<UserContext value={user}>{children}</UserContext>
</UserActionsContext>);
} 51. What is useSyncExternalStore, and why does it exist?
useSyncExternalStore subscribes a component to a data source that lives outside React, such as a browser API or a store library. You give it a subscribe function that registers a callback and returns an unsubscribe function, a getSnapshot function that returns the current value, and optionally a getServerSnapshot for server rendering. It exists because reading external mutable data during concurrent rendering can cause tearing, where parts of the screen show different values from the same render; this hook guarantees a consistent snapshot. getSnapshot must return the same value when nothing changed, or React re-renders endlessly.
import { useSyncExternalStore } from "react";
function subscribe(callback) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
} 52. How do you write a useInterval hook that always calls the latest callback?
If the callback is listed as an effect dependency, the interval restarts on every render because inline functions change each time. If it is left out, the interval keeps calling the first render's callback with stale values. The classic fix stores the latest callback in a ref, updated after each render, and lets the interval call ref.current, so the timer depends only on the delay. React 19.2 added useEffectEvent for this pattern: it gives you a function that always sees the latest props and state without being a dependency.
import { useEffect, useRef } from "react";
export function useInterval(callback, delay) {
const saved = useRef(callback);
useEffect(() => { saved.current = callback; }, [callback]);
useEffect(() => {
if (delay == null) return;
const id = setInterval(() => saved.current(), delay);
return () => clearInterval(id);
}, [delay]);
} 53. What causes hydration mismatch errors, and how do you fix them?
During hydration, React expects the first client render to produce exactly the HTML the server sent. A mismatch happens when output depends on something that differs between the two: Date.now() or new Date(), Math.random(), window or localStorage checks, locale-specific formatting, or invalid HTML nesting such as a div inside a p that the browser rearranges. Fixes: render client-only values after mount in an effect, pass the server's value down as data, use useId for ids, and use suppressHydrationWarning only for unavoidable text like timestamps.
import { useId } from "react";
export default function Field({ label }) {
const id = useId(); // same on server and client, unique per instance
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
} 54. How would you design a reusable Tabs component with a flexible API?
Use the compound components pattern: a parent Tabs component owns the state and shares it through context, while Tab and Panel children read it. Users then arrange the pieces freely, adding icons, badges or custom layouts, without a giant configuration prop. Consider supporting both controlled use, with value and onChange props, and uncontrolled use with a default. Accessibility is part of the API: tabs need role="tab", aria-selected, a tablist container, and arrow-key navigation. Throw a clear error if a Tab is used outside Tabs.
import { createContext, useContext, useState } from "react";
const TabsCtx = createContext(null);
export function Tabs({ defaultTab, children }) {
const [active, setActive] = useState(defaultTab);
return <TabsCtx value={{ active, setActive }}>{children}</TabsCtx>;
}
export function Tab({ id, children }) {
const { active, setActive } = useContext(TabsCtx);
return <button role="tab" aria-selected={active === id} onClick={() => setActive(id)}>{children}</button>;
}
export function Panel({ id, children }) {
return useContext(TabsCtx).active === id ? <div role="tabpanel">{children}</div> : null;
} 55. How do you render a list of 10,000 rows without freezing the page?
Rendering every row creates thousands of DOM nodes, which makes the first render, scrolling and memory use slow. The standard fix is virtualisation, or windowing: render only the rows visible in the viewport plus a small buffer, inside a container sized to the full list height, and swap rows as the user scrolls. Libraries such as TanStack Virtual and react-window do this. Trade-offs include harder support for variable row heights, browser find-in-page not seeing hidden rows, and extra accessibility work. Alternatives are server-side pagination, infinite loading, or CSS content-visibility for simpler cases.
56. How do you test a custom hook that uses timers?
Use renderHook from React Testing Library, which renders the hook inside a small test component and exposes the latest return value as result.current. Use fake timers so the test does not wait in real time, and wrap the timer advance in act so React applies the resulting state update before you assert. rerender lets you pass new arguments, such as a new value to debounce. Restore real timers at the end so other tests are not affected. This tests the hook's behaviour, not its internals.
import { act, renderHook } from "@testing-library/react";
import { expect, test, vi } from "vitest";
import { useDebouncedValue } from "./useDebouncedValue";
test("updates only after the delay", () => {
vi.useFakeTimers();
const { result, rerender } = renderHook(({ v }) => useDebouncedValue(v, 300), { initialProps: { v: "a" } });
rerender({ v: "ab" });
expect(result.current).toBe("a");
act(() => { vi.advanceTimersByTime(300); });
expect(result.current).toBe("ab");
vi.useRealTimers();
}); 57. Why can an object in the dependency array cause an infinite fetch loop?
Dependencies are compared with Object.is. An object or array created during render, such as const filters = { city, page }, is a new reference every time, so the effect runs after every render. If the effect sets state, that state change causes another render, a new object, and another fetch, endlessly. Fix it by depending on the primitive values themselves, creating the object inside the effect, or memoising it with useMemo. The same applies to functions defined in the component. Adding an AbortController also cancels any request that a newer one replaces.
import { useEffect, useState } from "react";
export default function JobCount({ city, page }) {
const [jobs, setJobs] = useState([]);
// Bug: const filters = { city, page }; with [filters] as deps, this would loop
useEffect(() => {
const controller = new AbortController();
fetch(`/api/jobs?city=${encodeURIComponent(city)}&page=${page}`, { signal: controller.signal })
.then((r) => r.json()).then(setJobs)
.catch((err) => { if (err.name !== "AbortError") console.error(err); });
return () => controller.abort();
}, [city, page]); // primitives are compared by value
return <p>{jobs.length} jobs</p>;
} 58. How should you structure complex component state to avoid bugs?
Keep state minimal and free of duplicates. Store the id of the selected item, not a copy of the item, otherwise editing the list leaves the copy stale. Do not store values you can derive. Group fields that always change together, and avoid contradictory flags such as isLoading and isError both being true; a single status field with values like idle, loading, success and error is safer. Avoid deep nesting: normalise data by id, as in a database. In the example, renaming a job updates the selected input automatically because the selection is derived from the id.
import { useState } from "react";
export default function JobPicker({ initialJobs }) {
const [jobs, setJobs] = useState(initialJobs);
const [selectedId, setSelectedId] = useState(null); // store the id, not a copy
const selected = jobs.find((j) => j.id === selectedId);
const rename = (id, title) =>
setJobs((list) => list.map((j) => (j.id === id ? { ...j, title } : j)));
return (<><ul>{jobs.map((j) => <li key={j.id} onClick={() => setSelectedId(j.id)}>{j.title}</li>)}</ul>
{selected && <input value={selected.title} onChange={(e) => rename(selected.id, e.target.value)} />}</>);
} 59. How does React protect against XSS, and where can you still introduce it?
React escapes any string you render inside JSX, so text from users is shown as text, never parsed as HTML. The protection ends where you bypass it. dangerouslySetInnerHTML inserts raw HTML, so sanitise it first with a library like DOMPurify, ideally on the server too. React 19 blocks javascript: URLs in attributes like href, but React 18 only warned, and other schemes such as data: still pass, so allow only http and https links from user input. Also be careful with server-rendered JSON embedded in script tags, third-party scripts, and refs that set innerHTML directly.
import DOMPurify from "dompurify";
const isSafeUrl = (url) => /^https?:\/\//i.test(url);
export function JobDescription({ html, applyUrl }) {
return (
<>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
{isSafeUrl(applyUrl) && <a href={applyUrl}>Apply on company site</a>}
</>
);
} 60. How can you stop an expensive child from re-rendering when a parent's state changes often, without memo?
Pass the expensive part as children instead of rendering it inside the component that owns the fast-changing state. In the example, MouseTracker re-renders on every mouse move, but the ExpensiveChart element was created by Page, which did not re-render, so React receives the same element and skips it. The related technique is moving state down: put fast-changing state in the smallest component that needs it. Both use composition rather than memoisation, so they also work without React.memo and keep the code simple.
import { useState } from "react";
function MouseTracker({ children }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
<p>{pos.x}, {pos.y}</p>
{children}
</div>
);
}
function ExpensiveChart() { return <canvas width={600} height={300} />; }
export default function Page() {
return <MouseTracker><ExpensiveChart /></MouseTracker>;
} Ready to apply?
There are 788 open jobs asking for React on CoreHRx right now. Create a free profile once and apply with it.