React Hooks Interview Questions and Answers
Short answer
Hooks questions cluster around four things: why hooks must be called in the same order every render, why state looks stale immediately after you set it, what the dependency array really controls, and when an effect is the wrong tool. The last one now separates mid from senior.
| Hook | What it is for | The question behind the question |
|---|---|---|
| useState | Local state that triggers a re-render | Batching, and the functional updater |
| useEffect | Synchronising with something outside React | Dependencies, cleanup, and not needing it |
| useRef | A mutable box that survives renders | That writing to it does not re-render |
| useContext | Reading context without prop drilling | That every consumer re-renders |
| useReducer | State with complex transitions | When it beats several useStates |
| useMemo / useCallback | Referential stability | That they are not free, and when they do nothing |
| useLayoutEffect | Reading layout before paint | That it blocks paint, so it is rarely right |
| Custom hooks | Sharing logic, not state | That two callers get two separate states |
What a hooks round is really testing
Hooks questions look like API questions and are not. Every one of them is checking whether you understand that a component function runs from scratch on every render, and that everything surviving between those runs is stored by React and handed back to you. Once that model is in place, the rules of hooks, stale closures, dependency arrays and the functional updater all stop being separate facts and become consequences of one thing.
The second axis, and the one that has moved most in the last two years, is restraint. React's own documentation now has a page called "You Might Not Need an Effect", and interviewers have read it. A candidate who reaches for `useEffect` to derive a value, to reset state when a prop changes, or to keep two pieces of state in sync will be asked why, and the honest answer is that none of those need an effect at all.
Why must hooks be called at the top level?
Because React identifies hooks by call order, not by name. Each component has a list of hook state slots, and on every render React walks that list in the order the hooks are called. Put a hook inside a condition and the order changes between renders, so slot two now returns what slot three held, which is why React throws rather than letting you find out at runtime.
1function Broken({ isLoggedIn }) {
2 if (isLoggedIn) {
3 const [name, setName] = useState(""); // slot 0 only on SOME renders
4 }
5 const [theme, setTheme] = useState("dark"); // slot 0 or 1, depending
6 // → "Rendered fewer hooks than expected" when isLoggedIn flips
7}
8
9function Fixed({ isLoggedIn }) {
10 const [name, setName] = useState(""); // always slot 0
11 const [theme, setTheme] = useState("dark");// always slot 1
12 if (!isLoggedIn) return null; // condition AFTER the hooks
13 return null;
14}
15
16// The same rule explains why a hook cannot go in a loop or a callback,
17// and why custom hooks must start with "use", that prefix is how the
18// linter knows to apply the rules inside them.
19// → eslint-plugin-react-hooks catches all of this at build timeThe follow-up is usually "how would you conditionally use state then?", and the answer is that you do not, you always call the hook and make the value conditional, or you extract the conditional part into a child component that either renders or does not. Splitting the component is the more idiomatic answer of the two and worth offering first.
Why is state not updated immediately after setState?
Because the state variable in the current render is a constant. Calling the setter schedules a re-render; it does not reassign the variable you are holding. React then batches every update triggered in the same event and re-renders once, and only in that next render does the variable have the new value.
1function Counter() {
2 const [count, setCount] = useState(0);
3
4 function handleWrong() {
5 setCount(count + 1);
6 setCount(count + 1);
7 setCount(count + 1);
8 console.log(count);
9 // → 0 the snapshot for THIS render never changes
10 // → final state is 1, not 3: all three computed 0 + 1
11 }
12
13 function handleRight() {
14 setCount((c) => c + 1);
15 setCount((c) => c + 1);
16 setCount((c) => c + 1);
17 // → final state is 3: each updater receives the latest value
18 }
19
20 // Setting state to the SAME value bails out of the re-render
21 function handleNoop() {
22 setCount(count);
23 // → React compares with Object.is and skips the render
24 }
25
26 return null;
27}
28// → since React 18, batching also applies inside promises, timeouts
29// → and native handlers, not just React event handlersThe rule to state plainly: whenever the next state depends on the previous state, pass a function. It is the same rule that fixes rapid clicks, updates inside a loop, and any setter called from an async callback that captured an old value. And the React 18 detail, that automatic batching was extended to promises, timeouts and native event handlers, is a good thing to know, because it changed the behaviour of real code on upgrade.
What does the useEffect dependency array actually do?
It tells React which values the effect used, so it can decide whether to run it again. After each render React compares every entry with the previous render's using `Object.is`; if any differ, it runs the cleanup from last time and then the effect again. No array means run after every render, an empty array means only after the first.
1useEffect(() => { /* after EVERY render */ });
2
3useEffect(() => { /* after the first render only */ }, []);
4
5useEffect(() => { /* when userId changes */ }, [userId]);
6
7useEffect(() => {
8 const controller = new AbortController();
9 let ignore = false;
10
11 fetch("/api/users/" + userId, { signal: controller.signal })
12 .then((r) => r.json())
13 .then((data) => { if (!ignore) setUser(data); });
14
15 return () => { // cleanup: runs BEFORE the next effect
16 ignore = true; // and on unmount
17 controller.abort();
18 };
19}, [userId]);
20// → switching users quickly: the old request is aborted and ignored,
21// → so a slow first response cannot overwrite a fast second one
22
23// The object trap: a new reference every render means the effect never stops
24// useEffect(() => {...}, [{ id: 1 }]); // → runs on EVERY render
25// useEffect(() => {...}, [id]); // → pass the primitive insteadThe cleanup function is where most marks are available, because it answers three questions at once, how you cancel a request, how you avoid setting state after unmount, and how you handle out-of-order responses. The `ignore` flag is the part people miss: aborting stops the request, but the flag is what guarantees a response that arrived first cannot overwrite a newer one.
What is a stale closure and how do you fix it?
Every render creates new functions that close over that render's values. If a function outlives the render, inside a `setInterval`, an event listener, or an effect with an incomplete dependency array, it keeps using the values it captured, not the current ones. The symptom is a counter that stops at one, or a handler that reads a prop from three renders ago.
1function Broken() {
2 const [count, setCount] = useState(0);
3 useEffect(() => {
4 const id = setInterval(() => setCount(count + 1), 1000);
5 return () => clearInterval(id);
6 }, []);
7 // → 1, then 1, then 1 forever: the interval captured count === 0
8 return count;
9}
10
11// Fix 1: the functional updater, no dependency on count at all
12useEffect(() => {
13 const id = setInterval(() => setCount((c) => c + 1), 1000);
14 return () => clearInterval(id);
15}, []);
16// → 1, 2, 3, ...
17
18// Fix 2: list the dependency, accepting the interval is recreated each tick
19useEffect(() => {
20 const id = setInterval(() => setCount(count + 1), 1000);
21 return () => clearInterval(id);
22}, [count]);
23// → correct, but tears down and rebuilds the timer every second
24
25// Fix 3: a ref for values you want to read but not react to
26const latest = useRef(count);
27latest.current = count;
28// → read latest.current inside the callback; it is never staleBeing able to give all three and say which you would pick is the complete answer. The functional updater is best when you only need the previous value. Listing the dependency is correct but recreates the effect. The ref is the escape hatch for values you want to read without re-subscribing, the pattern React's own docs describe for event handlers that should not restart a subscription.
This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399When should you not use useEffect?
Whenever the thing you are doing is not synchronising with something outside React. Effects exist to reach out to the world, a subscription, a DOM measurement, an analytics call, a non-React widget. Using one to compute a value from props, to reset state when a prop changes, or to run logic that belongs in an event handler creates an extra render pass and a whole class of bugs.
1// 1. Deriving state - just compute it during render
2const [full, setFull] = useState("");
3useEffect(() => { setFull(first + " " + last); }, [first, last]);
4// → renders twice, and full is briefly wrong
5const fullName = first + " " + last;
6// → one render, never out of sync. Wrap in useMemo only if it is expensive.
7
8// 2. Resetting state when a prop changes, use a key instead
9useEffect(() => { setDraft(""); }, [userId]);
10// → <Editor key={userId} /> remounts and resets ALL its state, for free
11
12// 3. Handler logic in an effect, put it in the handler
13useEffect(() => {
14 if (submitted) { track("signup"); postToApi(form); }
15}, [submitted]);
16// → do it where the event happens:
17// function onSubmit() { track("signup"); postToApi(form); }
18
19// What an effect IS for:
20useEffect(() => {
21 const onResize = () => setWidth(window.innerWidth);
22 window.addEventListener("resize", onResize);
23 return () => window.removeEventListener("resize", onResize);
24}, []);
25// → subscribing to something outside React, with cleanupThe `key` trick in the second example is the one that impresses, because most candidates have never used it deliberately. Changing a component's key tells React this is a different component instance, so it unmounts the old one and mounts a fresh one with clean state, which is a one-word solution to a problem people usually solve with three effects and a bug.
What is useRef for, beyond DOM access?
A ref is a mutable box that persists across renders and does not trigger one when you write to it. That second half is the whole point and the part interviews probe. Anything you need to remember but not render, a timer id, the previous value of a prop, a mutable instance of a third-party library, whether this is the first render, belongs in a ref rather than in state.
1function Search() {
2 const inputRef = useRef(null); // DOM node
3 const timerRef = useRef(null); // a value that must survive renders
4 const isFirst = useRef(true);
5 const [query, setQuery] = useState("");
6 const prevQuery = useRef(query);
7
8 useEffect(() => {
9 if (isFirst.current) { isFirst.current = false; return; }
10 console.log("changed from", prevQuery.current, "to", query);
11 prevQuery.current = query;
12 // → skips the mount, logs only real changes
13 }, [query]);
14
15 function onChange(e) {
16 clearTimeout(timerRef.current);
17 timerRef.current = setTimeout(() => setQuery(e.target.value), 300);
18 // → writing to a ref does NOT re-render, which is exactly what we want
19 }
20
21 useEffect(() => { inputRef.current?.focus(); }, []);
22 // → current is null until after the first render commits
23 return null;
24}How do you write a custom hook?
A function whose name starts with `use` and which calls other hooks. That is the entire definition, there is no special registration. The subtlety worth stating is what a custom hook shares: it shares stateful logic, not state. Two components calling the same hook get two completely independent pieces of state, which is the difference between a hook and context.
1function useDebounced(value, delay = 300) {
2 const [debounced, setDebounced] = useState(value);
3
4 useEffect(() => {
5 const id = setTimeout(() => setDebounced(value), delay);
6 return () => clearTimeout(id); // each keystroke cancels the last
7 }, [value, delay]);
8
9 return debounced;
10}
11
12function useLocalStorage(key, initial) {
13 const [value, setValue] = useState(() => {
14 try { return JSON.parse(localStorage.getItem(key)) ?? initial; }
15 catch { return initial; }
16 // → lazy initialiser: the function runs ONCE, not on every render
17 });
18
19 useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]);
20 return [value, setValue];
21}
22
23// Two components calling useDebounced get two separate timers and two
24// separate pieces of state, sharing the LOGIC, not the VALUE.
25// → to share the value, you need context or a storeThe lazy initialiser in `useLocalStorage` is worth pointing out unprompted. Passing `useState(JSON.parse(...))` would parse localStorage on every single render and throw the result away; passing a function makes React call it only on the first render. It is a small thing that appears in almost every real custom hook and is a clean signal of practical experience.
Which newer hooks should you know?
`useId` generates a stable id that matches between server and client, which is the correct way to link a label to an input in a reusable component, `Math.random()` produces a hydration mismatch. `useTransition` marks an update as non-urgent so typing stays responsive while an expensive list re-renders. `useDeferredValue` does something similar for a value rather than an update. `useSyncExternalStore` is how a state library subscribes safely in concurrent React.
You are unlikely to be asked to implement any of these, but naming `useId` for the hydration problem and `useTransition` for keeping input responsive during a heavy render shows you have followed React past hooks' first release. If the role mentions React 19, add `use`, which reads a promise or context and can be called conditionally, unlike every other hook, and the fact that the compiler is intended to remove much of the manual `useMemo` and `useCallback` work over time.
What loses marks in a hooks round
The failures here are about the model, not the API surface.
- →Saying state updates are asynchronous without explaining that the render's variable is a fixed snapshot.
- →Not reaching for the functional updater when the next state depends on the previous one.
- →Suppressing the exhaustive-deps lint warning instead of fixing the cause.
- →Never mentioning the cleanup function when asked about useEffect.
- →Using an effect to derive state that could be computed during render.
- →Sprinkling useMemo and useCallback everywhere, with no memoised child to benefit.
- →Thinking a custom hook shares state between the components that call it.
Frequently asked questions
- What are the rules of hooks and why do they exist?
- Call hooks only at the top level of a component or another hook, never inside conditions, loops or nested functions. React identifies each hook by its position in the call order, so a hook that sometimes runs shifts every later hook to the wrong slot. The eslint plugin enforces both rules at build time.
- Why is state not updated immediately after calling setState?
- The state variable in the current render is a constant snapshot. The setter schedules a re-render rather than reassigning it, and React batches every update in the same event into one render. The new value only exists in the next render, which is why logging straight after setting shows the old one.
- When should I use the functional updater form of setState?
- Whenever the next state depends on the previous one. setCount(c => c + 1) receives the latest value each time it runs, so three calls in one handler produce three increments, whereas setCount(count + 1) three times produces one. It is also the fix for setters called from timers or async callbacks.
- What does the useEffect dependency array do?
- It lists the values the effect reads so React can decide whether to run it again. After each render React compares each entry with the previous one using Object.is and re-runs the effect if any changed. No array means every render; an empty array means only after the first render.
- What is the useEffect cleanup function for?
- It runs before the next execution of the effect and when the component unmounts. Use it to remove event listeners, clear timers, close subscriptions and abort in-flight requests. Setting an ignore flag inside it also prevents a slow earlier response from overwriting a newer one.
- What is a stale closure in React?
- A function that outlives the render it was created in and keeps using that render's values, typically inside a setInterval, an event listener, or an effect with a missing dependency. Fix it with a functional state updater, by listing the dependency, or by reading the value from a ref.
- When should I not use useEffect?
- When you are not synchronising with something outside React. Deriving a value from props should happen during render, resetting state when a prop changes is better done by changing the component's key, and logic that belongs to a user action belongs in the event handler. Effects are for subscriptions, measurements and external systems.
- What is the difference between useRef and useState?
- Writing to a ref does not trigger a re-render and the value is available immediately; setting state schedules a re-render and the new value appears in the next one. Use state for anything the UI displays, and a ref for values you need to remember but not render, timer ids, previous values, DOM nodes.
- Do useMemo and useCallback improve performance by default?
- No, both add work on every render and only pay off in specific cases: keeping a prop referentially stable for a React.memo child or a dependency array, or skipping a genuinely expensive computation. Applied everywhere they make the code harder to read and slightly slower.
- How do you create a custom hook?
- Write a function whose name begins with use and which calls other hooks. There is nothing else to it. The important property is that it shares stateful logic rather than state: two components calling the same custom hook each get their own independent state, so sharing a value still needs context or a store.
- Can hooks be called conditionally?
- No. Always call the hook and make the value conditional, or extract the conditional part into a child component that either renders or does not. Splitting the component is usually the cleaner answer, and it is what the React documentation recommends.
- Is it safe to disable the exhaustive-deps lint rule?
- Almost never. The warning means the effect will use values from an older render. Move the function inside the effect, memoise it at its definition, switch to a functional updater so the dependency is unnecessary, or store the value in a ref if you deliberately want to read it without re-subscribing.
This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399