React Interview Questions and Answers
27 questions8 beginner · 13 intermediate · 6 advancedLast reviewed
This set is for front-end and full-stack candidates preparing for React rounds, from a first junior screen to a senior discussion about performance and architecture. It focuses on the reasoning interviewers want to hear: why React behaves the way it does, not only which hook to reach for.
Practise by explaining each answer aloud in under a minute before you open it, then compare what you missed. For the code samples, type them into a scratch project and change one thing, such as removing a key or a dependency, and predict what breaks. If JavaScript fundamentals feel shaky, revisit the React course and JavaScript fundamentals alongside the JavaScript question set.
1What is a component in React, and how are props different from state?Beginner
A component is a function that receives props and returns a description of UI (JSX). React calls it to find out what to show, and you build screens by composing small components into bigger ones.
Props are inputs passed down by the parent, and the component treats them as read-only. State is memory the component owns between renders, created with
useState; changing it schedules a re-render. In practice data flows down through props and events flow up through callback props.For example, a parcel card receives a
parcelprop but owns anexpandedstate. A common mistake is copying a prop into state, which then goes stale when the parent sends a new value.What the interviewer is checking: That you can explain the data-flow model in your own words, including the stale copy pitfall.
Likely follow-up: A child component needs to change something the parent owns. How does it do that?
2Why does React need a
keyon list items, and why is the array index a risky choice?BeginnerKeys let React match items between renders, so it knows which row is which. With stable keys, React reuses the same DOM node and component state for the same logical item and simply moves it when the order changes.
A good key is unique among siblings and stable, typically the database ID. The index is only safe for a static list that never reorders, inserts or deletes. Otherwise state and DOM details such as typed text or focus stay attached to the position instead of the item, so after removing the first row the second row may show the first row's input text.
Random keys are worse: a new key every render means every row remounts, losing state and doing extra work.
tsx type Slot = { id: string; label: string }; function SlotList({ slots }: { slots: Slot[] }) { return ( <ul> {slots.map((slot, index) => ( // Risky: when a slot is inserted or removed, index n now points at a different slot, // so React keeps the old row's DOM and input text for the wrong slot. <li key={index}> <label> {slot.label} <input defaultValue="" /> </label> </li> ))} </ul> ); } function SlotListFixed({ slots }: { slots: Slot[] }) { return ( <ul> {slots.map((slot) => ( <li key={slot.id}> <label> {slot.label} <input defaultValue="" /> </label> </li> ))} </ul> ); } export { SlotList, SlotListFixed };What the interviewer is checking: Whether you understand keys as identity for reconciliation, not just a warning to silence.
Likely follow-up: Is it acceptable to use the index as a key for a list that is paginated but never reordered? What would you check?
3What is the difference between controlled and uncontrolled inputs? When would you pick each?Beginner
A controlled input gets its value from React state and updates it through
onChange, so React is the single source of truth. An uncontrolled input keeps its value in the DOM; you give it adefaultValueand read the value through a ref orFormDatawhen you need it.Controlled inputs suit live validation, formatting as you type, disabling a button based on the value, or fields that depend on one another. Uncontrolled inputs are simpler and re-render less for plain forms that you only read on submit, and they are needed for file inputs.
A classic bug is switching an input from uncontrolled to controlled, for example
value={undefined}first and a string later. Always initialise controlled values, usually to an empty string.tsx import { useRef, useState } from "react"; // Controlled: React state is the single source of truth for the value. function PromoCodeControlled() { const [code, setCode] = useState(""); return <input value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} />; } // Uncontrolled: the DOM keeps the value; we read it when we need it. function PromoCodeUncontrolled({ onApply }: { onApply: (code: string) => void }) { const inputRef = useRef<HTMLInputElement>(null); return ( <form onSubmit={(e) => { e.preventDefault(); onApply(inputRef.current?.value ?? ""); }} > <input ref={inputRef} defaultValue="" /> <button type="submit">Apply</button> </form> ); } export { PromoCodeControlled, PromoCodeUncontrolled };What the interviewer is checking: Practical judgement about forms, and awareness of the controlled/uncontrolled switching warning.
Likely follow-up: How would you handle a large form with fifty fields without re-rendering everything on each keystroke?
4Why can't you modify state directly, for example with
items.push(x)? What should you do instead?BeginnerReact finds out that something changed when you call the setter, and it compares the new value with the old one by reference. If you mutate the array in place and pass the same reference, React sees no change and may skip the re-render. Memoised children and effects that compare by reference would not notice either.
Create a new value instead:
[...items, x],items.filter(...),items.map(...), and for objects{ ...user, name }. For deeply nested state, flatten it or use a helper like Immer.Related gotcha: state is a snapshot for each render. Calling
setCount(count + 1)three times in one handler adds only one, becausecountis the same value each time. Use the updater form,setCount((c) => c + 1), when the next value depends on the previous one.What the interviewer is checking: Whether you understand reference comparison and that state is a per-render snapshot.
Likely follow-up: How would you update one field of one item inside an array of objects held in state?
5What is
useEffectfor, and what should you not use it for?BeginneruseEffectlets a component synchronise with something outside React after rendering: subscriptions, timers, network requests, browser APIs, or a third-party widget. It runs after the screen has been updated.It is often misused. Do not use it to compute values from props or state, because you can calculate those during render. Do not use it to react to a click, because that belongs in the event handler. And do not use it to reset state when a prop changes; give the component a different
keyinstead.A useful rule of thumb: if there is no external system involved, you probably do not need an effect. Extra effects add extra render passes and make the data flow harder to follow.
What the interviewer is checking: Whether you see effects as a synchronisation escape hatch rather than a general-purpose lifecycle callback.
Likely follow-up: A filtered list is computed in an effect and stored in state. How would you simplify that?
6What is the virtual DOM, and why does React use it?Beginner
The virtual DOM is the tree of lightweight JavaScript objects (React elements) that your components return. When state changes, React calls the components again to get a new tree, compares it with the previous one, and applies only the differences to the real DOM.
The point is not that it beats hand-written DOM code in raw speed. The benefit is the declarative model: you describe what the UI should look like for the current state, and React works out the minimal updates.
It also means work happens in two phases. Render is a pure calculation of the next tree; commit is where the DOM is updated and effects run. Performance tuning is about avoiding renders that are unnecessary or expensive, since calling your component functions still costs time even if the DOM does not change.
What the interviewer is checking: Whether you avoid the "it is faster than the DOM" cliché and describe render versus commit.
Likely follow-up: Does a re-render always change the DOM?
7Two sibling components need the same piece of data. How do you share it?Beginner
Lift the state up to their closest common parent. The parent owns the value with
useStateand passes it down as props, along with a callback so either child can request a change. Both children then always agree because there is only one copy.In the sample, the seat map and the summary both show the chosen seat, and the booking component owns it. If the state is needed far down a deep tree, passing props through many layers gets noisy, and context or a store may be better. But lifting state is the first tool, and it is often enough.
Avoid keeping a copy in each child and syncing them with an effect. That creates two sources of truth and bugs that depend on timing.
tsx import { useState } from "react"; import { renderToStaticMarkup } from "react-dom/server"; function SeatMap({ selected, onSelect }: { selected: string; onSelect: (seat: string) => void }) { return ( <div> {["A1", "B2", "C3"].map((seat) => ( <button key={seat} aria-pressed={seat === selected} onClick={() => onSelect(seat)}> {seat} </button> ))} </div> ); } function Summary({ seat }: { seat: string }) { return <p>You picked seat {seat}</p>; } // The parent owns the state; both children receive it through props. function Booking() { const [seat, setSeat] = useState("B2"); return ( <> <SeatMap selected={seat} onSelect={setSeat} /> <Summary seat={seat} /> </> ); } console.log(renderToStaticMarkup(<Booking />));Output
<div><button aria-pressed="false">A1</button><button aria-pressed="true">B2</button><button aria-pressed="false">C3</button></div><p>You picked seat B2</p>
What the interviewer is checking: Whether you default to the simplest data-flow solution and can name its limit.
Likely follow-up: The state is needed ten levels down. What are your options besides passing props through every level?
8When does a React component re-render?Beginner
A component re-renders when its own state changes, when its parent re-renders, or when a context it reads changes. Notice that changing props is not the trigger by itself. By default, a parent re-render re-renders all its children, even if their props are identical.
Re-rendering means React calls the function again. It does not necessarily change the DOM; React compares the output and only touches what differs. Setting state to the same value (by
Object.is) usually lets React bail out.React.memolets a child skip re-rendering when its props are shallow-equal. But re-rendering is normally cheap, so measure before optimising. The most effective fix is often to move state down so fewer components are affected.What the interviewer is checking: An accurate model of the render triggers, including the common belief that unchanged props prevent re-rendering.
Likely follow-up: Typing in a search box re-renders a heavy table next to it. How do you stop that?
9How does the
useEffectdependency array work, and what is the cleanup function for?IntermediateThe effect runs after the first render and again after any render in which a value in the array changed, compared with
Object.is. An empty array means once on mount. Omitting the array means after every render. List everything from the component scope that the effect reads; the exhaustive-deps lint rule checks this, and silencing it invites stale values.The function you return is the cleanup. React runs it before the next run of the effect and when the component unmounts, so use it to undo what the effect did: unsubscribe, clear a timer, abort a request.
If a dependency is an object or function created during render, the effect re-runs every time. Move it inside the effect, derive a primitive, or stabilise it, rather than removing it from the array.
What the interviewer is checking: Whether you know the exact re-run rules and treat the lint rule as correct instead of an annoyance.
Likely follow-up: An effect re-runs on every render because it depends on an options object. What are your choices?
10A setInterval inside useEffect keeps showing the initial count. What is going on and how do you fix it?Intermediate
This is a stale closure. Each render creates a new function that captures that render's
count. With an empty dependency array the effect runs once, so the interval callback keeps the closure from the first render, wherecountis always 0.There are three fixes. Use the updater form,
setCount((c) => c + 1), which does not need the current value from the closure. Addcountto the dependencies so the interval is recreated on each change, which works but restarts the timer. Or keep the latest callback in a ref that the interval reads.The updater form is usually best. The key idea is that every render has its own values, and a function only sees the ones from the render that created it.
tsx import { useEffect, useState } from "react"; function MeterBuggy() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); // count is the value from the first render: always 0 }, 1000); return () => clearInterval(id); }, []); // the effect never re-runs, so the closure is never refreshed return <p>{count}</p>; } function MeterFixed() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount((c) => c + 1); // the updater receives the latest value }, 1000); return () => clearInterval(id); }, []); return <p>{count}</p>; } export { MeterBuggy, MeterFixed };What the interviewer is checking: Whether you understand closures per render, not just the memorised fix.
Likely follow-up: How would you use a ref so a timer always calls the latest version of a callback prop?
11What do useMemo, useCallback and React.memo do, and how do they work together?Intermediate
React.memowraps a component so it skips re-rendering when its props are shallow-equal to the previous ones.useMemocaches a computed value between renders until its dependencies change.useCallbackcaches a function's identity, and is equivalent touseMemo(() => fn, deps).They combine because
React.memocompares props by reference. If the parent passes a new function or object on every render, the memo check always fails.useCallbackanduseMemokeep those references stable so the child can really skip.Treat them as performance hints, not guarantees that React will never recompute. In the sample, rows are memoised, the click handler has a stable identity, and filtering only reruns when the list or query changes.
tsx import { memo, useCallback, useMemo, useState } from "react"; type Fare = { id: string; label: string; price: number }; declare function track(id: string): void; const FareRow = memo(function FareRow({ fare, onSelect }: { fare: Fare; onSelect: (id: string) => void }) { return <li onClick={() => onSelect(fare.id)}>{fare.label}: {fare.price}</li>; }); function FareList({ fares }: { fares: Fare[] }) { const [query, setQuery] = useState(""); // Stable identity, so memo(FareRow) can skip re-rendering rows while typing. const onSelect = useCallback((id: string) => track(id), []); // Only recomputed when the inputs change. const visible = useMemo( () => fares.filter((f) => f.label.toLowerCase().includes(query.toLowerCase())), [fares, query], ); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul> {visible.map((fare) => ( <FareRow key={fare.id} fare={fare} onSelect={onSelect} /> ))} </ul> </> ); } export { FareList };What the interviewer is checking: Whether you can explain how the three fit together instead of treating each as a magic speed-up.
Likely follow-up: If FareRow is not wrapped in memo, does the useCallback on onSelect do anything useful?
12Is wrapping everything in useMemo and useCallback a good habit? When does memoisation hurt?Intermediate
No. Each memo has a cost: comparing dependencies, holding old values in memory, extra code to read, and the risk of a wrong dependency list that returns stale data. If the saved work is cheaper than that overhead, you have made things slower and harder to read.
It pays off when a stable reference matters, such as a prop to a memoised child or a dependency of an effect, when you have measured an expensive calculation like sorting thousands of rows, or when a context value object is read by many consumers. It does nothing if the child is not memoised or if the props change anyway.
Fix the structure first: move state down, split components, or pass heavy parts as
children. Verify with the Profiler. Where the React Compiler is adopted, much of this memoisation is automated.What the interviewer is checking: Judgement: measured optimisation over cargo-cult memoisation.
Likely follow-up: How could you stop a slow sibling re-rendering on every keystroke without using memo at all?
13What is
useRefused for, and how does it differ from state?IntermediateuseRefreturns a mutable object,{ current }, that persists across renders. Changingcurrentdoes not trigger a re-render. Use it for two things: holding a DOM node, to focus an input or measure an element, and storing values you need to remember but that do not affect what is on screen, such as a timer ID, the previous value or the latest callback.State is a snapshot that triggers a re-render when it changes, and it should drive the UI. A ref is an escape hatch. Do not read or write
ref.currentduring render for anything that changes the output, because React will not update the screen when it changes.DOM refs are
nulluntil the component is committed, so access them in effects or event handlers.What the interviewer is checking: Whether you can separate render-driving data from bookkeeping data.
Likely follow-up: How would you store the previous value of a prop with a ref, and why does it update one render late?
14What is a custom hook? When would you write one, and what are the rules?Intermediate
A custom hook is a function whose name starts with
useand that calls other hooks. It reuses stateful logic, not UI, between components. Typical examples are debouncing, subscriptions, form handling or a media-query listener.Each call gets its own isolated state, so two components that call the same hook do not share values. If you need sharing, use context or a store. The
useprefix matters because the linter then applies the rules of hooks to the function.Keep the API small and pass dependencies in as arguments. Pitfall: returning a new object or function on every render forces consumers to re-run effects or defeats memoisation, so return stable references when it matters.
tsx import { useEffect, useState } from "react"; export function useDebouncedValue<T>(value: T, delayMs: number): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delayMs); return () => clearTimeout(id); // a new keystroke cancels the pending update }, [value, delayMs]); return debounced; } // Usage: the effect that calls the API depends on the debounced text, not the raw text. export function ClinicSearch({ search }: { search: (q: string) => void }) { const [text, setText] = useState(""); const debounced = useDebouncedValue(text, 300); useEffect(() => { if (debounced) search(debounced); }, [debounced, search]); return <input value={text} onChange={(e) => setText(e.target.value)} />; }What the interviewer is checking: Whether you can extract logic cleanly and know that hooks share logic but not state.
Likely follow-up: In the usage above, what happens if the parent passes a new search function on every render?
15Why must hooks be called at the top level, and not inside conditions or loops?Intermediate
React does not identify hooks by name. It stores each component's hook state in a list and matches calls by their order during render: first
useState, seconduseEffect, and so on. If a hook is skipped on one render, every later hook reads the wrong slot, and state gets mixed up.So the rules are: call hooks in the same order every render, at the top level of a component or custom hook, and only from those places. An early
returnbefore a hook also breaks it.To make something conditional, put the condition inside the hook, for example an
ifinside an effect, or move the hook into a child component that you render conditionally. In React 19 theuseAPI is a deliberate exception that can be called conditionally.What the interviewer is checking: Whether you know the mechanism behind the rule instead of reciting it.
Likely follow-up: How would you show a different data-fetching hook depending on a prop, without breaking the rules?
16Context or a state library like Redux or Zustand: how do you decide?Intermediate
Context is a way to pass a value down without prop drilling. It is not a state manager. It fits values that are needed widely and change rarely, such as theme, locale or the signed-in user. When the value changes, every component that reads that context re-renders, and there is no selector to subscribe to just a slice.
So a large, fast-changing object in context causes broad re-renders. You can split contexts or memoise the value, but at some point a store with selectors (Redux Toolkit, Zustand, Jotai) fits better and brings devtools and middleware.
A sensible order is local state, lifting state up, context for stable app-wide values, then a store for complex shared client state. Server data usually belongs in a data-fetching cache, not copied into either.
tsx import { createContext, useContext } from "react"; import { renderToStaticMarkup } from "react-dom/server"; const CurrencyContext = createContext<"USD" | "INR">("USD"); // default used without a provider function Price({ amount }: { amount: number }) { const currency = useContext(CurrencyContext); return <span>{amount} {currency}</span>; } console.log( renderToStaticMarkup( <> <Price amount={10} /> <CurrencyContext.Provider value="INR"> <Price amount={10} /> </CurrencyContext.Provider> </>, ), );Output
<span>10 USD</span><span>10 INR</span>
What the interviewer is checking: Whether you know context's re-render behaviour and choose tools by problem, not by fashion.
Likely follow-up: A context value is an object created inline in the provider. What goes wrong?
17How does React decide whether to update or recreate a component? Where does
keyfit in?IntermediateDuring reconciliation React compares the old and new element at the same position in the tree. If the type is the same, such as the same component or the same tag, it updates in place, keeps its state, and compares the children. If the type differs, it unmounts the old subtree, destroying its state, and mounts a new one.
Two consequences. Defining a component inside another component's body creates a new type on every render, so it remounts each time and loses state and focus; define components at module level. And you can use
keydeliberately:<ProfileForm key={userId} />remounts when the user changes, which resets the form cleanly without an effect.Keys in lists work the same way: they tell React which element is which when the order changes.
What the interviewer is checking: Whether you understand identity and position in the tree, and the "change the key to reset" technique.
Likely follow-up: Why does a component defined inside another component lose its input focus on every keystroke?
18What do error boundaries catch, and what do they not catch?Intermediate
An error boundary is a class component that defines
getDerivedStateFromErrorand optionallycomponentDidCatch. It catches errors thrown while rendering its descendants, in their lifecycle methods and in constructors, and shows a fallback instead of unmounting the whole app.It does not catch errors in event handlers (use try/catch there), in asynchronous code such as timers and promises, during server rendering, or in the boundary itself. A rejected promise in a click handler never reaches it.
Place boundaries at sensible granularity: around a widget or route so the rest of the page keeps working, plus one at the top. Log in
componentDidCatch. There is no hook version; libraries like react-error-boundary wrap the class, and frameworks often provide a file convention for it.tsx import { Component, type ErrorInfo, type ReactNode } from "react"; declare function logToService(error: Error, componentStack: string | null | undefined): void; type Props = { fallback: ReactNode; children: ReactNode }; type State = { failed: boolean }; export class WidgetBoundary extends Component<Props, State> { state: State = { failed: false }; static getDerivedStateFromError(): State { return { failed: true }; // switch to the fallback UI on the next render } componentDidCatch(error: Error, info: ErrorInfo) { logToService(error, info.componentStack); // side effects such as logging belong here } render() { return this.state.failed ? this.props.fallback : this.props.children; } } // <WidgetBoundary fallback={<p>Chart unavailable</p>}><RevenueChart /></WidgetBoundary>What the interviewer is checking: Precise knowledge of the boundary's scope, including what slips past it.
Likely follow-up: How would you let the user retry after the fallback is shown?
19Explain React.lazy and Suspense. When would you code-split a component?Intermediate
React.lazy(() => import("./Chart"))creates a component whose code is downloaded the first time it renders. While it loads, the component suspends, and the nearest<Suspense fallback={...}>above it shows the fallback. Once the code arrives, React renders the real component.Where you put the boundary decides the experience: one high up gives a whole-page spinner, several low down give per-widget placeholders. Suspense is not limited to lazy loading. Frameworks and data libraries that support it can suspend while data loads, and it is part of streaming server rendering.
Split heavy or rarely used UI such as chart libraries, editors, modals and admin screens. Do not split tiny components. Combine with an error boundary for failed chunk loads, and note that a plain
useEffectfetch does not trigger Suspense.What the interviewer is checking: A conceptual model of suspending, with sensible judgement about what to split.
Likely follow-up: What does the user see if the lazy chunk fails to load, and how do you handle it?
20How do you test React components? What do you avoid testing?Intermediate
Test behaviour from the user's point of view. React Testing Library renders the component into a DOM, queries it the way a user finds things, by role, label or text, drives it with user-event, and asserts on what appears. Because the test does not know the internals, you can refactor freely.
Avoid asserting on state values, internal method calls, class names, or large snapshots, since they break on harmless refactors and prove little. Mock at the network boundary, with a tool like MSW or a stubbed
fetch, instead of mocking your own hooks. For asynchronous UI usefindByqueries orwaitFor.Mix test types: unit tests for pure logic and custom hooks, component tests for behaviour, and a few end-to-end tests for critical flows such as checkout. Query by accessible role when possible, which also nudges you toward accessible markup.
What the interviewer is checking: A testing philosophy centred on user-visible behaviour, plus awareness of the cost of brittle tests.
Likely follow-up: A test passes but the button is unusable with a keyboard. What would have caught it?
21Why does my effect run twice in development? Is something broken?Intermediate
In development,
<StrictMode>deliberately mounts a component, unmounts it and mounts it again, so effects run, clean up, and run again. It also calls render functions twice. This does not happen in production.The purpose is to expose bugs: an effect without a proper cleanup, or a render that is not pure. If the double run causes a visible problem such as a duplicate subscription or two analytics events, your cleanup is incomplete. The fix is a symmetrical cleanup, for instance unsubscribe or abort the request, not a "has run" ref hack that hides the problem.
Truly one-time work, such as app initialisation, belongs at module level or in an event handler. This resilience also matters because React may unmount and restore state in newer features.
What the interviewer is checking: Whether you read StrictMode as a diagnostic tool and fix the cleanup instead of fighting it.
Likely follow-up: How would you make a chat connection effect safe under this mount, unmount, mount sequence?
22What are server components and client components? How do you decide where the boundary goes?Advanced
In frameworks that support React Server Components, such as the Next.js App Router, components run on the server by default. Their code is not shipped to the browser, so they can read a database or secrets directly, use async/await, and shrink the bundle. They cannot use state, effects, event handlers or browser APIs.
A file marked with
"use client"creates a client boundary: that component and everything it imports are bundled for the browser, hydrated, and can be interactive. Client components are still pre-rendered on the server for the first load. Props that cross from server to client must be serialisable.A good rule is to keep components on the server and push
"use client"down to small interactive leaves. A server component can pass server-rendered content aschildreninto a client component. Details differ by framework and version, so check the docs. See the Next.js course.What the interviewer is checking: A clear mental model of what runs where, hedged appropriately for framework differences.
Likely follow-up: Why can't you pass a plain function as a prop from a server component to a client component?
23A form feels laggy while typing. Walk me through how you would find and fix the cause.Advanced
Measure before changing code. Record the interaction in the React DevTools Profiler to see which components render, how long each takes, and why they rendered; use the browser Performance panel for long tasks. Test a production build, since development mode and StrictMode are slower.
Then fix by cause. State held too high up: move it down or isolate the input in its own component. Children re-rendering needlessly: pass them as
children, or memoise with stable props. Expensive derived data:useMemo. Huge lists: virtualise them so only visible rows render. Heavy but non-urgent updates:useDeferredValueorstartTransition.Also check for inline context values and effects that set state in a chain. Re-measure after each change, and stop once the interaction feels responsive; the target is the user's experience, not zero re-renders.
What the interviewer is checking: A methodical measure-diagnose-fix approach, not a list of memoisation tricks.
Likely follow-up: The Profiler shows one component taking 200 ms to render. What do you check inside it?
24What do useTransition and useDeferredValue do? When would you use each?Advanced
Both are concurrent rendering features from React 18. They let you mark some updates as non-urgent so that urgent ones, like typing and clicking, stay responsive. React can interrupt the background render if a new urgent update arrives.
useTransitiongives youstartTransitionandisPending; state updates inside the callback are treated as a transition. Use it when you control the update, such as switching a tab.useDeferredValuetakes a value and returns a version that may lag behind: React first renders with the old value, then re-renders in the background with the new one. Use it when the value comes from props or an input you do not want to wrap.Neither makes a slow render faster, and neither debounces network requests. They only make the work interruptible.
tsx import { useDeferredValue, useMemo, useState } from "react"; function ResultList({ items }: { items: string[] }) { return ( <ul> {items.map((item) => ( <li key={item}>{item}</li> ))} </ul> ); } export function RecipeSearch({ recipes }: { recipes: string[] }) { const [query, setQuery] = useState(""); const deferredQuery = useDeferredValue(query); // may lag behind query while React is busy const results = useMemo( () => recipes.filter((r) => r.toLowerCase().includes(deferredQuery.toLowerCase())), [recipes, deferredQuery], ); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ResultList items={results} /> </> ); }What the interviewer is checking: Whether you understand that these prioritise work rather than reduce it, and know their limits.
Likely follow-up: How is useDeferredValue different from debouncing the input?
25Fetching data in useEffect can show stale or wrong results. Why, and how do you make it safe?Advanced
Requests can finish out of order. If the user switches from one route to another and the first response arrives last, the screen shows data for the wrong input. The same problem appears when a component unmounts before the response lands.
Return a cleanup from the effect that cancels the request with an
AbortController, or sets anignoreflag so late results are dropped. Reset or show a loading state when inputs change, and handle errors separately from aborts. The double mount in StrictMode is a good test for this.In real applications, prefer a data layer such as TanStack Query or SWR, or your framework's loaders. They add caching, de-duplication, retries and race handling that are tedious and error-prone to rebuild by hand.
tsx import { useEffect, useState } from "react"; type Quote = { total: number }; export function useRideQuote(pickup: string, drop: string) { const [quote, setQuote] = useState<Quote | null>(null); const [error, setError] = useState<string | null>(null); useEffect(() => { const controller = new AbortController(); setQuote(null); setError(null); const params = new URLSearchParams({ from: pickup, to: drop }); fetch("/api/quote?" + params, { signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error("HTTP " + res.status); return res.json() as Promise<Quote>; }) .then(setQuote) .catch((err: unknown) => { if (err instanceof DOMException && err.name === "AbortError") return; // superseded request setError(err instanceof Error ? err.message : "failed"); }); return () => controller.abort(); // runs before the next effect and on unmount }, [pickup, drop]); return { quote, error }; }What the interviewer is checking: Whether you can reason about asynchronous races and know when to hand the problem to a library.
Likely follow-up: What does a library like TanStack Query give you that this hook does not?
26What is a hydration mismatch, what causes it, and how do you fix it?Advanced
With server rendering, the browser first receives HTML, then React hydrates it by attaching to the existing DOM. React expects the first client render to produce the same output as the server did. A mismatch means they differ.
Typical causes: values that change between server and client such as
Date.now(),Math.random()or a date formatted in a different locale or time zone; branching ontypeof windoworlocalStorageduring render; invalid HTML nesting such as a<div>inside a<p>; and browser extensions that modify the page. You get a warning in development, and React may patch or discard the server HTML, causing flicker.Fixes: compute browser-only values after mount in an effect, starting from a server-safe initial state; use
useIdfor ids; pass timestamps and locale from the server; usesuppressHydrationWarningonly for a single unavoidable text value; or load browser-only widgets without server rendering.What the interviewer is checking: Understanding of the server/client rendering contract and concrete debugging habits.
Likely follow-up: A price shows the correct currency on the server but flashes a different one after load. Where do you look first?
27You are building a reusable Tabs or Modal component for many teams. How do you design its API?Advanced
Prefer composition over configuration. Instead of a dozen boolean and string props, expose parts that callers arrange themselves, such as
Tabs,Tabs.ListandTabs.Panel, sharing state through context internally. When callers need full control over rendering, offer a headless hook or a render prop.Support both controlled use (
valueplusonChange) and uncontrolled use (defaultValue). Forward refs and spread remaining props onto the root element, and build in accessibility: roles, keyboard navigation and focus management. Use TypeScript discriminated unions so invalid prop combinations do not compile.The pitfall is a growing surface of flags like
variant,isCompactandhasFooter, which is hard to test and impossible to extend without breaking changes. Keep the public API small and stable, and document it with examples.What the interviewer is checking: Design judgement for shared components: composition, controlled and uncontrolled modes, accessibility.
Likely follow-up: How would you let consumers customise styling without exposing every internal class name?
Fill the gaps with a free course
If one of these React answers felt shaky, these 3 courses cover the same ground step by step: React, Next.js, JavaScript Fundamentals.
More interview questions
- Java
- Spring Boot
- Python
- JavaScript
- Node.js
- C++
- Data Structures & Algorithms
- SQL & Databases
- System Design
- DevOps
- AWS & Cloud
- Security
- Generative AI & LLMs
- Machine Learning
- Behavioral & HR
Practising React for a real opening? Upload your resume to see which open jobs list React skills you already have, or browse open roles.