Top 5 Mistakes Developers Make During React Interviews (And How to Avoid Them)

·13 min read
reactinterview-questionsinterview-tipsfrontendcareerhooks

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 userId from props. When userId changes, the effect doesn't re-run—we're stuck showing the old user.

The fix is to include userId in 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

  1. You understand closures - Effects capture values from the render they were created in
  2. You respect the lint rules - Disabling exhaustive-deps is almost always wrong
  3. 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:

  1. Move functions inside the effect - If a function is only used in the effect, define it there so it's not a dependency
  2. Use useCallback for functions - If the function needs to be outside, wrap it in useCallback with its own dependencies
  3. 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

  1. You understand the default behavior - State change = re-render self + children
  2. You know multiple solutions - memo, useMemo, restructuring
  3. 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:

  1. Props change frequently - Memo adds comparison overhead for no benefit
  2. The component is cheap to render - The memo comparison might cost more than just rendering
  3. 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 stale count value (0) because state updates are asynchronous and batched. All three calls effectively do setCount(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.log still shows 0 though—you can't read the new state until the next render."

What Interviewers Want to Hear

  1. You understand batching - React groups state updates for performance
  2. You know the functional form - setState(prev => ...) for updates based on previous state
  3. 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:

  1. 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 })).
  2. 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

  1. You know the rules - Only call hooks at the top level, only call hooks from React functions
  2. You understand why - React uses call order, not names, to track hooks
  3. 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

  1. You understand reconciliation - Keys help React identify which elements changed
  2. You know the failure modes - Wrong updates, lost state, performance issues
  3. 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:

  1. The list is static—no adds, removes, or reorders
  2. Items have no state or uncontrolled inputs
  3. 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

MistakeWhat Goes WrongThe Fix
Empty dependency arrayStale closures, outdated dataInclude all used values in deps
Index as keyWrong updates, lost stateUse stable unique IDs
Direct state reads in updatesBatching causes stale valuesUse functional form prev => ...
Conditional hooksHook order changes, React crashesHooks at top level, always called
Memoizing everythingWasted comparisons, slower codeProfile first, optimize bottlenecks

Related Articles

If you found this helpful, check out these related guides:


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.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides