Top 5 Mistakes Developers Make During React Interviews (And How to Avoid Them)
I've conducted hundreds of React interviews over 12 years in fintech. The same mistakes keep appearing. Senior developers with 5+ years of experience trip on the same questions that junior developers struggle with. Not because React is hard to use—because React is easy to use without understanding how it works.
These five mistakes cost developers job offers. I've seen candidates fail on exactly these points at companies like BNY Mellon, UBS, and major tech firms. Here's how to avoid them.
Mistake #1: Misunderstanding the useEffect Dependency Array
This is the most common interview failure point. A candidate explains they've used React for years, then can't explain why their effect runs infinitely or never updates.
The Mistake
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Bug: Missing dependency
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return <div>{user?.name}</div>;
}
When asked "What happens when userId changes?", weak answers include:
- "It fetches the new user" (wrong—the effect never re-runs)
- "I disabled the lint warning so it's fine" (red flag)
- "I'm not sure why the linter complains" (concerning uncertainty)
The Strong Answer
"This effect has a stale closure problem. The empty dependency array means it only runs on mount, but it uses
userIdfrom props. WhenuserIdchanges, the effect doesn't re-run—we're stuck showing the old user.The fix is to include
userIdin the dependency array:useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);The lint rule exists because React can't detect what values you use inside the effect. Every value from component scope used inside useEffect must be in the dependency array, or you'll have stale data."
What Interviewers Want to Hear
- You understand closures - Effects capture values from the render they were created in
- You respect the lint rules - Disabling exhaustive-deps is almost always wrong
- You can identify stale closures - Recognizing when values won't update
The Deeper Follow-Up
If they ask "How do you avoid unnecessary effect re-runs?", be ready:
"Three strategies:
- Move functions inside the effect - If a function is only used in the effect, define it there so it's not a dependency
- Use useCallback for functions - If the function needs to be outside, wrap it in useCallback with its own dependencies
- Use refs for values you don't want to trigger re-runs - useRef gives you a mutable container that doesn't cause re-renders
But the first question should be: does this even need to be an effect? Data fetching often belongs in event handlers or a data fetching library like React Query."
Mistake #2: Not Understanding What Triggers Re-Renders
Every React interview probes rendering behavior. The question that catches people: "Why is this component slow?"
The Mistake
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Clicked {count} times
</button>
<ExpensiveComponent /> {/* Re-renders on every click! */}
</div>
);
}
function ExpensiveComponent() {
// Heavy computation on every render
const result = heavyCalculation();
return <div>{result}</div>;
}
Weak answers:
- "Wrap everything in useMemo" (cargo culting)
- "Use React.memo on every component" (misses the point)
- "I'd need to profile it" (avoiding the conceptual question)
The Strong Answer
"When App's state changes, React re-renders App and all its children by default—including ExpensiveComponent, even though it receives no props. This is React's reconciliation model.
The fix depends on the situation:
If ExpensiveComponent doesn't need App's state:
const MemoizedExpensive = React.memo(ExpensiveComponent); // Now it only re-renders if its props change (none, so never)If the computation is the expensive part:
function ExpensiveComponent() { const result = useMemo(() => heavyCalculation(), []); return <div>{result}</div>; }Best approach—restructure to avoid the problem:
function App() { return ( <div> <Counter /> {/* State lives here now */} <ExpensiveComponent /> {/* Never re-renders */} </div> ); }Moving state down or lifting content up is often better than memoization."
What Interviewers Want to Hear
- You understand the default behavior - State change = re-render self + children
- You know multiple solutions - memo, useMemo, restructuring
- You prefer structural fixes - Memoization is a last resort, not first instinct
The Deeper Follow-Up
If they ask "When should you NOT use React.memo?", be ready:
"Don't use React.memo when:
- Props change frequently - Memo adds comparison overhead for no benefit
- The component is cheap to render - The memo comparison might cost more than just rendering
- Props include children or render props - These create new references every render, defeating memo
Profile first. React DevTools Profiler shows exactly what's re-rendering and why. Premature optimization with memo can actually make things slower."
Mistake #3: State Updates Aren't Immediate
This catches even experienced developers. They understand React is declarative but forget state updates are asynchronous.
The Mistake
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
console.log(count); // Still 0!
};
return <button onClick={handleClick}>Count: {count}</button>;
}
When asked "What's the count after clicking?", weak answers include:
- "3" (wrong—it's 1)
- "I'm not sure, I'd have to run it" (should know conceptually)
- "React batches updates so maybe 1?" (right answer, uncertain delivery)
The Strong Answer
"The count will be 1, not 3. Each
setCount(count + 1)reads the same stalecountvalue (0) because state updates are asynchronous and batched. All three calls effectively dosetCount(0 + 1).The fix is the functional update form:
const handleClick = () => { setCount(prev => prev + 1); // 0 -> 1 setCount(prev => prev + 1); // 1 -> 2 setCount(prev => prev + 1); // 2 -> 3 };With functional updates, React queues each update and passes the latest pending state to the next updater function. The
console.logstill shows 0 though—you can't read the new state until the next render."
What Interviewers Want to Hear
- You understand batching - React groups state updates for performance
-
You know the functional form -
setState(prev => ...)for updates based on previous state - You know when to use which - Direct value for replacements, function for derivations
The Deeper Follow-Up
If they ask "How is this different from class component setState?", be ready:
"Two key differences:
- useState doesn't merge -
this.setState({ name })merged with existing state.setUser({ name })replaces the entire state. For objects, spread manually:setUser(prev => ({ ...prev, name })).- No callback - Class
this.setState({}, callback)ran after state updated. With hooks, use useEffect to react to state changes.React 18 also made batching automatic everywhere—even in setTimeout and promises—whereas class components only batched in event handlers."
Mistake #4: Breaking the Rules of Hooks
This is a fundamental React concept that trips up developers who learned hooks by example rather than understanding the rules.
The Mistake
function UserProfile({ userId }) {
if (!userId) {
return <div>Please select a user</div>;
}
// Error: Hook called conditionally!
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
When asked "What's wrong with this code?", weak answers include:
- "It looks fine to me" (misses the fundamental issue)
- "Maybe the effect should have different deps" (wrong focus)
- "I've done this before and it worked" (anecdotal, dangerous)
The Strong Answer
"This breaks the Rules of Hooks. The early return means useState and useEffect aren't called when userId is falsy, but they are called when it's truthy. React tracks hooks by their call order—if the order changes between renders, React loses track of which hook is which.
The fix is to always call hooks unconditionally:
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { if (!userId) return; fetchUser(userId).then(setUser); }, [userId]); if (!userId) { return <div>Please select a user</div>; } return <div>{user?.name}</div>; }Hooks go at the top, early returns come after. The conditional logic moves inside the effect."
What Interviewers Want to Hear
- You know the rules - Only call hooks at the top level, only call hooks from React functions
- You understand why - React uses call order, not names, to track hooks
- You use the linter - eslint-plugin-react-hooks catches these automatically
The Deeper Follow-Up
If they ask "Can you call hooks in a loop?", be ready:
"No, for the same reason—the number of hook calls must be constant between renders. This breaks:
// Wrong: different number of hooks per render items.forEach(item => { const [selected, setSelected] = useState(false); });Instead, lift state up or use a single state object:
const [selectedItems, setSelectedItems] = useState({}); // or const [selectedItems, setSelectedItems] = useState(new Set());If you need per-item state, consider a separate component for each item that manages its own state."
Mistake #5: Misusing Keys in Lists
This seems basic but catches developers constantly. It's often the source of mysterious bugs that are hard to trace.
The Mistake
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}> {/* Bug: using index as key */}
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
}
When asked "What could go wrong here?", weak answers include:
- "Nothing, I always use index as key" (wrong)
- "The linter doesn't complain" (it should, or you're not using one)
- "It's fine as long as the list doesn't change" (partial understanding)
The Strong Answer
"Using array index as key causes bugs when items are added, removed, or reordered. React uses keys to identify which items changed. With index keys, if I add an item at the beginning, every item's index shifts—React thinks every item changed and re-renders them all, potentially losing input state.
The fix is to use stable, unique identifiers:
{todos.map(todo => ( <li key={todo.id}> {/* ... */} </li> ))}Real-world bug: with index keys, if each todo has an input field and I delete the second item, the third item's input shows the deleted item's text because React kept the DOM element and just updated the data."
What Interviewers Want to Hear
- You understand reconciliation - Keys help React identify which elements changed
- You know the failure modes - Wrong updates, lost state, performance issues
- You use stable identifiers - Database IDs, UUIDs, anything that doesn't change
The Deeper Follow-Up
If they ask "When is index as key actually okay?", be ready:
"Index keys are acceptable when ALL of these are true:
- The list is static—no adds, removes, or reorders
- Items have no state or uncontrolled inputs
- Items have no unique IDs available
For static navigation menus or display-only lists that never change, index is fine. But the moment there's any interactivity or dynamic content, you need real keys.
Also never use
key={Math.random()}—this creates a new key every render, forcing React to destroy and recreate the element, losing all state and killing performance."
What Interviewers Actually Look For
After conducting hundreds of React interviews, patterns emerge in what separates successful candidates:
They Explain the "Why"
Good candidates don't just know the fix—they explain the underlying model. "useEffect needs dependencies because of closures" is better than "the linter says so."
They Know Multiple Solutions
For re-render problems: memo, useMemo, restructuring. For state issues: functional updates, useReducer. Interviewers want to see you evaluate trade-offs, not reach for one tool every time.
They Admit Uncertainty Appropriately
"I'd profile this before adding memoization" shows mature judgment. "I always wrap everything in useMemo" shows cargo culting.
They Reference Real Debugging Experience
"I've seen this cause bugs when..." is more convincing than "I read that this is bad." Interviewers can tell who's actually debugged these issues.
Quick Reference
| Mistake | What Goes Wrong | The Fix |
|---|---|---|
| Empty dependency array | Stale closures, outdated data | Include all used values in deps |
| Index as key | Wrong updates, lost state | Use stable unique IDs |
| Direct state reads in updates | Batching causes stale values | Use functional form prev => ...
|
| Conditional hooks | Hook order changes, React crashes | Hooks at top level, always called |
| Memoizing everything | Wasted comparisons, slower code | Profile first, optimize bottlenecks |
Related Articles
If you found this helpful, check out these related guides:
- Complete Technical Interview Career Guide - comprehensive preparation guide for the entire interview process
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- React Hooks Interview Guide - Deep dive into all hooks patterns
- React Advanced Interview Questions - Server components, Suspense, and architecture
- React 19 New Features - What's new and what interviewers ask about it
- JavaScript Closures Interview Guide - The foundation behind useEffect issues
Ready for More React Interview Questions?
This is just one topic from our complete React interview prep guide. Get access to 50+ questions covering:
- Custom hooks patterns and implementation
- State management (Context, Redux, Zustand)
- Performance optimization strategies
- Testing React components
- Server-side rendering and hydration
Get Full Access to All React Questions →
Or try our free preview to see more questions like this.
Written by the EasyInterview team, based on real interview experience from 12+ years in tech and hundreds of technical interviews conducted at companies like BNY Mellon, UBS, and leading fintech firms.

Written by
EasyInterview Team
Based on 15+ years in tech and hundreds of technical interviews conducted at companies like BNY Mellon, UBS, and leading fintech firms.
Ready for More Interview Questions?
This is just one topic from our complete interview prep guide. Get access to 800+ questions across 13 technologies.