Interview prep ยท questions
React interview questions
26 questions, from core to senior. Say your answer out loud first, then open the model answer and check it against the points a strong answer covers.
What does it mean when React renders a component, and what causes a component to re-render?
Show the model answer
Rendering means that React calls the component function to get a description of the UI. It does not mean that the DOM changes. The work has two phases. In the render phase React calls the components and compares the new result with the previous one. This phase must be pure, with no side effects, because React can run it more than once. In the commit phase React applies the needed changes to the DOM and then runs the effects. After the first render, a component renders again for three reasons: its own state changed, its parent rendered, or a context that it reads changed. A new prop does not start a render by itself. Props change because the parent rendered. So a render does not always change the page: if the output is the same, React does not touch the DOM.
A strong answer covers
- Says rendering is calling the component function, not updating the DOM
- Separates the render phase (pure) from the commit phase (DOM changes, then effects)
- Names the three triggers: own state, parent render, context change
- Notes that a render with the same output changes nothing in the DOM
Likely follow-up
A parent re-renders but the props of its child are the same. Does the child render again, and how can you avoid it?
How does React decide what to change in the DOM after a component re-renders?
Show the model answer
React compares the new element tree with the tree from the previous render and changes only what is different. This process is called reconciliation. React uses two rules. First, it looks at the element type. If the element at the same position has the same type, React keeps the DOM node and the state and only updates the props. If the type is different, React removes the old subtree with its state and builds a new one. That happens when a div becomes a section, or when one component replaces another. Second, in a list of children React uses the key to match old and new items. With these two rules the comparison takes linear time, and React does not re-create the whole DOM. One practical result: a component defined inside another component is a new type on every render, so it loses its state each time.
A strong answer covers
- Says React compares element trees and applies only the differences
- Same type at the same position: keeps the DOM node and state, updates props
- Different type: removes the old subtree with its state and mounts a new one
- Says keys match the children of a list between renders
- Gives a practical result, such as the bug with a component defined inside a component
Likely follow-up
How can you make React reset the state of a component on purpose?
A new todo is added at the top of this list. What goes wrong, and what should the key be?
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input type="checkbox" /> {todo.text}
</li>
))}
</ul>
);
}
Show the model answer
The tick appears next to the wrong todo, and the key should be a stable id from the data. A key tells React which list item is which between renders. Here the key is the array index, so it belongs to the position and not to the todo. When a new todo is added at the top, every todo moves down by one, but the keys 0, 1, 2 stay in place. React thinks that the first item is the same as before and only changes its text. The checkbox state stays with the position, so the tick now stands next to the wrong todo. Deleting and sorting cause the same problem. A good key is a database id or another stable id, and it must be unique among its siblings. The index is acceptable only for a static list whose items have no state.
A strong answer covers
- Says a key gives each item a stable identity between renders
- Explains that an index key follows the position, not the item
- Shows the result: state such as a ticked checkbox stays with the wrong item
- Names a good key: a stable id from the data, unique among siblings
- Knows when the index is acceptable: a static list with no state in the items
Likely follow-up
The items come from an API without an id field. What do you use as the key?
What is the difference between props and state, and how do you decide where state should live?
Show the model answer
Props are inputs that a component receives from its parent, and it must not change them. State is data that a component owns and changes over time through a setter. Data flows down the tree, so each piece of state needs one owner: the closest common parent of the components that use it. When two siblings need the same value, I lift the state up to their parent and pass down the value and a function that changes it. I also keep state minimal. If a value can be calculated from props or other state, I calculate it during render and do not store it. For example, I store the list and the filter text, and I compute the filtered list in the render. If I stored the filtered list too, the two copies could become different, and I would need an effect to keep them equal.
A strong answer covers
- Says props are read-only inputs and state is owned by the component
- Gives each piece of state one owner: the closest common parent
- Explains lifting state up: pass the value and a change function down
- Computes derived values during render; does not store and sync them
Likely follow-up
The derived value is expensive to calculate on every render. What do you do?
The user clicks the button once. What does the console print, and what number does the button show? Explain why.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
console.log(count);
}
return <button onClick={handleClick}>{count}</button>;
}
Show the model answer
The console prints 0 and the button shows 1. Calling setCount does not change the count variable. It asks React to render again with a new value. Inside this handler, count is a constant that belongs to the current render, so it is 0 in all three calls and in the log. Each call says: set the state to 1. React also batches updates. It waits until the handler finishes and then renders once, not three times. To get 3, I pass an updater function: setCount(c => c + 1). React puts the updaters in a queue and gives each one the result of the previous one. I use this form whenever the next state depends on the previous state. Since React 18, batching also works inside promises, timeouts, and native event handlers, not only in React event handlers.
A strong answer covers
- Answers 0 in the console and 1 on the button
- Explains that count is a fixed value for the current render
- Says updates are batched into one render after the handler ends
- Fixes it with the updater form, setCount(c => c + 1)
- Knows that since React 18 batching also covers promises and timeouts
Likely follow-up
How can you force React to apply a state update immediately, for example to measure the DOM right after it?
What is useEffect for, and when does an effect run?
Show the model answer
useEffect lets a component synchronise with something outside React: a network connection, a browser API, a timer, or a third-party widget. The effect runs after React has committed the render to the DOM, usually after the browser has painted. The dependency array controls when it runs again. With no array, it runs after every render. With an empty array, it runs only after the first render. With values in the array, it runs again when one of them has changed. The function that the effect returns is the cleanup. React calls it before the effect runs again and when the component unmounts. For example, an effect connects to a chat room and the cleanup disconnects, so a new roomId closes the old connection first. In development, Strict Mode runs setup and cleanup one extra time on mount, to show a missing cleanup.
A strong answer covers
- Describes effects as synchronising with a system outside React
- Says effects run after the commit, not during render
- Explains the three cases: no array, empty array, listed dependencies
- Explains cleanup: before the next run and on unmount
- Mentions the extra Strict Mode run in development and why it exists
Likely follow-up
What is the difference between useEffect and useLayoutEffect, and when do you need useLayoutEffect?
This timer shows 1 and never goes higher. What is wrong, and which other mistakes with effects do you see often?
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <p>{seconds}</p>;
}
Show the model answer
This is a stale closure caused by a missing dependency. The effect reads seconds, but the dependency array is empty, so it runs once. The interval callback keeps the value from the first render, 0, so every tick sets the state to 1. The best fix is the updater form, setSeconds(s => s + 1), because then the effect does not read seconds at all. Adding seconds to the dependencies also works, but restarts the interval every second. I keep the exhaustive-deps lint rule on, because it reports this mistake. The second mistake is an infinite loop: the effect sets state and depends on an object or function that is new on every render. I depend on primitive values or create the object inside the effect. The third mistake is an effect that is not needed: a derived value belongs in render, and click logic belongs in the event handler.
A strong answer covers
- Names the stale closure: the callback keeps the value from the first render
- Fixes it with the updater form or with correct dependencies
- Explains the infinite loop from object or function dependencies
- Says derived values and click logic do not need an effect
- Keeps the exhaustive-deps lint rule on and does not silence it
Likely follow-up
An effect must read the latest value of a prop but must not run again when that prop changes. What are your options?
userId changes quickly from 1 to 2, and sometimes the page shows user 1. Why does this happen, and how do you fix it?
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then((data) => setUser(data));
}, [userId]);
return <h1>{user?.name}</h1>;
}
Show the model answer
This is a race condition. Two requests run at the same time, and nothing guarantees that the responses arrive in the same order. If the response for user 1 arrives last, it replaces user 2 on the screen. I fix it in the effect cleanup. One way is a flag. I declare let ignore = false in the effect, and the cleanup sets it to true. The callback updates state only when ignore is false. The other way is an AbortController: I pass its signal to fetch and call abort in the cleanup, which also cancels the request. The effect also needs loading and error states. In a real project I usually use a data library such as TanStack Query or RTK Query. It handles races, caching, deduplication, and retries, which a hand-written effect does not.
A strong answer covers
- Names the race condition: responses can arrive out of order
- Fixes it in the cleanup with an ignore flag or an AbortController
- Adds loading and error states
- Says a data library handles caching, deduplication and retries
Likely follow-up
What does AbortController give you that the ignore flag does not?
What is useRef for, and how is a ref different from state?
Show the model answer
useRef returns an object with a current property that React keeps for the whole life of the component. Changing it does not cause a render. I use it for two things. The first is access to a DOM node: I pass the ref to an element, and then I can focus it, measure it, or scroll to it. The second is a mutable value that the UI does not show, such as a timer id or an AbortController. State is different: setting state triggers a render, and the value is fixed during each render. The rule is that I do not read or write ref.current during render, because render must be pure. I use refs in event handlers and effects. In React 19 a function component receives ref as a normal prop, so forwardRef is no longer needed.
A strong answer covers
- Says a ref keeps a value between renders and does not trigger a render
- Gives both uses: DOM access and mutable values such as a timer id
- Contrasts with state, which triggers a render and is fixed per render
- Does not read or write ref.current during render
- Knows that ref is a normal prop in React 19
Likely follow-up
When would you use a callback ref and not a ref object?
What is the difference between useMemo and useCallback, and when is it worth using them?
Show the model answer
useMemo caches the result of a calculation, and useCallback caches a function. Both return the cached value until a dependency changes, and useCallback(fn, deps) does the same as useMemo(() => fn, deps). They have a cost: React stores the value and compares the dependencies on every render, and the code is harder to read. I use them in three cases. First, a calculation is really slow, for example sorting thousands of rows, and I have measured it. Second, the value is a prop of a child wrapped in React.memo, where a new object or function on every render makes the memo useless. Third, the value is a dependency of an effect and must stay stable. They only improve performance, so the code must be correct without them. The React Compiler can memoize automatically when a project enables it.
A strong answer covers
- Says useMemo caches a value and useCallback caches a function
- Names the cost: stored values, dependency checks, harder code
- Gives the three cases: slow calculation, memoized child props, effect dependencies
- Says the code must work without them, and measures before optimising
- Mentions the React Compiler as automatic memoization when it is enabled
Likely follow-up
You wrapped a handler in useCallback, but the child still re-renders. What do you check?
Chart is wrapped in memo, but it re-renders on every key press in the input. Why, and how do you fix it?
const Chart = memo(function Chart({ data, options, onPick }) {
/* slow to render */
});
function Dashboard({ data }) {
const [search, setSearch] = useState("");
return (
<>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<Chart data={data} options={{ animate: true }} onPick={(p) => log(p)} />
</>
);
}
Show the model answer
Chart renders again because two of its props are new on every render: the options object and the onPick function. React.memo skips a render only when every new prop is equal to the old one, and it compares them with Object.is. This is a shallow comparison, so an object or a function is equal only when it is the same reference. Each key press renders Dashboard, which creates both values again, so the memo never helps. I fix it by making the props stable. The options object can be a constant outside the component, and the handler can be wrapped in useCallback. Wrapping in memo helps when a component is slow to render and often receives the same props, such as a chart or a long list. It does not stop renders caused by the component's own state or by a context it reads.
A strong answer covers
- Says memo skips a render when props are shallowly equal (Object.is per prop)
- Finds the two unstable props: the inline object and the inline function
- Fixes them with a constant outside the component and useCallback
- Says memo does not block renders from own state or context
- Uses memo only for slow components that often get the same props
Likely follow-up
When would you pass a custom comparison function to memo, and what is the risk?
What problem does Context solve, and what should you know about its performance?
Show the model answer
Context passes a value to any component below a provider without sending it through every level as props. It solves prop drilling for data that many components need, such as the theme or the current user. The limit is that every component that reads a context renders again when the provider value changes. A component cannot subscribe to only one field. So I memoize the value with useMemo when it is an object. Otherwise it is a new object on every render of the component that owns the provider. I also split contexts, for example one for the data and one for the functions that change it. Context only transports a value. It is not a state manager: the state still lives in useState or useReducer. For global state that changes often, I use a store with selectors, such as Redux Toolkit or Zustand.
A strong answer covers
- Says context avoids passing props through many levels
- Says every consumer re-renders when the provider value changes
- Memoizes an object value so it is not new on every render
- Splits contexts by what changes often and what is stable
- Says context transports a value and is not a state manager
Likely follow-up
How would you let a component subscribe to only one field of a large shared state?
What is the difference between a controlled and an uncontrolled input, and when do you use each one?
Show the model answer
In a controlled input, React state is the source of truth. I pass value and onChange, and every key press updates the state and renders the component. In an uncontrolled input, the DOM keeps the value. I pass defaultValue and read the value when I need it, with a ref or from FormData on submit. I choose controlled inputs when the UI must react while the user types: live validation, formatting, or fields that depend on each other. Uncontrolled inputs need less code and fewer renders, so they fit simple forms where I only need the values on submit. An input must not switch between the two modes, so value must never become undefined. For large forms I use a library such as React Hook Form. It keeps the inputs uncontrolled by default and handles validation, so typing does not render the whole form again.
A strong answer covers
- Controlled: React state holds the value through value and onChange
- Uncontrolled: the DOM holds the value; read it with a ref or FormData
- Chooses controlled for live validation, formatting or dependent fields
- Knows an input must not switch modes (value must not become undefined)
- Explains what a form library adds: fewer renders and validation
Likely follow-up
How do form actions and useActionState in React 19 change the way you handle a submit?
What is wrong with this component, and why does React have this rule?
function Profile({ user }) {
const [editing, setEditing] = useState(false);
if (!user) {
return <p>Please sign in</p>;
}
useEffect(() => {
document.title = user.name;
}, [user.name]);
return <ProfileForm user={user} editing={editing} onEdit={setEditing} />;
}
Show the model answer
useEffect is called after an early return, so it runs on some renders and not on others. This breaks the first rule of hooks: call them only at the top level, never inside a condition, a loop, a nested function, or after a return. The second rule: call them only from React function components or custom hooks. The reason is the way React stores hook data. For each component it keeps a list, and it matches every hook call to its place by call order, not by name. If a render skips a hook, the later calls no longer match their data, and here React throws an error. I would move the effect above the return and check user inside it, or move the signed-in part into its own component. A custom hook shares logic, not state: every component that calls it gets its own state.
A strong answer covers
- Finds the hook that is called after the early return
- States both rules: top level only, and only in components or custom hooks
- Explains why: React matches hook data by call order
- Fixes it by moving the hook up or splitting the component
- Says a custom hook shares logic, and each caller gets its own state
Likely follow-up
Which React 19 API can read a context or a promise and may be called inside a condition?
What does this component show when messages is an empty array, and how do you write conditions in JSX safely?
function Inbox({ messages, userId }) {
return (
<section>
{messages.length && <MessageList messages={messages} />}
<ReplyForm userId={userId} />
</section>
);
}
Show the model answer
It shows the number 0 above the form. The && operator returns its left side when that side is falsy, and here the left side is 0. React renders nothing for false, null, and undefined, but it does render numbers, including 0. So the left side must be a real boolean, such as messages.length > 0, or I use a ternary with null. I use a ternary for two possible outputs, and an early return when the whole component has nothing to show, for example while data is loading. A related tool is the key. React keeps the state of a component while the same component stays at the same position in the tree. If ReplyForm must start empty for another user, I give it key={userId}. When the key changes, React removes the old form and mounts a new one, so the draft is cleared without an effect.
A strong answer covers
- Answers that 0 appears, because && returns the falsy left side
- Knows React skips false, null and undefined but renders the number 0
- Fixes it with a real boolean (length > 0) or a ternary
- Uses an early return when the whole component has nothing to show
- Uses a key to reset the state of a component
Likely follow-up
A ternary switches between two inputs of the same type at the same position. Does the typed text stay, and why?
What is an error boundary, and which errors does it not catch?
Show the model answer
An error boundary is a component that catches errors thrown while React renders the tree below it and shows a fallback UI instead. Without a boundary, an error during render removes the whole application from the screen. A boundary catches errors in rendering and in effects of its children. It does not catch errors in event handlers, in asynchronous code such as a timeout or a promise callback, during server rendering, or inside the boundary itself. For those I use try and catch and put the error into state. There is no hook for this. A boundary must be a class component with getDerivedStateFromError or componentDidCatch, so most teams use the react-error-boundary library. I place one boundary at the top of the app and more around independent parts, such as a route or a widget. Then one failure does not break the whole page.
A strong answer covers
- Says it catches errors from rendering and effects in the tree below and shows a fallback
- Lists what it misses: event handlers, async callbacks, server rendering, its own errors
- Handles those other errors with try and catch and error state
- Knows it needs a class component or the react-error-boundary library
- Places boundaries at the root and around independent parts
Likely follow-up
A request fails inside an event handler. How do you show that error in the same fallback UI?
Why does React prefer composition to inheritance, and which composition patterns do you use?
Show the model answer
React reuses UI by composition: a component receives other components as children or as props and decides where to place them. I do not extend a base component class. Inheritance hides where data comes from, and composition keeps the data flow visible in the JSX. For example, a Card renders its children, so it can wrap any content. For parts that work together, such as Tabs with a tab list and panels, I use compound components. The parent owns the state and shares it through context. To share logic without UI, custom hooks have replaced most render props and higher-order components. A render prop still fits when the caller decides what a child renders, such as a list row. Composition also reduces prop drilling. I create the Avatar element at the top and pass it down as children, so the components between do not need the user data.
A strong answer covers
- Explains composition with children and with elements passed as props
- Says why not inheritance: it hides where the data comes from
- Describes compound components that share state through context
- Says custom hooks replaced most render props and higher-order components
- Reduces prop drilling by passing a ready element down, not the data
Likely follow-up
How would you design the API of a reusable Tabs or Modal component for other teams?
A screen feels slow and you think a component re-renders too often. How do you find the cause, and what are the usual fixes?
Show the model answer
I find the cause with the Profiler in React DevTools before I change any code. I record the slow interaction and read which components rendered in each commit and how long each one took. A Profiler setting also records why each component rendered. Development mode is slower than production, so I confirm the numbers in a production profiling build. Then I choose the smallest fix. Often the state is too high in the tree, so I move it down to the component that uses it. Or I pass the slow part as children, so it does not render again. If a slow child gets the same props, I wrap it in memo and keep its props stable. If a context changes often, I split it or use a store with selectors. If a list is very long, I virtualize it. After the fix I record again and compare.
A strong answer covers
- Measures with the React DevTools Profiler before changing code
- Reads which components rendered, how long they took, and why
- Moves state down or passes slow parts as children
- Uses memo with stable props, splits context, virtualizes long lists
- Knows development is slower than production, and measures again after the fix
Likely follow-up
The Profiler shows that React renders fast, but the interaction is still slow. Where do you look next?
How do you split the JavaScript bundle of a React app, and how do you decide what to split?
Show the model answer
I split with dynamic import. React.lazy takes a function that calls import() and gives back a component that loads when it first renders. A Suspense boundary above it shows a fallback while the file downloads, and the bundler puts that code in a separate chunk. I start with route-level splitting, because a user opens one page at a time. Frameworks such as Next.js do this automatically. After that I split heavy code that most users do not need at the start, such as a chart library or a rich text editor. Small components are not worth splitting, because every chunk costs a request and can show a loading state. To hide the delay, I preload the chunk on hover or focus of a link, by calling the same import() early. A chunk can fail to load, so I put an error boundary around lazy parts.
A strong answer covers
- Uses React.lazy with a dynamic import and a Suspense fallback
- Starts with route-level splitting
- Splits heavy code that few users need at the start; not small components
- Preloads on hover or other intent by calling the import early
- Handles a failed chunk load with an error boundary
Likely follow-up
After a deployment, some users see an error that a chunk failed to load. Why does it happen, and how do you handle it?
How do you decide where a piece of state should live in a React app and which tool to use for it?
Show the model answer
I decide by the kind of state, because each kind has a natural place. Local UI state, such as an open menu or a form field, stays in useState or useReducer inside the component that needs it. State that must survive a reload or be shared in a link, such as filters or the page number, goes into the URL as search params. Server state, the data that the backend owns, goes into a library such as TanStack Query or RTK Query, which handles caching and refetching. What is left is global client state, such as a shopping cart. For that I use Context when the value changes rarely. When it changes often or has complex update logic, I use a store such as Redux Toolkit or Zustand. A common mistake is to put everything into one global store. Without server data and URL state, that store is usually small.
A strong answer covers
- Classifies state as local, URL, server or global client state
- Keeps local state in the component with useState or useReducer
- Puts shareable state such as filters into the URL
- Uses a server-state library for backend data
- Uses Context for rare changes and a store for frequent or complex ones
Likely follow-up
A team keeps all API data in Redux slices with manual loading flags. What would you change, and how would you migrate?
Why is server state different from client state, and how does a library such as TanStack Query or RTK Query handle it?
Show the model answer
Server state is a copy of data that the backend owns, so it can be out of date at any moment. Client state, like an open modal, belongs to the browser and is always up to date. A copy needs a cache, a rule for when data is stale, deduplication of equal requests, and invalidation after a write. In TanStack Query every query has a key, such as ['todos', filters]. The key identifies the cache entry, so it must contain every variable of the request. The staleTime option says how long data counts as fresh. The default is zero, so on the next mount or window focus the library shows cached data and refetches in the background. After a mutation I invalidate the matching keys. RTK Query uses tags for this: queries provide them and mutations invalidate them. I can also update the cache optimistically and roll back on failure.
A strong answer covers
- Says server state is a copy of remote data that can become stale
- Lists what is needed: cache, deduplication, background refetch, invalidation
- Explains query keys as the identity of a cache entry
- Explains stale time and what happens with the default of zero
- Describes invalidation after a mutation and optimistic updates with rollback
Likely follow-up
How do you build an optimistic update for a like button, and what happens when the request fails?
What is the difference between client-side rendering, server-side rendering, and static generation, and what are the trade-offs?
Show the model answer
The difference is where and when the HTML is built. With client rendering, the server sends an almost empty page and JavaScript builds the UI in the browser. It is simple, but the first load is slower and SEO is weaker, so it fits apps behind a login. With server rendering, the server builds the HTML for every request. Content appears sooner and SEO is good, but each request costs server time. With static generation, the HTML is built once at build time and served from a CDN. It is the fastest, but the data is only as fresh as the last build. Incremental regeneration, a Next.js feature, rebuilds a static page after a set time or on demand. Server and static pages are then hydrated: React attaches event handlers, and until then the page is visible but not interactive. Streaming with Suspense sends the page shell first and slow parts later.
A strong answer covers
- Defines each one by where and when the HTML is built
- Gives the trade-offs: first load, SEO, data freshness, server cost
- Explains incremental regeneration: static pages rebuilt after a time or on demand
- Explains hydration and the time when a page is visible but not interactive
- Mentions streaming with Suspense for slow parts of a page
Likely follow-up
What is a hydration mismatch, what causes it, and how do you fix it?
What are React Server Components, and how do they differ from client components?
Show the model answer
A Server Component runs only on the server or at build time, and its code is never sent to the browser. The browser receives the rendered result, not the component. So it can be async, read a database directly, and use a large library without adding to the bundle. That is the main benefit: less JavaScript to download and hydrate. A Server Component has no state, no effects, no event handlers, and no browser APIs. For interactive parts I put the 'use client' directive at the top of a file. That file and everything it imports are then part of the client bundle. A Server Component can pass props to a client component, but they must be serializable, so no ordinary functions. A client component cannot import a Server Component, but it can receive one as children. In the Next.js App Router, components are Server Components by default.
A strong answer covers
- Says Server Components run on the server and their code is not sent to the browser
- Names the benefit: direct data access and less JavaScript to download and hydrate
- Lists the limits: no state, no effects, no event handlers, no browser APIs
- Explains 'use client' as a boundary for a file and everything it imports
- Knows props across the boundary must be serializable; server parts pass as children
Likely follow-up
How are Server Components different from classic server-side rendering?
A search input filters a large list, and typing feels slow. Which React features keep the input responsive?
Show the model answer
useTransition and useDeferredValue let React treat the slow list update as not urgent, so typing stays fast. The input text must update at once, but the filtered list can wait. With useTransition I wrap the slow state update in startTransition. React renders it in the background, can interrupt it when a new key press arrives, and gives me isPending for a loading hint. With useDeferredValue I pass a deferred copy of the value to the slow list. The copy keeps the old value during the urgent render, and React updates it in the background. In both cases the slow list should be wrapped in memo, so React can skip it during the urgent render. Unlike debouncing, there is no fixed delay. Also, React batches state updates automatically, and during a transition Suspense keeps the visible content and does not show the fallback again.
A strong answer covers
- Separates the urgent update (the input) from the non-urgent one (the list)
- Explains useTransition: a background render that can be interrupted, plus isPending
- Explains useDeferredValue: a copy of a value that updates later
- Says the slow child needs memo for this to help
- Compares with debouncing; mentions automatic batching and Suspense
Likely follow-up
When would you still choose debouncing and not useDeferredValue?
How does Redux Toolkit work, and when is Redux too much for a project?
Show the model answer
Redux Toolkit is the official, shorter way to write Redux. Redux keeps application state in one store. Components dispatch actions, reducers calculate the next state, and components read it through selectors. configureStore creates the store with good defaults, such as the thunk middleware and DevTools. createSlice takes a name, an initial state, and reducer functions, and it generates the action creators. Inside these reducers I can write code that looks like mutation, such as state.items.push(item). Toolkit runs reducers through Immer, which records the changes on a draft and produces a new immutable state. For async logic, createAsyncThunk dispatches pending, fulfilled, and rejected actions that I handle in extraReducers. For server data I prefer RTK Query, which generates hooks and manages the cache. Components read with useSelector, and createSelector memoizes derived data. Redux is too much for a small app, or when most state is server data or local UI state.
A strong answer covers
- Describes the flow: dispatch an action, a reducer makes new state, a selector reads it
- Explains createSlice and why reducers can mutate: Immer works on a draft
- Compares createAsyncThunk (manual async logic) with RTK Query (fetching and caching)
- Uses memoized selectors with useSelector
- Says when Redux is too much: small apps, or mostly server and local state
Likely follow-up
A useSelector call returns state.items.filter(...) and the component re-renders after every action. Why, and how do you fix it?
How do you render a list of ten thousand rows without making the page slow?
Show the model answer
I render only the rows that are visible, plus a few above and below. This is called virtualization or windowing. The list has a container with the full scroll height. From the scroll position the library calculates which rows are inside the viewport, renders only those, and places them at the right offset. The DOM then holds a few dozen nodes, not ten thousand, and scrolling stays fast. I use a library such as TanStack Virtual or react-window. There are trade-offs. Find in page cannot find rows that are not in the DOM. Screen readers need extra ARIA attributes to know the full size of the list. Rows with variable height must be measured, which adds complexity. So I first ask if the user needs all rows at once. Pagination or a load more button is simpler and more accessible, so I prefer it when continuous scrolling is not required.
A strong answer covers
- Renders only the visible rows plus a small buffer
- Explains the mechanism: full-height container and rows placed by offset
- Names a library such as TanStack Virtual or react-window
- Lists trade-offs: find in page, accessibility, variable heights
- Considers pagination or a load more button as the simpler option
Likely follow-up
How do you virtualize rows when you do not know their height before they render?