React Hooks changed how we build modern React apps and they’re a staple in hiring loops. If you’re prepping for a frontend role, this guide to react hooks interview questions will help you master what interviewers actually test: the render cycle, dependency arrays, cleanup functions, stale closures, memoization, and custom hook patterns. You’ll find beginner-friendly questions for freshers, deeper React Hooks interview questions for experienced developers, and clear, code-backed answers you can explain out loud. We also include a one-screen cheat sheet, a printable PDF workflow, and a GitHub-style practice repo plan so you can revise fast and interview with confidence.
New to React interviews overall? Start with our React interview questions and answers.
Table of Contents
React Hooks Explained (Quick Overview)
Hooks are how modern React components manage state, effects, refs, context, and memoization-without classes. The key mental model:
- Render: React calls your component function to compute the next UI.
- Commit: React applies changes to the DOM.
- Effects: React runs
useEffectcallbacks to synchronize with external systems (timers, subscriptions, network, DOM APIs). (React)
Why hooks exist (interview-friendly)
- Reuse logic via custom hooks instead of HOCs/render props.
- Keep related logic together (effects + cleanup).
- Encourage pure rendering and push “impure work” to effects.
Rules of Hooks (must-know)
- Call hooks only at the top level (no loops/conditions).
- Call hooks only from React functions (components or custom hooks).
Mini definitions
- State: Component memory that triggers a re-render when updated.
- Effects: “Escape hatch” to sync with external systems.
- Refs: Mutable values that persist across renders without re-render.
- Context: Dependency injection for data shared across trees.
- Memoization: Cache a computation/value (
useMemo) or function identity (useCallback).
Explaination
“React calls my component to render-this is just calculating UI. After committing DOM updates, React runs effects for side effects like fetching, subscriptions, and timers. Hooks like
useStatestore state,useRefstores mutable values without re-rendering,useMemocaches expensive derived values, anduseCallbackstabilizes function identity for memoization. The Rules of Hooks ensure consistent call order. In interviews, I emphasize that effects are for external synchronization and dependency arrays prevent stale closures and bugs.”
Broken → fixed: calling hooks conditionally
// Broken: Hooks in a condition
function Profile({ user }) {
if (user) {
const [tab, setTab] = React.useState("posts");
// ...
}
return null;
}
// Fixed: Always call hooks at top level
function Profile({ user }) {
const [tab, setTab] = React.useState("posts");
if (!user) return null;
return <div>Tab: {tab}</div>;
}
React Hooks List (Core + Advanced Hooks)
Here’s a practical core and advanced hooks you’ll see in real code and in interviews.
Core + advanced hooks
| Hook | What it does | When to use | Common pitfalls |
|---|---|---|---|
useState |
Local state | UI state, inputs | Derived state, stale updates |
useEffect |
Side effects after commit | Fetching, subscriptions | Missing deps, infinite loops |
useContext |
Read context | Theming, auth, i18n | Over-rendering entire tree |
useRef |
Mutable value container | DOM refs, instance vars | Using as state replacement |
useMemo |
Cache computed value | Expensive calc, derived data | Memoizing everything |
useCallback |
Cache function identity | Stable callbacks, memo children | Overuse, hides design issues |
useReducer |
State transitions reducer | Complex state logic | Overengineering simple state |
useLayoutEffect |
Effect before paint | Measure layout without flicker | SSR warnings, blocking paint |
useImperativeHandle |
Customize ref API | Component libraries | Leaking abstraction |
useId |
Stable unique IDs | A11y, form labels | Using for keys |
useTransition |
Mark updates as non-urgent | Keep UI responsive | Misusing for data fetching |
useDeferredValue |
Defer a value | Search/filter UI | Expecting it to “debounce” |
useSyncExternalStore |
Subscribe to external store | Libraries, global stores | Not caching snapshots |
Advanced hooks with examples
useTransition (keep typing responsive)
import React, { useState, useTransition } from "react";
export function SearchWithTransition({ items }) {
const [query, setQuery] = useState("");
const [filtered, setFiltered] = useState(items);
const [isPending, startTransition] = useTransition();
function onChange(e) {
const next = e.target.value;
setQuery(next);
startTransition(() => {
setFiltered(items.filter(x => x.toLowerCase().includes(next.toLowerCase())));
});
}
return (
<div>
<input value={query} onChange={onChange} placeholder="Search..." />
{isPending ? <p>Updating…</p> : <p>Results: {filtered.length}</p>}
</div>
);
}
useDeferredValue (defer expensive rendering)
import React, { useMemo, useState, useDeferredValue } from "react";
export function SearchWithDeferredValue({ items }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(() => {
return items.filter(x => x.toLowerCase().includes(deferredQuery.toLowerCase()));
}, [items, deferredQuery]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<p>Showing results for: {deferredQuery}</p>
<ul>{filtered.slice(0, 20).map(x => <li key={x}>{x}</li>)}</ul>
</div>
);
}
useSyncExternalStore (subscribe safely to an external store)
import React, { useSyncExternalStore } from "react";
const store = {
value: 0,
listeners: new Set(),
subscribe(fn) { store.listeners.add(fn); return () => store.listeners.delete(fn); },
getSnapshot() { return store.value; },
inc() { store.value += 1; store.listeners.forEach(l => l()); },
};
export function ExternalStoreCounter() {
const value = useSyncExternalStore(store.subscribe, store.getSnapshot);
return <button onClick={() => store.inc()}>Count: {value}</button>;
}
(React recommends this hook for external stores to work well with concurrent rendering.) (React)
useId (accessible label/input pairing)
import React, { useId } from "react";
export function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email</label>
<input id={id} name="email" type="email" />
</div>
);
}
Building a real app? Here are some production-ready picks from our best React libraries list
React Hooks Interview Questions for Freshers
Concise answers and tiny code where helpful. We have also added short answer and long answer with code with key questions.
What are hooks in React?
Answer: Hooks are functions that let functional components use state, effects, refs, and other React features.
Why were hooks introduced?
Answer: To reuse stateful logic without classes, reduce complexity, and keep related logic together.
What are the Rules of Hooks?
Answer: Call hooks only at the top level and only from React functions (components/custom hooks).
What does useState return?
Answer: A state value and a setter: [value, setValue].
How do you update state based on previous state?
Why interviewers ask this: They’re checking you know functional updates prevent stale state when batching happens.
Short answer: Use functional updates: setCount(c => c + 1).
Long answer (with code):
import React, { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function incThrice() {
// Correct: each update uses the latest value
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}
return <button onClick={incThrice}>Count: {count}</button>;
}
What is a dependency array?
Answer: A list of values that an effect/memo/callback depends on; React re-runs when they change.
What does useEffect do?
Why interviewers ask this: They want the mental model: effects run after commit and are for external sync.
Short answer: Runs side-effect code after render and optionally cleans up.
Long answer (with code):
import React, { useEffect, useState } from "react";
export function DocumentTitleCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
// No cleanup needed: just syncing title
}, [count]);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
How do you clean up in useEffect?
Short answer: Return a cleanup function from the effect.
Long answer (with code):
import React, { useEffect, useState } from "react";
export function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function onResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", onResize);
// Cleanup prevents leaks
return () => window.removeEventListener("resize", onResize);
}, []);
return <p>Width: {width}</p>;
}
What happens if you omit the dependency array?
Answer: The effect runs after every render.
What does an empty dependency array [] mean?
Answer: Run effect once on mount and cleanup on unmount (plus dev behaviors in Strict Mode).
What is useRef used for?
Answer: Store a mutable value that persists across renders without causing re-renders.
useRef vs useState – what’s the difference?
Why interviewers ask this: They want you to understand render cycle and re-render triggers.
Short answer: State updates re-render; ref updates don’t.
Long answer (with code):
import React, { useRef, useState } from "react";
export function RefVsState() {
const renders = useRef(0);
const [count, setCount] = useState(0);
renders.current += 1;
return (
<div>
<p>Renders: {renders.current}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Re-render</button>
</div>
);
}
What is useContext?
Answer: A hook to read the nearest Provider value from React Context.
How do you avoid prop drilling with context?
Why interviewers ask this: They’re checking you can model shared state and understand rerenders.
Short answer: Put shared value in Context Provider and consume with useContext.
Long answer (with code):
import React, { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function ThemeToggle() {
const theme = useContext(ThemeContext);
return <div>Theme is: {theme}</div>;
}
export function App() {
const [theme] = useState("dark");
return (
<ThemeContext.Provider value={theme}>
<ThemeToggle />
</ThemeContext.Provider>
);
}
Can you call hooks inside loops?
Answer: No-hooks must be called in the same order every render.
What is a custom hook?
Answer: A function that starts with use and calls other hooks to reuse logic.
What is “controlled component”?
Answer: Input value comes from React state and updates via onChange.
What is “uncontrolled component”?
Answer: Input value is managed by the DOM; you read it with a ref.
Controlled vs uncontrolled inputs (tiny demo)
Why interviewers ask this: Forms are common; they want you to know tradeoffs and terminology.
import React, { useRef, useState } from "react";
export function ControlledVsUncontrolled() {
const [name, setName] = useState("");
const emailRef = useRef(null);
function submit(e) {
e.preventDefault();
alert(`Controlled name=${name}, Uncontrolled email=${emailRef.current.value}`);
}
return (
<form onSubmit={submit}>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name (controlled)" />
<input ref={emailRef} placeholder="Email (uncontrolled)" />
<button>Submit</button>
</form>
);
}
What is useMemo used for?
Answer: Cache an expensive computed value to avoid recalculation.
What is useCallback used for?
Answer: Cache function identity to reduce re-renders of memoized children.
When should you avoid useMemo/useCallback?
Answer: When it adds complexity without measurable benefit; memoization has overhead.
What is the difference between props and state?
Answer: Props come from parent; state is owned by component.
Can you set state during render?
Answer: Avoid; it can cause infinite render loops (except specific patterns like derived initialization).
What’s a common useEffect bug?
Answer: Missing dependencies → stale closure or incorrect syncing.
React Hooks Interview Questions and Answers for Experienced
We have shared deeper explanations, broken → fixed pairs, and interview scripts will also include
- Why interviewers ask this
- Common follow-up questions
- Often a broken → fixed pair
Explain the render cycle and where effects run
Short answer: React renders (pure), commits DOM updates, then runs effects for external synchronization.
Long answer: Rendering must stay pure-no subscriptions or mutations. useEffect runs after commit to sync with external systems; cleanup runs before re-run/unmount.
Common follow-up questions
- What is the cleanup function timing?
- Why does Strict Mode run effects twice in dev?
- When would you use
useLayoutEffectinstead?
What is react hooks exhaustive deps and why does it matter?
Why interviewers ask this: Missing deps is the #1 source of subtle bugs (stale closure, wrong fetch, stale props).
Short answer: eslint exhaustive-deps ensures dependency arrays include all reactive values so effects stay correct.
Long answer (with code + proper fix): The linter warns when you reference values in the effect that aren’t listed in the dependency array, which can cause stale closures. React documents this lint rule and the stale closure risk. (React)
import React, { useEffect, useState } from "react";
export function UserGreeting({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
async function run() {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!ignore) setUser(data);
}
run();
return () => { ignore = true; };
}, [userId]); // include userId
return <div>Hello {user?.name ?? "…"}</div>;
}
Common follow-up questions
- When is it okay to disable exhaustive-deps?
- How do you stabilize functions/objects used in deps?
- How do refs help?
What is a stale closure? Show a fix
Why interviewers ask this: Stale closures reveal whether you understand closures + dependency arrays.
Short answer: A function captures old state/props, so it uses outdated values later.
Broken → fixed (interval example):
// Broken: count is captured as 0 forever
import React, { useEffect, useState } from "react";
export function BrokenInterval() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []); // missing count, but adding it causes interval reset
return <p>{count}</p>;
}
// Fixed: functional update avoids stale closure
import React, { useEffect, useState } from "react";
export function FixedInterval() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
return <p>{count}</p>;
}
Common follow-up questions
- When do you use refs vs functional updates?
- Why not just add
countto deps?
Why do infinite loops happen in useEffect?
Why interviewers ask this: They want you to spot dependency array traps quickly.
Short answer: The effect updates state that changes a dependency, triggering another run.
Broken → fixed:
// Broken: derived state + dependency loop
import React, { useEffect, useState } from "react";
export function BrokenLoop({ items }) {
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(items.filter(x => x.active));
}, [items, filtered]); // filtered changes because we set it
return <p>{filtered.length}</p>;
}
// Fixed: don’t put derived state in deps; compute from items
import React, { useMemo } from "react";
export function FixedLoop({ items }) {
const filtered = useMemo(() => items.filter(x => x.active), [items]);
return <p>{filtered.length}</p>;
}
useMemo vs useCallback (with examples)
Why interviewers ask this: It tests whether you understand memoization and referential equality.
Short answer: useMemo memoizes a value; useCallback memoizes a function.
import React, { useCallback, useMemo } from "react";
export function MemoDemo({ items, onSelect }) {
const total = useMemo(() => items.reduce((s, x) => s + x.price, 0), [items]);
const handleSelect = useCallback(
(id) => onSelect(id),
[onSelect]
);
return <button onClick={() => handleSelect(items[0]?.id)}>Total: {total}</button>;
}
Common follow-up questions
- When does
useCallbackactually help? - What’s the cost of memoization?
- How does React.memo interact with these?
React.memo + useCallback synergy: when helpful vs premature
Short answer: Use React.memo to skip re-render when props are referentially equal; use useCallback to keep function props stable.
Broken → fixed (child re-rendering):
// Broken: inline handler changes every render
import React, { useState } from "react";
const Child = React.memo(function Child({ onClick }) {
console.log("Child render");
return <button onClick={onClick}>Child</button>;
});
export function BrokenMemo() {
const [n, setN] = useState(0);
return (
<div>
<button onClick={() => setN(n + 1)}>Parent: {n}</button>
<Child onClick={() => alert("hi")} />
</div>
);
}
// Fixed: stable callback (only if Child is memoized and it matters)
import React, { useCallback, useState } from "react";
const Child = React.memo(function Child({ onClick }) {
console.log("Child render");
return <button onClick={onClick}>Child</button>;
});
export function FixedMemo() {
const [n, setN] = useState(0);
const onClick = useCallback(() => alert("hi"), []);
return (
<div>
<button onClick={() => setN(x => x + 1)}>Parent: {n}</button>
<Child onClick={onClick} />
</div>
);
}
Common follow-up questions
- When does
React.memonot help? - Why do objects/arrays as props often break memoization?
useReducer vs useState tradeoffs
Short answer: useState is simplest; useReducer shines for complex transitions, multiple sub-values, and predictable updates.
import React, { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "add": return { ...state, items: [...state.items, action.item] };
case "remove": return { ...state, items: state.items.filter(x => x.id !== action.id) };
default: return state;
}
}
export function Cart() {
const [state, dispatch] = useReducer(reducer, { items: [] });
return (
<div>
<button onClick={() => dispatch({ type: "add", item: { id: Date.now(), name: "Item" } })}>
Add
</button>
<ul>{state.items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
</div>
);
}
useRef for mutable values and avoiding re-renders
Short answer: Use refs for “instance variables” like timers, previous values, and flags.
import React, { useEffect, useRef, useState } from "react";
export function PreviousValue({ value }) {
const prev = useRef(value);
useEffect(() => { prev.current = value; }, [value]);
return <p>Now: {value}, Prev: {prev.current}</p>;
}
useLayoutEffect vs useEffect
Why interviewers ask this: They want you to know layout measurement and flicker prevention.
Short answer: useLayoutEffect runs before paint (blocking), good for measuring layout; useEffect runs after paint. (React)
import React, { useLayoutEffect, useRef, useState } from "react";
export function MeasureBox() {
const ref = useRef(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
setWidth(ref.current.getBoundingClientRect().width);
}, []);
return <div ref={ref} style={{ border: "1px solid #ccc" }}>Width: {width}</div>;
}
Common follow-up questions
- Why can useLayoutEffect cause SSR warnings?
- How do you avoid layout thrash?
Concurrency hooks: when/why useTransition and useDeferredValue
Short answer: useTransition marks updates as non-urgent; useDeferredValue delays a value to keep UI responsive.
Practical pattern: transition for heavy list rendering
import React, { useState, useTransition } from "react";
export function BigList({ items }) {
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
const [visible, setVisible] = useState(items);
function onChange(e) {
const next = e.target.value;
setQuery(next);
startTransition(() => {
setVisible(items.filter(x => x.includes(next)));
});
}
return (
<div>
<input value={query} onChange={onChange} />
{isPending && <span>Rendering…</span>}
<ul>{visible.slice(0, 200).map(x => <li key={x}>{x}</li>)}</ul>
</div>
);
}
Build a custom hook: rules + structure
Short answer: A custom hook is a function starting with use that composes hooks and returns reusable logic.
Custom hook skeleton
import { useEffect, useState } from "react";
export function useThing(input) {
const [state, setState] = useState(null);
useEffect(() => {
// sync with external system / async work
setState(input);
}, [input]);
return state;
}
Common follow-up questions
- Where do you put validation and errors?
- How do you test a custom hook?
- How do you avoid re-subscribing repeatedly?
Custom hook example: useDebounce
import { useEffect, useState } from "react";
export function useDebounce(value, delayMs) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(id);
}, [value, delayMs]);
return debounced;
}
Usage:
import React, { useState } from "react";
import { useDebounce } from "./useDebounce";
export function DebouncedSearch() {
const [q, setQ] = useState("");
const debounced = useDebounce(q, 300);
return (
<div>
<input value={q} onChange={(e) => setQ(e.target.value)} />
<p>Searching for: {debounced}</p>
</div>
);
}
Custom hook example: useFetch with AbortController
Why interviewers ask this: Fetching is where race conditions and cleanup bugs show up fast.
import { useEffect, useMemo, useState } from "react";
export function useFetch(url, options) {
const optsKey = useMemo(() => JSON.stringify(options ?? {}), [options]);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [status, setStatus] = useState("idle");
useEffect(() => {
if (!url) return;
const controller = new AbortController();
setStatus("loading");
setError(null);
fetch(url, { ...(options ?? {}), signal: controller.signal })
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(json => {
setData(json);
setStatus("success");
})
.catch(e => {
if (e.name === "AbortError") return;
setError(e);
setStatus("error");
});
return () => controller.abort();
}, [url, optsKey]); // options handled via optsKey
return { data, error, status };
}
Race conditions in effects: show broken → fixed
Short answer: Multiple in-flight requests can resolve out of order; use AbortController or request IDs.
// Broken: out-of-order responses can win
import React, { useEffect, useState } from "react";
export function BrokenUserSearch({ query }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/user?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(setUser);
}, [query]);
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}
// Fixed: abort previous request
import React, { useEffect, useState } from "react";
export function FixedUserSearch({ query }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user?q=${encodeURIComponent(query)}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(e => { if (e.name !== "AbortError") throw e; });
return () => controller.abort();
}, [query]);
return <pre>{JSON.stringify(user, null, 2)}</pre>;
}
When should you NOT use an effect?
Short answer: If you’re just deriving state from props/state, compute during render or useMemo instead of syncing via effect.
Interview phrasing: “Effects are for external synchronization; derived values should be calculated during render.”
What values belong in a dependency array?
Answer: Any reactive value used inside the callback: props, state, and functions/objects created during render (unless stabilized).
How do you fix exhaustive-deps warnings without disabling?
Answer: Move logic inside effect, stabilize with useCallback/useMemo, or use refs/reducer patterns.
import React, { useCallback, useEffect, useState } from "react";
export function Search({ term }) {
const [results, setResults] = useState([]);
const run = useCallback(async () => {
const r = await fetch(`/api/search?q=${term}`);
setResults(await r.json());
}, [term]);
useEffect(() => {
run();
}, [run]);
return <div>{results.length}</div>;
}
Why is “disable exhaustive-deps” usually a smell?
Answer: You’re opting out of correctness checks; better to restructure code so deps are accurate.
How do refs help with stable values in effects?
Answer: Put mutable, non-render-affecting values in ref.current to read latest without rerunning effect.
import React, { useEffect, useRef, useState } from "react";
export function LatestValueLogger({ value }) {
const latest = useRef(value);
const [tick, setTick] = useState(0);
useEffect(() => { latest.current = value; }, [value]);
useEffect(() => {
const id = setInterval(() => console.log(latest.current), 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setTick(t => t + 1)}>Tick {tick}</button>;
}
When does useMemo help performance?
Answer: When the computation is expensive and inputs change less often than renders.
When can useMemo hurt performance?
Answer: When memoization overhead outweighs recomputation, or deps are unstable and invalidate constantly.
How do unstable object/array deps break memoization?
Answer: New references every render cause recomputation.
// Fix by memoizing options
const options = React.useMemo(() => ({ mode: "fast" }), []);
What’s referential equality and why does it matter?
Answer: React compares props by reference for memoization; stable references prevent re-renders.
How do you prevent unnecessary re-renders from Context?
Answer: Split contexts (value vs actions), memoize provider values, or use selector patterns.
import React, { createContext, useMemo, useState } from "react";
export const StateCtx = createContext(null);
export const ActionsCtx = createContext(null);
export function Provider({ children }) {
const [count, setCount] = useState(0);
const state = useMemo(() => ({ count }), [count]);
const actions = useMemo(() => ({ inc: () => setCount(c => c + 1) }), []);
return (
<ActionsCtx.Provider value={actions}>
<StateCtx.Provider value={state}>{children}</StateCtx.Provider>
</ActionsCtx.Provider>
);
}
What’s the difference between useEffect and event handlers?
Answer: Effects synchronize after commit; event handlers run in response to user actions and should be the primary place for mutations triggered by events.
How do you handle errors in async effects?
Answer: Capture errors, ignore AbortError, and avoid setting state after unmount.
What is a cleanup function used for?
Answer: Unsubscribe, clear timers, abort requests, reset external state.
How do you avoid “setState on unmounted component”?
Answer: Abort fetch, use flags, or check mounted ref in cleanup.
When do you choose useLayoutEffect?
Answer: When you must measure layout before paint to prevent flicker.
What is useImperativeHandle for?
Answer: Expose an imperative API from a component (common in component libraries).
import React, { forwardRef, useImperativeHandle, useRef } from "react";
export const FancyInput = forwardRef(function FancyInput(_, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { if (inputRef.current) inputRef.current.value = ""; }
}), []);
return <input ref={inputRef} />;
});
Why shouldn’t you use useId for list keys?
Answer: Keys must be stable per item; useId is per component instance, not per list item identity.
What problem does useSyncExternalStore solve?
Answer: Correct subscriptions to external stores under concurrent rendering.
What are common patterns for custom hooks?
Answer: Return {state, actions}, accept configuration, expose stable callbacks, and isolate side effects.
How do you structure a hook that depends on options?
Answer: Memoize options or accept primitives; avoid re-running due to new object references.
How do you avoid derived state anti-pattern?
Answer: Derive during render, or store minimal source-of-truth state.
What does “effects are for syncing with external systems” mean?
Answer: If you’re only transforming existing state/props, compute in render; use effects for external APIs/timers/subscriptions. (React)
How do you deal with expensive recalculation on every render?
Answer: useMemo + stable deps, or move computation outside component if static.
What’s the difference between controlled and uncontrolled inputs in performance?
Answer: Controlled inputs re-render on every keystroke; uncontrolled can avoid frequent re-renders but is harder to validate live.
How do you debug “effect runs too often”?
Answer: Look for unstable deps (inline objects/functions) and Strict Mode behavior.
How do you answer “Should I always use useCallback?”
Answer: No-use it when it prevents meaningful re-renders (memoized children) or stabilizes deps; otherwise it’s complexity.
Common follow-up questions (key topics)
useEffect follow-ups
- What belongs in dependency array?
- How do you handle async + cleanup?
- When do you avoid effects?
useMemo follow-ups
- When does memoization pay off?
- Why does unstable deps invalidate the cache?
useCallback follow-ups
- How does React.memo change the answer?
- How do you avoid “callback hell” with too many callbacks?
Custom hooks follow-ups
- How do you test a custom hook?
- How do you avoid re-subscribing?
- How do you design the hook API?
Debug Checklist (Symptom → Cause → Fix)
| Symptom | Likely cause | Fix |
|---|---|---|
| Effect runs every render | Missing deps array or unstable deps | Add deps; memoize objects/functions |
| UI shows old state in async callback | Stale closure | Functional updates, refs, correct deps |
| Infinite loop | Effect sets state used in deps | Remove derived state; compute in render/useMemo |
| “Too many re-renders” | setState during render | Move to event handler/effect guarded by condition |
| Child rerenders constantly | New callback/object props | useCallback/useMemo + React.memo selectively |
| Flicker during measurement | Measuring after paint | useLayoutEffect or CSS/layout fix |
| Fetch results “jump back” | Race condition | AbortController / request IDs |
Common Mistakes (with concrete symptoms + fixes)
- Disabling exhaustive-deps to “make warning go away”
- Symptom: UI sometimes shows stale values, hard-to-repro bugs
- Fix: restructure code, stabilize deps, use reducers/refs, or move logic into effect
- Derived state stored in useState
- Symptom: state gets out of sync with props
- Fix: compute derived values during render or memoize
- Using refs as a state replacement
- Symptom: UI doesn’t update after ref changes
- Fix: state for UI, ref for instance vars
- Memoizing everything
- Symptom: complex code with no speedup
- Fix: measure first; memoize hot paths only
- Async effects without cleanup
- Symptom: updates after unmount, race conditions
- Fix: AbortController, cleanup flags, request IDs
How to Answer in Interviews (you can memorize it)
useEffect dependency array
“I treat the dependency array as the list of reactive values used inside the effect. If I reference props/state/functions created during render, they go into deps unless I intentionally stabilize them. This prevents stale closures and keeps my effect synced correctly.”
useEffect dependency array
“Effects run after React commits and are for external synchronization-subscriptions, timers, network, imperative DOM APIs. The dependency array tells React when to re-run the sync. If ESLint exhaustive-deps warns, I don’t disable it; I restructure: move logic into the effect, memoize unstable functions/objects with useCallback/useMemo, or use refs/reducers when I need stable identities. My goal is correctness first, then performance.”
stale closure
“Stale closures happen when async callbacks capture old values because dependencies are missing. I fix it with functional updates, correct deps, or refs for latest values.”
memoization
“I use useMemo for expensive calculations and useCallback when passing callbacks to memoized children or as stable dependencies. I avoid memoization when it adds complexity without measurable benefit.”
custom hooks
“A custom hook is just a function that composes hooks. I keep the API simple-return state and actions, ensure cleanup for subscriptions, and keep dependencies correct so behavior is predictable and testable.”
React Hook Form / react form hook
“In interviews, ‘react form hook’ usually means building forms with hooks-controlled inputs with useState, or using React Hook Form for performance and less boilerplate. I can implement validation, handle submit, and explain controlled vs uncontrolled tradeoffs.”
Testing Hooks & Components (Interview Questions + Patterns)
Below are 8 testing-focused questions with realistic patterns. (You can adapt to Jest/Vitest + React Testing Library.)
For a broader testing round, see our React testing interview questions and answers
How do you test a component that uses useEffect for fetching?
Answer: Mock fetch, render, await UI update, and ensure cleanup/abort for unmount.
// pseudo-test (adapt to your runner)
async function testFetchRendersData() {
global.fetch = async () => ({ ok: true, json: async () => ({ name: "Ada" }) });
const ui = render(<UserGreeting userId="1" />);
await ui.findByText(/Ada/);
}
How do you test cleanup functions?
Answer: Unmount and assert unsubscribe/abort called.
function createStoreMock() {
const unsub = { called: 0 };
return {
subscribe(fn) { return () => { unsub.called += 1; }; },
getSnapshot() { return 0; },
unsub,
};
}
function testCleanupOnUnmount() {
const store = createStoreMock();
const App = () => {
React.useSyncExternalStore(store.subscribe, store.getSnapshot);
return null;
};
const ui = render(<App />);
ui.unmount();
assert(store.unsub.called === 1);
}
How do you test timers in effects?
Answer: Use fake timers, advance time, assert updates.
function testDebounce() {
useFakeTimers();
const { getByRole } = render(<DebouncedSearch />);
type(getByRole("textbox"), "react");
advanceTimersByTime(299);
// assert not updated yet
advanceTimersByTime(1);
// assert updated
}
How do you test race conditions?
Answer: Control promise resolution order; ensure the latest request wins.
function deferred() {
let resolve;
const promise = new Promise(r => (resolve = r));
return { promise, resolve };
}
async function testRaceCondition() {
const a = deferred();
const b = deferred();
global.fetch = (url) => {
if (url.includes("q=a")) return Promise.resolve({ ok: true, json: () => a.promise });
return Promise.resolve({ ok: true, json: () => b.promise });
};
const ui = render(<FixedUserSearch query="a" />);
ui.rerender(<FixedUserSearch query="b" />);
b.resolve({ name: "B" });
a.resolve({ name: "A" });
await ui.findByText(/B/); // must not revert to A
}
How do you test stale closures?
Answer: Trigger the async callback after state changes; ensure it uses latest state via functional update/ref.
How do you test useCallback / memo behavior?
Answer: Spy on child render counts; ensure it doesn’t re-render unnecessarily when props are stable.
How do you test transitions?
Answer: Assert UI stays responsive and pending indicator appears for heavy updates.
How do you test custom hooks?
Answer: Prefer testing via components that use the hook; assert outputs and side effects.
function HookProbe({ url }) {
const { status } = useFetch(url);
return <div>Status:{status}</div>;
}
Scenario-Based React Hooks Interview Questions
15 “Given this bug, how would you fix it?” scenarios. Each includes (1) buggy code (2) fixed code (3) short explanation.
Missing deps bug (stale props)
// Bug: logs old userId
function Bug({ userId }) {
React.useEffect(() => {
console.log("User:", userId);
}, []); // missing userId
return null;
}
// Fix
function Fix({ userId }) {
React.useEffect(() => {
console.log("User:", userId);
}, [userId]);
return null;
}
Explanation: Missing dependency causes stale closure.
Infinite loop in useEffect
// Bug: effect updates filtered and depends on filtered
function Bug({ items }) {
const [filtered, setFiltered] = React.useState([]);
React.useEffect(() => {
setFiltered(items.filter(x => x.active));
}, [items, filtered]);
return filtered.length;
}
// Fix: derive in render
function Fix({ items }) {
const filtered = React.useMemo(() => items.filter(x => x.active), [items]);
return filtered.length;
}
Explanation: Derived state + dependency loop.
Derived state anti-pattern
// Bug: state gets out of sync
function Bug({ price, qty }) {
const [total, setTotal] = React.useState(price * qty);
React.useEffect(() => setTotal(price * qty), [price, qty]);
return <p>{total}</p>;
}
// Fix: compute directly
function Fix({ price, qty }) {
const total = price * qty;
return <p>{total}</p>;
}
Explanation: Derived values usually shouldn’t be stored in state.
Performance issue from recalculation
// Bug: expensive calc runs every render
function Bug({ items }) {
const total = items.reduce((s, x) => s + heavy(x), 0);
return <p>{total}</p>;
}
// Fix
function Fix({ items }) {
const total = React.useMemo(() => items.reduce((s, x) => s + heavy(x), 0), [items]);
return <p>{total}</p>;
}
Explanation: Memoize expensive work with stable deps.
Callback identity causes re-renders
// Bug: Child re-renders due to new function prop
const Child = React.memo(({ onPick }) => <button onClick={() => onPick(1)}>Pick</button>);
function Bug() {
const [n, setN] = React.useState(0);
return (
<>
<button onClick={() => setN(n + 1)}>{n}</button>
<Child onPick={(id) => console.log(id)} />
</>
);
}
// Fix
function Fix() {
const [n, setN] = React.useState(0);
const onPick = React.useCallback((id) => console.log(id), []);
return (
<>
<button onClick={() => setN(x => x + 1)}>{n}</button>
<Child onPick={onPick} />
</>
);
}
Explanation: Memoized child needs stable props.
Race condition in fetch
// Bug: last response might not be latest request
function Bug({ q }) {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetch(`/api?q=${q}`).then(r => r.json()).then(setData);
}, [q]);
return <pre>{JSON.stringify(data)}</pre>;
}
// Fix: AbortController
function Fix({ q }) {
const [data, setData] = React.useState(null);
React.useEffect(() => {
const c = new AbortController();
fetch(`/api?q=${q}`, { signal: c.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== "AbortError") throw e; });
return () => c.abort();
}, [q]);
return <pre>{JSON.stringify(data)}</pre>;
}
Explanation: Abort stale requests.
Effect depends on inline object
// Bug: object is new each render
function Bug({ userId }) {
const options = { headers: { "x-user": userId } };
React.useEffect(() => { /* fetch with options */ }, [options]);
return null;
}
// Fix: memoize object
function Fix({ userId }) {
const options = React.useMemo(() => ({ headers: { "x-user": userId } }), [userId]);
React.useEffect(() => { /* fetch with options */ }, [options]);
return null;
}
Explanation: Stabilize deps.
Effect uses function defined in render
// Bug: handler changes every render, triggers effect
function Bug({ term }) {
const run = () => fetch(`/api?q=${term}`);
React.useEffect(() => { run(); }, [run]);
return null;
}
// Fix: move inside effect or useCallback
function Fix({ term }) {
React.useEffect(() => {
fetch(`/api?q=${term}`);
}, [term]);
return null;
}
Explanation: Avoid unstable function deps.
Using ref for UI state
// Bug: UI won't update
function Bug() {
const count = React.useRef(0);
return <button onClick={() => (count.current += 1)}>{count.current}</button>;
}
// Fix: state for UI
function Fix() {
const [count, setCount] = React.useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Explanation: Refs don’t trigger re-render.
Flicker when measuring DOM
// Bug: measure after paint can flicker
function Bug() {
const ref = React.useRef(null);
const [w, setW] = React.useState(0);
React.useEffect(() => setW(ref.current.getBoundingClientRect().width), []);
return <div ref={ref}>Width:{w}</div>;
}
// Fix: useLayoutEffect
function Fix() {
const ref = React.useRef(null);
const [w, setW] = React.useState(0);
React.useLayoutEffect(() => setW(ref.current.getBoundingClientRect().width), []);
return <div ref={ref}>Width:{w}</div>;
}
Explanation: Layout effect runs before paint.
Forgetting cleanup for subscription
// Bug: memory leak
function Bug() {
React.useEffect(() => {
window.addEventListener("scroll", () => {});
}, []);
return null;
}
// Fix: cleanup
function Fix() {
React.useEffect(() => {
const onScroll = () => {};
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
return null;
}
Explanation: Always unsubscribe in cleanup.
Setting state after unmount
// Bug: async resolves after unmount
function Bug({ id }) {
const [u, setU] = React.useState(null);
React.useEffect(() => {
fetch(`/api/u/${id}`).then(r => r.json()).then(setU);
}, [id]);
return <pre>{JSON.stringify(u)}</pre>;
}
// Fix: abort
function Fix({ id }) {
const [u, setU] = React.useState(null);
React.useEffect(() => {
const c = new AbortController();
fetch(`/api/u/${id}`, { signal: c.signal })
.then(r => r.json())
.then(setU)
.catch(e => { if (e.name !== "AbortError") throw e; });
return () => c.abort();
}, [id]);
return <pre>{JSON.stringify(u)}</pre>;
}
Explanation: Abort prevents late updates.
Form handling pitfall: uncontrolled + validation mismatch
// Bug: reading values inconsistently
function Bug() {
const ref = React.useRef(null);
const [error, setError] = React.useState(null);
function submit(e) {
e.preventDefault();
if (!ref.current.value.includes("@")) setError("Invalid");
}
return (
<form onSubmit={submit}>
<input ref={ref} />
{error && <p>{error}</p>}
<button>Submit</button>
</form>
);
}
// Fix: controlled or React Hook Form
function Fix() {
const [email, setEmail] = React.useState("");
const [error, setError] = React.useState(null);
function submit(e) {
e.preventDefault();
setError(email.includes("@") ? null : "Invalid");
}
return (
<form onSubmit={submit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
{error && <p>{error}</p>}
<button>Submit</button>
</form>
);
}
Explanation: Use consistent form strategy.
useMemo used with wrong deps
// Bug: total never updates
function Bug({ items }) {
const total = React.useMemo(() => items.reduce((s, x) => s + x.price, 0), []);
return <p>{total}</p>;
}
// Fix
function Fix({ items }) {
const total = React.useMemo(() => items.reduce((s, x) => s + x.price, 0), [items]);
return <p>{total}</p>;
}
Explanation: Correct dependency array is correctness, not “optimization.”
Context value causes rerenders
// Bug: provider value is new object every render
function BugProvider({ children }) {
const [count, setCount] = React.useState(0);
const value = { count, inc: () => setCount(c => c + 1) };
return <MyCtx.Provider value={value}>{children}</MyCtx.Provider>;
}
// Fix: memoize value
function FixProvider({ children }) {
const [count, setCount] = React.useState(0);
const inc = React.useCallback(() => setCount(c => c + 1), []);
const value = React.useMemo(() => ({ count, inc }), [count, inc]);
return <MyCtx.Provider value={value}>{children}</MyCtx.Provider>;
}
Explanation: Stable provider values reduce rerenders.
React Form Hook (Forms in Interviews)
Interviewers may say react form hook to mean: “Build forms using React hooks.” You should be ready to:
- Build controlled forms with
useState - Explain controlled vs uncontrolled inputs
- Mention when to use React Hook Form (a popular react hooks library) for performance and reduced boilerplate
If you want more options beyond forms, explore our roundup of the best React libraries
Simple controlled form with useState
import React, { useState } from "react";
export function SignupForm() {
const [values, setValues] = useState({ email: "", password: "" });
const [errors, setErrors] = useState({});
function onChange(e) {
setValues(v => ({ ...v, [e.target.name]: e.target.value }));
}
function validate(v) {
const next = {};
if (!v.email.includes("@")) next.email = "Email is invalid";
if (v.password.length < 8) next.password = "Min 8 characters";
return next;
}
function onSubmit(e) {
e.preventDefault();
const nextErrors = validate(values);
setErrors(nextErrors);
if (Object.keys(nextErrors).length === 0) alert("Submitted!");
}
return (
<form onSubmit={onSubmit}>
<label>
Email
<input name="email" value={values.email} onChange={onChange} />
</label>
{errors.email && <p role="alert">{errors.email}</p>}
<label>
Password
<input name="password" type="password" value={values.password} onChange={onChange} />
</label>
{errors.password && <p role="alert">{errors.password}</p>}
<button>Create account</button>
</form>
);
}
When to use React Hook Form
Use it when:
- Large forms cause performance issues with controlled inputs
- You want built-in validation patterns and less boilerplate
- You prefer an uncontrolled-by-default approach with refs under the hood
Small React Hook Form example (interview-friendly)
import React from "react";
import { useForm } from "react-hook-form";
export function RHFLogin() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();
const onSubmit = async (data) => {
await new Promise(r => setTimeout(r, 300));
alert(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
placeholder="Email"
{...register("email", { required: "Email required" })}
/>
{errors.email && <p role="alert">{errors.email.message}</p>}
<input
placeholder="Password"
type="password"
{...register("password", { required: "Password required", minLength: { value: 8, message: "Min 8 chars" } })}
/>
{errors.password && <p role="alert">{errors.password.message}</p>}
<button disabled={isSubmitting}>{isSubmitting ? "Loading..." : "Login"}</button>
</form>
);
}
React Hooks Tutorial (Mini Prep Plan)
A compact react hooks tutorial plan to prep for a react hooks interview.
3-day plan (fast)
Day 1
- Topics:
useState, controlled components, functional updates - Practice: Build counter + todo + controlled form
- Must explain out loud: render cycle, state updates, batching
Day 2
- Topics:
useEffect, dependency array, cleanup function, stale closure - Practice: fetch with abort, resize listener, interval counter (correct)
- Must explain: exhaustive-deps mindset, race conditions
Day 3
- Topics:
useMemo,useCallback,React.memo,useRef,useReducer - Practice: optimize list filtering, memoized child, reducer-based form
- Must explain: referential equality, memoization tradeoffs
7-day plan (deep)
Day 1: Render model + Rules of Hooks + useState patterns
Day 2: useEffect basics + cleanup + “You might not need an effect” mindset
Day 3: Dependency array mastery + stale closure + exhaustive-deps fixes
Day 4: useRef patterns (DOM, previous values, instance vars)
Day 5: Memoization (useMemo, useCallback, React.memo) + measurement
Day 6: Advanced hooks: useLayoutEffect, useId, useTransition, useDeferredValue, useSyncExternalStore
Day 7: Custom hooks + forms + testing patterns (async, cleanup, timers)
React Hooks Cheat Sheet
You can screenshot this react hooks cheat sheet .
- Functional setState:
setX(x => next(x)) - Effect cleanup:
useEffect(() => { subscribe(); return unsubscribe; }, [deps]) - Dependency array rule: “If it’s used inside and can change, it’s a dependency.”
- Stale closure fixes:
- Use functional updates:
setCount(c => c + 1) - Use refs for latest value:
latest.current = value
- Use functional updates:
- Memoization cues:
- Expensive compute →
useMemo(() => calc(a), [a]) - Memo child rerendering due to callback prop →
useCallback(fn, [deps])
- Expensive compute →
- Ref usage:
- DOM:
const inputRef = useRef(null) - Instance var:
const timerId = useRef(null)
- DOM:
- Custom hook skeleton:
function useX(args) { const [s,setS]=useState(); useEffect(()=>{...; return cleanup}, [deps]); return s; }
- Fetch abort pattern:
useEffect(() => {
const c = new AbortController();
fetch(url, { signal: c.signal }).catch(e => e.name === "AbortError" ? null : Promise.reject(e));
return () => c.abort();
}, [url]);
- React.memo + useCallback: only when child render cost matters and props stability is achievable.
Download “React Hooks Interview Questions PDF”
Download the PDF (printable) – you can review offline.
What the PDF includes
- The top react hooks interview questions and answers (freshers + experienced)
- One-screen cheat sheet
- Key broken → fixed patterns (deps, stale closures, race conditions)
- Debug checklist table + common mistakes checklist
- Testing questions section
React Hooks Interview Questions GitHub (Code Repo)
See the GitHub repo structure – A complete blueprint you can create into a real repo.
Repo folder structure
react-hooks/
README.md
package.json
src/
index.jsx
useEffect/
missing-deps.jsx
race-condition.jsx
memoization/
react-memo.jsx
usememo-vs-usecallback.jsx
custom-hooks/
useDebounce.js
useFetch.js
forms/
controlled-form.jsx
react-hook-form.jsx
tests/
useFetch.test.js
timers.test.js
FAQ
- What are the most common react hooks interview questions?
Expect questions onuseState,useEffectdependency array, cleanup function,useRef,useMemo,useCallback, and custom hooks patterns. - Do you have react hooks interview questions and answers in one place?
Yes-this page includes both freshers and experienced Q&A plus scenario-based fixes and testing patterns. - What are react hooks interview questions for freshers?
Focus on Rules of Hooks,useStatebasics,useEffectbasics, dependency array, refs vs state, and controlled vs uncontrolled components. - What are react hooks interview questions for experienced devs?
Expect stale closure, exhaustive deps, race conditions, memoization tradeoffs, React.memo, reducers, concurrency hooks, and testing. - What is react hooks exhaustive deps?
It’s the ESLint rule that validates dependency arrays so your effects/memos/callbacks don’t use stale values. - How do I explain useEffect in interviews?
Say effects run after commit to sync with external systems, and the dependency array defines when to re-sync. - What’s the difference between useMemo and useCallback?
useMemo memoizes a value; useCallback memoizes a function identity. - Is React.memo always a good idea?
No-use it when skipping renders meaningfully improves performance and props can be stable. - How do I avoid stale closures?
Use correct dependencies, functional updates, or refs to read latest values inside async callbacks. - What is a cleanup function?
A function returned by useEffect that unsubscribes, clears timers, or aborts requests before re-run/unmount. - What does “react hooks explained” mean in simple terms?
Hooks let function components use state, effects, refs, and context, replacing most class patterns. - Do you have reactjs hooks interview questions?
Yes-this FAQ and the main sections cover reactjs hooks interview questions and answers with modern patterns. - Can I get a react hooks interview questions pdf?
Yes-use the PDF section: print the page and save as PDF. - Is there a react hooks interview questions github repo?
Yes-the GitHub section includes a full repo blueprint, README, and file stubs. - What is React Hook Form and why mention it?
React Hook Form is a popular library for forms; it often comes up when interviews mention “react form hook” patterns.
References + Useful Resources
- React interview questions and answers
- React testing interview questions and answers
- React Hooks API Reference (Built-in Hooks)
- useEffect docs
- You Might Not Need an Effect
- ESLint rule (react-hooks/exhaustive-deps)
- eslint-plugin-react-hooks README
- React Hook Form docs (Get Started)
Conclusion
You now have a complete, interview-ready set of react hooks interview questions-from freshers to experienced-plus scenario fixes, testing patterns, a one-screen react hooks cheat sheet, a printable PDF workflow, and a react hooks interview questions github repo blueprint.
Next step (do this today):
- Follow the 3-day prep plan
- Build the GitHub repo structure and run each example
- Practice explaining: deps, stale closure, memoization, custom hooks, and React Hook Form out loud
