Hooks are the first thing a React interviewer probes, because they reveal in two minutes whether you actually built components or only read about them. This set covers the hooks that come up again and again — useState, useEffect, useRef, useMemo, useCallback and custom hooks — with answers you can speak in a live round and code you can write on a whiteboard.
Why interviewers ask about React hooks
Hooks are how every modern React feature is wired together, so your grasp of them predicts how you will behave in the real codebase. An interviewer wants to know if you understand when a hook runs, not just what it returns.
The trap in most React interviews is not a hard hook — it is a simple one used wrongly. A candidate who cannot explain why a useEffect fires twice, or why a button handler prints an old count, will write bugs that ship. So the questions stay close to daily work: dependency arrays, re-renders, and cleanup.
If you are new to the API, work through the React hooks introduction first, then come back here to rehearse how the same concepts sound as interview answers.
How to answer hook questions
Structure every answer in three beats: what the hook does in one line, when it runs, and one pitfall you have hit. The "when it runs" part is what separates a real developer from someone who memorized the signature.
Wherever you can, write code. React interviews are practical — a five-line snippet that behaves correctly earns more trust than a paragraph. And always tie a hook back to a component you built: "I used useEffect to fetch on mount and cancel the request on unmount" beats a textbook definition every time.
Q1. What is the difference between useState and a normal variable?
A normal variable resets on every render and does not trigger a re-render when you change it. useState gives you a value that React preserves across renders and a setter that schedules a re-render when the value changes.
React re-runs your component function on every render. Any plain let count = 0 is recreated each time, so its updates are lost. useState stores the value outside the function body — React holds it — and returns the current value plus a setter.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
State updates are also asynchronous and batched: calling setCount does not change count on the current line, it queues a new render.
Interview note: Follow-up: "what does
setCount(count + 1)twice in a row do?" It increments only once, because both reads see the samecount. UsesetCount(c => c + 1)to increment twice.
Q2. When does useEffect run, and what does the dependency array control?
useEffect runs after the render is painted. The dependency array controls how often: [] runs it once after mount, no array runs it after every render, and [a, b] runs it after mount and whenever a or b changes.
The effect is for side effects — data fetching, subscriptions, timers, manual DOM work — things that should not happen during rendering. React compares each dependency to its previous value with Object.is and re-runs only if something changed.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // runs after mount and whenever count changes
The rule interviewers care about: every value from the component scope used inside the effect must be in the array. Omitting one causes a stale value bug.
Interview note: Trap: "why does my effect run twice in development?" React 18 Strict Mode intentionally mounts, unmounts and remounts components to surface missing cleanup — it does not happen in production.
Q3. How does the useEffect cleanup function work?
The function you return from useEffect is the cleanup. React runs it before the effect runs again and when the component unmounts — so you use it to cancel timers, unsubscribe from events, or abort fetches.
Without cleanup you leak subscriptions and update state on unmounted components. The pattern below adds a listener on mount and removes exactly that listener on unmount.
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
The cleanup closes over the same values as the effect, so listing correct dependencies also keeps cleanup correct.
Interview note: Follow-up: "when exactly does cleanup run with dependencies
[userId]?" Before each re-run triggered by a changeduserId, and once more on unmount.
Q4. What are the rules of hooks?
Call hooks only at the top level — never inside loops, conditions or nested functions — and call them only from React function components or other custom hooks.
React identifies each hook by the order it is called on every render. If you wrap useState in an if, the order shifts between renders and React attaches state to the wrong hook, corrupting your component.
// Wrong - conditional hook
if (isLoggedIn) {
const [name, setName] = useState(""); // breaks hook order
}
// Right - hook first, condition inside
const [name, setName] = useState("");
if (isLoggedIn) { /* use name */ }
The ESLint plugin eslint-plugin-react-hooks enforces both rules; mention that you rely on it.
Interview note: Trap: "can you call a hook inside a
map?" No — that is a loop; the count of hooks would vary with the array length.
Q5. useMemo vs useCallback — what is the difference?
useMemo caches a computed value; useCallback caches a function. useCallback(fn, deps) is just useMemo(() => fn, deps). Both recompute only when their dependencies change.
You reach for them to avoid expensive recalculation or to keep a stable reference so a memoized child does not re-render. They are optimizations, not correctness tools — adding them everywhere makes code slower and harder to read.
const filtered = useMemo(
() => items.filter(i => i.active),
[items]
);
const handleClick = useCallback(() => onSelect(id), [id, onSelect]);
Use useMemo when the computation is genuinely heavy or feeds a dependency array; use useCallback when passing a callback to a React.memo child.
Interview note: Follow-up: "does useMemo guarantee the value is cached?" No — React may discard the cache to free memory, so never rely on it for correctness.
Q6. What is useRef used for?
useRef returns a mutable object whose .current survives re-renders without triggering one. Two main uses: accessing a DOM node, and holding a mutable value like a timer id or the previous state.
Unlike state, changing ref.current does not re-render. That makes it perfect for values you need to remember but do not want to display.
const inputRef = useRef(null);
useEffect(() => inputRef.current.focus(), []);
return <input ref={inputRef} />;
A common non-DOM use is storing an interval id so cleanup can clear it, or tracking whether the component is still mounted.
Interview note: Trap: "why not use a normal variable instead of a ref?" A normal variable resets every render; a ref persists across renders.
Q7. useState vs useReducer — when do you switch?
Use useState for independent, simple values. Switch to useReducer when the next state depends on the previous one, when several values change together, or when update logic gets complex enough to test on its own.
useReducer centralizes transitions in a pure reducer function, which makes complex state predictable and easy to unit test. It shines for forms, wizards and anything with many related fields.
function reducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "reset": return { count: 0 };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
For the bigger picture on choosing between local state, context and reducers, review the React state management overview.
Interview note: Follow-up: "is useReducer better than Redux?" It manages local state; Redux adds a global store, middleware and devtools. They solve different scopes.
Q8. How does useContext work and when should you avoid it?
useContext reads a value from the nearest matching Context.Provider above it, letting you skip prop drilling. Avoid it for values that change very often, because every consumer re-renders when the context value changes.
Context is ideal for stable, app-wide data: theme, current user, locale. For fast-changing state shared widely, a dedicated state library or splitting contexts prevents render storms.
const ThemeContext = createContext("light");
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Save</button>;
}
Interview note: Trap: "why did all my components re-render?" A new object literal as the provider value creates a fresh reference each render — memoize it with
useMemo.
Q9. What is a custom hook and why write one?
A custom hook is a function whose name starts with use and which calls other hooks. You write one to extract and reuse stateful logic — like a data fetch or a form field — across components without duplicating it.
Custom hooks share logic, not state; each component that calls the hook gets its own independent state. This is React's answer to code reuse that class components solved awkwardly with HOCs and render props.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn(o => !o), []);
return [on, toggle];
}
Being able to design a small custom hook live is one of the strongest signals in an experienced-candidate interview.
Interview note: Follow-up: "do two components using useToggle share state?" No — each call runs its own
useState, so state is isolated.
Q10. What is a stale closure and how do you fix it?
A stale closure is when a callback or effect uses an outdated value because it was created on a previous render. The fix is to list the value as a dependency, or use the functional updater form of the setter so you never read the old value.
This bites hardest inside setInterval or event handlers registered once in an effect. The closure captured count at value 0 and never sees updates.
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []); // functional update avoids the stale count
Reaching for setCount(c => c + 1) instead of setCount(count + 1) is the cleanest fix and shows you understand closures.
Interview note: Trap: "why did adding
countto the deps fix it but restart the interval?" Because the effect re-runs on every count change — the functional updater avoids both problems.
Q11. How do keys work when rendering lists, and why do they matter?
A key is a stable identity React uses to match elements between renders so it can update, move or remove them efficiently. Use a unique, stable id — never the array index when the list can reorder or change length.
Wrong keys cause React to reuse the wrong DOM and component state, producing bugs like text staying in the wrong input row after a delete.
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
Index keys are only safe for a static list that never reorders. This question overlaps heavily with fresher rounds — the React freshers question set drills it further.
Interview note: Follow-up: "what actually breaks with index keys?" On reorder or delete, component state and uncontrolled inputs attach to the wrong item.
Q12. How do you prevent unnecessary re-renders?
Wrap pure child components in React.memo, pass stable callbacks with useCallback, memoize expensive values with useMemo, and keep state as local as possible so fewer components sit in the update path.
The most common cause of wasted renders is passing a fresh function or object to a memoized child every render. Combine React.memo on the child with useCallback/useMemo on the props.
Do not optimize blindly, though — measure with the React Profiler first. Premature memoization adds complexity for no gain. The React performance optimization guide walks through profiling before you touch code.
Interview note: Trap: "does React.memo compare props deeply?" No — it is a shallow comparison, so a new object or array reference defeats it.
How to prepare
Split your prep like this: build one small app (a search box or to-do list) that uses useState, useEffect with cleanup, and one custom hook. Break it on purpose — remove a dependency and watch the stale value appear — because bugs you have seen are answers you cannot forget.
Rehearse the top five hooks out loud until you can explain when each runs without notes. Then pressure-test yourself with the same fresher questions and the broader interview hub for mixed rounds. If you want the full front-end syllabus behind these questions, the Java Full Stack with AI course covers React alongside the backend you will connect it to.
Frequently Asked Questions
Which hooks are asked most often in interviews?
What are the rules of hooks and why do they matter?
How do I explain the useEffect dependency array in an interview?
Do I need to know class components for a React interview?
What is a stale closure and how do I avoid it?
How much React should a fresher know for interviews?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

