Hooks Interview Questions
These questions separate people who use hooks from people who understand hooks. Interviewers love them because the follow-ups reveal whether you've hit real bugs or just read the docs.
Q4: What's the Difference Between useEffect and useLayoutEffect?
Interview Question: "When would you use useLayoutEffect instead of useEffect?"
The Simple Explanation
Think of it like painting a room.
useEffect is like painting the room, letting everyone walk in and see it, and THEN rearranging the furniture. People see the room briefly before the furniture is in place.
useLayoutEffect is like painting the room AND arranging the furniture before you open the door. Nobody sees the messy in-between state.
The difference is timing:
useEffectruns after the browser paints pixels to the screenuseLayoutEffectruns before the browser paints, but after React has computed the DOM changes
React computes changes â DOM is updated â useLayoutEffect fires â Browser paints â useEffect firesWhen This Matters
function Tooltip({ targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
// BAD: useEffect â tooltip appears at (0,0) for one frame, then jumps
useEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
setPosition({ top: rect.bottom, left: rect.left });
}, [targetRef]);
// GOOD: useLayoutEffect â tooltip is positioned before user sees anything
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
setPosition({ top: rect.bottom, left: rect.left });
}, [targetRef]);
return <div style={{ position: 'absolute', ...position }}>Tooltip!</div>;
}With useEffect, you get a visual flicker â the tooltip appears at position (0, 0) for one frame, then jumps to the correct position. With useLayoutEffect, the positioning happens before the browser paints, so the user never sees the jump.
The Rule of Thumb
Use useLayoutEffect when you need to:
- Measure DOM elements (getBoundingClientRect, offsetHeight)
- Synchronously update position or size before the user sees anything
- Prevent visual flicker in animations or tooltips
Use useEffect for everything else â data fetching, subscriptions, logging, timers.
The One-Liner That Impresses
"useEffect fires asynchronously after paint, so the user sees the render before your effect runs. useLayoutEffect fires synchronously after DOM mutation but before paint, blocking the browser â which is exactly what you want for measurements and position calculations that would otherwise cause a visual flicker."
Common Follow-Up Questions
"Can useLayoutEffect hurt performance?"
Yes. Because it runs synchronously before the browser paints, a slow useLayoutEffect blocks the entire paint. If your effect takes 100ms, the user stares at a frozen screen for 100ms. That's why you should only use it for quick DOM measurements, never for API calls or heavy computation.
"What about SSR?"
useLayoutEffect fires a warning during server-side rendering because there's no DOM to measure on the server. Use useEffect for SSR-compatible code, or conditionally use useLayoutEffect only on the client.
Red Flags in Your Answer
- Saying "they're the same thing"
- Not being able to describe the timing difference (before paint vs. after paint)
- Using useLayoutEffect for data fetching
- Not mentioning the SSR warning
Q5: How Does the Dependency Array Work in useEffect?
Interview Question: "Explain the dependency array in useEffect. What are stale closures?"
The Simple Explanation
Think of the dependency array like a watchlist. You're telling React: "Only re-run this effect when something on this watchlist changes."
// Runs on EVERY render (no dependency array)
useEffect(() => { /* ... */ });
// Runs ONCE on mount (empty watchlist)
useEffect(() => { /* ... */ }, []);
// Runs when `userId` or `page` changes
useEffect(() => { /* ... */ }, [userId, page]);React compares the new values to the previous values using Object.is (basically ===). If any dependency changed, the effect re-runs. If nothing changed, it skips.
The Stale Closure Trap
This is the #1 hooks bug. A stale closure happens when your effect "captures" an old value and never sees the updated one.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// BUG: `count` is always 0 â this closure captured the initial value
console.log(count);
setCount(count + 1); // Always sets to 1
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps = effect never re-runs, so `count` is forever 0
return <div>{count}</div>;
}Think of it like taking a photograph. The [] dependency array means you took one photo on mount and your effect is forever looking at that photo â even though the real count has changed.
How to Fix Stale Closures
Fix 1: Add the dependency (effect re-runs when count changes)
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]); // Re-creates interval every time count changesFix 2: Use the updater function (no dependency needed)
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // Always reads the latest value
}, 1000);
return () => clearInterval(interval);
}, []); // Safe â we don't read `count`, we use the updaterFix 3: Use a ref for values you want to read but not react to
const countRef = useRef(count);
countRef.current = count; // Always in sync
useEffect(() => {
const interval = setInterval(() => {
console.log(countRef.current); // Always the latest value
}, 1000);
return () => clearInterval(interval);
}, []);The One-Liner That Impresses
"The dependency array tells React when to re-synchronize an effect, and stale closures happen when an effect captures a variable's value at the time the closure was created â not its current value. The fix is either adding it to deps, using a state updater function, or storing it in a ref."
Common Follow-Up Questions
"Why does the exhaustive-deps lint rule exist?"
Because manually managing dependencies is error-prone. If your effect reads count but you forget to add count to the deps array, you get a stale closure. The lint rule catches this at development time.
"What about objects and arrays as dependencies?"
Objects and arrays are compared by reference, not value. { name: 'Alice' } !== { name: 'Alice' } â they're different objects in memory. So if you create a new object every render, the effect re-runs every render. Either memoize the object with useMemo, or depend on the specific primitive values you care about (like user.id instead of user).
Red Flags in Your Answer
- Not being able to explain what a stale closure is
- Saying "just add everything to the dependency array" without understanding why
- Using
// eslint-disable-next-lineto silence the exhaustive-deps warning as a first resort - Not knowing the updater function pattern (
setCount(prev => prev + 1))
Q6: When Should You Use useRef?
Interview Question: "What is useRef and when would you use it?"
The Simple Explanation
Think of useRef like a sticky note on your desk. You can write anything on it, read it anytime, and change what's written â but nobody else in the room (React) notices or cares when you do. Changing a ref does NOT cause a re-render.
Compare that to useState, which is like a public whiteboard. Every time you change it, everyone (React) sees the update and reacts to it.
const ref = useRef(initialValue);
// ref.current = whatever you want â React doesn't careThe Three Use Cases
1. Accessing DOM Elements
The most common use â get a reference to an actual DOM node.
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // Focus the input on mount
}, []);
return <input ref={inputRef} placeholder="I auto-focus!" />;
}2. Storing Mutable Values That Don't Need Re-renders
Perfect for tracking values across renders without triggering updates.
function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef(callback);
// Always point to the latest callback without restarting the interval
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}3. Tracking Flags Like isFetching or hasMore (Infinite Scroll Pattern)
function useInfiniteScroll(fetchPage: (page: number) => Promise<Item[]>) {
const [items, setItems] = useState<Item[]>([]);
const isFetching = useRef(false);
const hasMore = useRef(true);
const pageRef = useRef(1);
const loadMore = useCallback(async () => {
if (isFetching.current || !hasMore.current) return;
isFetching.current = true;
const newItems = await fetchPage(pageRef.current);
if (newItems.length === 0) {
hasMore.current = false;
} else {
setItems(prev => [...prev, ...newItems]);
pageRef.current += 1;
}
isFetching.current = false;
}, [fetchPage]);
return { items, loadMore, hasMore };
}Why refs for isFetching and hasMore? Because these are "guard" values â you need to read them instantly (not after a re-render) and you don't want changing them to trigger a re-render. Using state would cause extra renders and the "instant read" would be stale due to React's async batching.
useState vs useRef Decision
| Need | Use |
|---|---|
| Value that should update the UI when it changes | useState |
| Value that persists across renders but doesn't affect UI | useRef |
| Access to a DOM element | useRef |
| Previous value tracking | useRef |
| Interval/timeout IDs | useRef |
| Flags (isFetching, isMounted) | useRef |
The One-Liner That Impresses
"useRef gives you a mutable container that persists across renders without triggering re-renders â it's React's escape hatch for values that need to survive the render cycle but shouldn't participate in it, like DOM nodes, timer IDs, and guard flags for async operations."
Common Follow-Up Questions
"What's the difference between useRef and creating a variable outside the component?"
A variable outside the component is shared across all instances. If you render two copies of the component, they share the same variable. useRef gives each component instance its own independent container.
"Can you use useRef to store the previous value of a prop?"
Yes â it's a common pattern:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current; // Returns the value from the previous render
}Red Flags in Your Answer
- Saying "useRef is only for DOM elements"
- Using state for values that don't affect rendering (timer IDs, flags)
- Not understanding that changing
.currentdoesn't trigger a re-render - Confusing useRef with createRef (createRef makes a new ref every render â useless in function components)
Q7: Can You Replace useEffect Completely?
Interview Question: "Is useEffect overused? Can you avoid it?"
The Simple Explanation
Think of it like this: useEffect is a Swiss Army knife â it can do everything, but it's rarely the best tool for a specific job. Most of the time, there's a better, more direct tool available.
The React team themselves say: "useEffect is an escape hatch." It's for synchronizing with external systems (the DOM, APIs, subscriptions). If you're using it to respond to user actions or derive data from state, you're probably doing it wrong.
Things That DON'T Need useEffect
1. Transforming data for rendering â just compute it
// BAD: useEffect to derive filtered list
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter(i => i.active));
}, [items]);
// GOOD: compute during render
const filteredItems = items.filter(i => i.active);
// GOOD: memoize if the computation is expensive
const filteredItems = useMemo(
() => items.filter(i => i.active),
[items]
);2. Responding to user events â use the event handler
// BAD: useEffect chain to handle form submission
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
submitForm(formData);
setSubmitted(false);
}
}, [submitted, formData]);
// GOOD: just call it in the handler
const handleSubmit = () => {
submitForm(formData);
};3. Measuring DOM elements â use callback refs
// BAD: useEffect + ref to measure an element
const ref = useRef(null);
const [height, setHeight] = useState(0);
useEffect(() => {
if (ref.current) setHeight(ref.current.offsetHeight);
}, []);
// GOOD: callback ref â fires when the element actually mounts
const [height, setHeight] = useState(0);
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node) setHeight(node.offsetHeight);
}, []);
return <div ref={measuredRef}>Content</div>;Callback refs are better because they fire exactly when the DOM node appears or disappears, even if it's conditional. A useEffect + ref combo misses updates if the element mounts later.
When useEffect IS the Right Tool
- Subscribing to external data sources (WebSocket, browser APIs, event listeners)
- Synchronizing with non-React systems (third-party libraries, analytics)
- Fetching data on mount or when deps change (though data-fetching libraries are often better)
useEffect(() => {
const ws = new WebSocket('wss://stream.example.com');
ws.onmessage = (event) => setData(JSON.parse(event.data));
return () => ws.close();
}, []);The One-Liner That Impresses
"useEffect is for synchronizing React with external systems â if you're using it to react to user events, derive state, or chain state updates, there's almost always a simpler pattern. Event handlers for actions, computed values during render, and callback refs for DOM measurement all eliminate unnecessary effects and the bugs they create."
Common Follow-Up Questions
"What's the 'effect chain' anti-pattern?"
It's when one useEffect sets state, which triggers another useEffect, which sets more state, creating a chain reaction. Each step causes a re-render. Instead, do all the logic in one event handler or compute the derived values directly.
"Are there cases where you truly can't avoid useEffect?"
Yes â anything involving cleanup and subscriptions. If you need to open a WebSocket when a component mounts and close it when it unmounts, useEffect with a cleanup function is the correct pattern. The same goes for event listeners, timers, and observers.
Red Flags in Your Answer
- Saying "useEffect is React's lifecycle method" â it's a synchronization mechanism
- Using useEffect to set derived state that could be computed during render
- Not knowing about callback refs as an alternative for DOM measurement
- Defending every useEffect in your codebase without considering alternatives
- Not understanding the cleanup function runs before every re-execution, not just on unmount