React Interview Questions: Hooks, Rendering, and Effects Quiz
Reviewed by Mark Dickie · Last updated
4 questions on this page you can answer and see marked on the spot. Go to the first one
React is a JavaScript library for building user interfaces out of components that hold their own state. Interviewers mostly test how data flows down a component tree, when a re-render happens, and how side effects stay in sync with that data. Expect to explain useState and useEffect, why list items need stable keys, and how React reconciles its element tree with the DOM. Most lost points come from effect dependencies and stale closures.
| Topic | What interviewers check |
|---|---|
| useState updates | Functional updates like setCount(c => c + 1), and why reads right after a setter still show the old value |
| useEffect dependency array | Missing dependencies that leave stale values, and cleanup functions for subscriptions and timers |
| Keys in lists | Why index keys break when items are reordered, inserted, or removed |
| useMemo and useCallback | When memoization prevents re-renders, and when it adds cost with no measurable gain |
| useTransition and useDeferredValue | Keeping typing responsive while a slow list re-renders |
| Actions and useActionState (React 19) | How form actions track pending and error state without manual flags |
How do you practise React for an interview?
- Build a todo list, then add a filter without re-rendering every row. Explain which props change and why memo and stable keys matter.
- Write a component that fetches by an id prop with useEffect. Handle the case where the id changes before the first request finishes, and add a cleanup.
- Rebuild a sign-up form twice, once as a controlled input and once with a React 19 action and useActionState, then compare the code paths.
- Trace out loud what happens on each render when a context value changes and a child reads it through useContext.
Key facts
- Tarmac has 214 React interview questions on this topic, 10 of them on this page, at difficulty 1–4 of 5.
- Tarmac tracked 2,281 job postings asking for React in October 2026.
- Roles asking for React advertise a median base salary of £77,000, across 561 job postings as of October 2026.
- Tarmac last reviewed these React interview questions on 2 November 2026.
At a glance
| Questions | 10 shown · 214 in the bank |
|---|---|
| Difficulty | 1–4 of 5 |
| Formats | Multiple choice, Fill in the blank, Code output, Flashcard, Short answer, Find the bug, Multiple answer, Ordering, True / false, Coding exercise |
| Interactive | 1 run your code against tests, in the app |
What you'll review
- usecontext
- external stores
- reconciliation
- lifting state
- keys
- useeffect deps
- effect races
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
Consider the following component tree:#
Options
Show answer
ThemedButton logs once per click — React.memo does not prevent the re-render. The Provider's value={{ theme, setTheme }} creates a fresh object literal on every App render, so useContext sees a changed reference and triggers a re-render of every consumer regardless of whether theme actually changed. React.memo only guards against prop-based bails, but the re-render here is driven by the context subscription, not by props.
When a context value changes, React re-renders every component that calls useContext with that context — regardless of whether the specific slice of data the component uses has changed. This is because useContext performs a reference equality check on the entire context value object. If the Provider's value is a new object literal on every parent render (e.g., value={{ user, setUser }}), all consumers re-render even if user hasn't changed. The only built-in escape hatch is to memoize the value with useMemo (or split contexts). React.memo on the consumer does NOT help because useContext bypasses the props-based bailout — the re-render is triggered by the context subscription, not by prop changes.
Complete the useSyncExternalStore call below so the component correctly subscribes to myStore.#
Show answer
Complete the useSyncExternalStore call below so the component correctly subscribes to myStore.
import { **useSyncExternalStore** } from 'react';
function Counter() {
const count = **useSyncExternalStore**(
myStore.**subscribe**,
myStore.getSnapshot
);
return <div>{count}</div>;
}
The hook is useSyncExternalStore and its first argument is a subscribe function that React calls to register a callback notified on store changes. The second argument is getSnapshot, which returns the current store value. Filling in useSyncExternalStore for {{0}} and subscribe for {{1}} produces a correctly wired external-store subscription.
What is rendered to the screen (as visible UI text) the first time this component mounts in a standard React 18 production environment (no Strict Mode)?#
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
console.log('render');
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count)}>Click me</button>
</div>
);
}
export default Counter;Show answer
0
Click me
On the initial mount the component renders once. useState(0) initialises count to 0, so <p>{count}</p> displays 0 and the button displays its label Click me. The question asks only about the visible DOM output on first mount, which is unambiguous regardless of Strict Mode or bailout behaviour: the user sees 0 and the button text Click me.
What is the difference between state and props?#
Show answer
Props are inputs passed in by a parent and are read-only to the receiving component. State is data the component owns and can change over time (via its setter), triggering a re-render.
Data flows down as props; a component manages its own state. Lifting state up means moving shared state to a common ancestor and passing it back down as props.
Why does React need a key on list items, and what makes a good key?#
Show answer
Keys give each list item a stable identity so React can match elements across renders during reconciliation, preserving their state and minimizing DOM work. A good key is stable and unique among siblings — typically a data id — not the array index when the list can reorder.
Without stable keys React falls back to index matching, which moves state to the wrong items when the list changes. Keys are about identity, not ordering.
The AuthProvider below causes all context consumers to re-render unnecessarily on every parent render, even when user and dispatch haven't changed. Identify the buggy line.#
function AuthProvider({ children }) {
const [user, dispatch] = React.useReducer(authReducer, null);
const value = { user, dispatch };
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}Show answer
The bug is on line 4.
The bug is on line 4: const value = { user, dispatch }; creates a new object reference on every render of AuthProvider. Because useContext compares context values by reference (Object.is), every consumer re-renders whenever AuthProvider re-renders for any reason — even if user and dispatch haven't changed. The fix is to wrap the value in useMemo: const value = React.useMemo(() => ({ user, dispatch }), [user, dispatch]);. The dispatch function from useReducer is stable across renders, and user changing is a legitimate reason to re-render consumers, so the memoized value only changes when it should.
You have a large React application using a single AppContext that holds both state (frequently updated) and dispatch (stable). Many components deep in the tree only need to call dispatch but are re-rendering on every state change because they consume AppContext via useContext.#
Options
Pick every one that applies.
Show answer
Split the single AppContext into two separate providers: an AppStateContext for state and an AppDispatchContext for dispatch. Wrap the dispatch value in useMemo(() => dispatch, [dispatch]) or set it once, since useReducer's dispatch holds a stable reference across renders. Components that only call dispatch then consume the dispatch context and never re-render when state changes, because their subscribed context value never changes. Keeping one combined context re-renders every consumer on any state update, even when the unused half of the value is memoized.
The correct pattern for avoiding unnecessary re-renders with complex context state is to split the context into a state context and a dispatch/setter context. Dispatch from useReducer is guaranteed stable (same reference every render), so components that only need to dispatch can consume the dispatch context and will never re-render due to state changes. Components that need state consume the state context and only re-render when state changes. Using a single combined context, even with useMemo, still re-renders all consumers on any state change. Using Redux or external state is not a React context technique. Passing values via props defeats the purpose of context. The multi-select correct answers are: splitting into separate state and dispatch contexts, and memoizing expensive child subtrees with React.memo/useMemo where appropriate.
A component has the following effect:#
Put these in order
Show answer
The exact console output order is: effect 0, then cleanup 0, then effect 1, then cleanup 1. On mount, the effect runs once with count at 0. When count changes to 1, React runs the previous cleanup before firing the new effect. On unmount, the final cleanup runs. This before-next-effect ordering is what lets you cancel stale work such as an in-flight fetch.
The useEffect cleanup function returned from a prior effect run is called before the next effect fires (when deps change) and also on unmount. So the sequence is: mount → effect-1 runs; dep changes → cleanup-1 runs, then effect-2 runs; unmount → cleanup-2 runs. This ordering is critical for avoiding race conditions, for example cancelling a previous fetch before starting a new one. The AbortController pattern exploits this: the cleanup aborts the in-flight request from the previous render cycle.
Consider the following React code:#
Options
Show answer
Child will re-render every time the button is clicked, so the statement is False. Although items is stabilized by useMemo and passes React.memo's shallow comparison, the style prop is an inline object literal ({ color: 'red' }) that creates a new reference on every parent render. React.memo performs referential equality checks (===) on each prop, so the changed style reference causes Child to re-render despite the memo wrapper.
This question tests understanding of React's fiber bailout mechanism and referential equality. React.memo wraps Child and performs a shallow prop comparison. On the second render triggered by setCount, style is an object literal { color: 'red' } created inline. Even though its shape and values are identical to the previous render's object, it is a new reference each time — so the shallow comparison (===) fails, and Child re-renders. items is defined with useMemo and has no changing dependencies, so it retains the same reference and passes the memo check alone. But because style fails, Child re-renders regardless. The fix would be to also memoize style with useMemo or useCallback. The correct answer is that Child does re-render on every parent render, despite React.memo.
Effect races in React: when a useEffect fetches data keyed on a prop, each re-run starts a new request and its cleanup marks the previous request as stale. A stale response must not overwrite the displayed value.#
Starter code
function finalDisplayedValue(events) {
let shown = null;
for (const e of events) {
if (e.type === 'resolve') shown = e.value;
}
return shown;
}Your solution must pass
- latest request wins even though stale one resolves last
- single request resolves
This one is written and run, not read. Solve it in the app and your code is executed against these tests and the hidden ones.
Sources
The official documentation these questions are checked against:
Related interview questions
Job market
See react salaries and hiring demand from live job postings.
The other 204 questions
This page shows 10 and marks what you pick. That's as far as a page can go. A free account opens the other 204 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 answers per 30 days, each marked with written feedback