DEV Community

Cover image for Improving Your React Code by Avoiding Hooks Mistakes
Amrendra kumar
Amrendra kumar

Posted on

Improving Your React Code by Avoiding Hooks Mistakes

Quick Summary:

In 2026, planning to design a website or apps is quite easier. One more thing is came which named as React hooks, that also discussed in this Blog, Mainly react hooks are came from three sources like as: missing dependencies from component also breaks render component for 'useffect'.

Most teams reach for useCallback and useMemo before diagnosing the actual re-render cause, which adds complexity without fixing the bug. Fix the dependency array first, profile second, memoize last.

After this, you will be able to triage a Hooks-related bug report without guessing, and decide whether useMemo is solving a real problem or just adding noise.

For deeper patterns on component design, see these React architecture patterns.

What Actually Breaks: The 3 Hook Mistakes That Cause Production Bugs

These three are responsible for the majority of Hooks bugs that make it past code review. Each one looks correct at a glance and only breaks under specific render conditions.

Stale closures from missing dependencies

A closure inside useEffect, useCallback, or useMemo captures variables at the time the function was created, not at call time. Omit a dependency and the callback keeps using the value from the render where it was defined.

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // always logs 0 — stale closure
    }, 1000);
    return () => clearInterval(id);
  }, []); // missing 'count' dependency

  return <button onClick={() => setCount((c) => c + 1)}>Increment</button>;
}
Enter fullscreen mode Exit fullscreen mode

The fix is not to silence the lint rule. Add count to the dependency array, or switch to the functional update form (setCount(c => c + 1)) if the effect doesn't actually need to read count directly. As per the React docs (react.dev, 2025), the dependency array exists specifically to keep effect callbacks synchronized with the latest render's values.

Conditional hook calls breaking render order

React tracks hook state by call order, not by name. A hook called behind a condition shifts every hook after it on the next render where the condition changes.

// Wrong — breaks hook order when `id` becomes empty
function Game({ id }) {
  if (!id) return <p>Select a game</p>;
  const [game, setGame] = useState(null);
  useEffect(() => { /* fetch game */ }, [id]);
  return <GameView game={game} />;
}

// Correct — hooks run unconditionally, logic moves after
function Game({ id }) {
  const [game, setGame] = useState(null);
  useEffect(() => {
    if (!id) return;
    /* fetch game */
  }, [id]);
  if (!id) return <p>Select a game</p>;
  return <GameView game={game} />;
}
Enter fullscreen mode Exit fullscreen mode

Run eslint-plugin-react-hooks (npmjs.com) in CI, not just locally. This class of bug often passes a quick manual test and fails only under specific prop transitions in production.

Unbounded useEffect re-runs without cleanup

Every useEffect that sets a timer, subscription, or listener needs a cleanup function. Skip it and every re-run stacks another active subscription on top of the last one.

useEffect(() => {
  const timer = setTimeout(() => doSomething(), 1000);
  return () => clearTimeout(timer); // prevents leaks and duplicate timers
}, [doSomething]);
Enter fullscreen mode Exit fullscreen mode

In codebases without this pattern enforced, a single screen with three subscribing effects can leak dozens of listeners after a few minutes of normal navigation. The symptom is rarely a crash — it's degraded performance that only shows up after the app has been open a while, which is why it survives code review.

useCallback vs useMemo vs Doing Nothing

Most Hooks guides tell you to memoize without explaining when memoization is wasted work. React re-runs a component function on every render regardless of memoization; useCallback and useMemo only stop downstream re-renders or recomputation, and they cost a comparison check every render to do it.

Situation Use Why
Function passed to a React.memo child useCallback Without it, a new function reference breaks the child's prop equality check every render
Expensive computation (sort, filter, transform) on large data useMemo Skips recomputation when inputs are unchanged
Function or value used only inside the same component Neither No downstream consumer benefits from referential stability; the memoization check costs more than the work it saves
Object or array literal passed as a prop useMemo (or restructure props) Object literals are a new reference every render, defeating React.memo on the receiving component
Simple inline handler with no memoized child Neither Premature memoization here is dead weight

In large React codebases, unnecessary useMemo/useCallback calls are one of the more common causes of code review churn — they signal a performance concern that was never measured, and they make diffs harder to read for no measurable gain.

How to Diagnose a Re-render Problem Before You Memoize Anything

Don't memoize speculatively. Open React DevTools, enable the Profiler tab, and record an interaction that feels slow. The flame graph shows which components re-rendered and why DevTools labels the trigger as props, state, context, or parent re-render.

If the trigger is "parent re-rendered" and the child's props are unchanged primitives, wrap the child in React.memo first. Only add useCallback/useMemo upstream if the Profiler still shows the child re-rendering after that change that's the signal a new reference is breaking the memo check, not a guess.

This sequence (profile, then memo the child, then memoize the source if needed) catches the actual bottleneck instead of scattering useCallback across a component tree based on intuition. Teams that skip the Profiler step typically end up memoizing the wrong layer and still ship the original slowdown.

Do React 19's Compiler and Actions Change These Mistakes?

The React Compiler (react.dev, 2025) auto-memoizes components and values at build time, which removes the need for manual useCallback/useMemo in most cases when the compiler is enabled. It does not fix stale closures or conditional hook calls those are still runtime correctness bugs the compiler can't infer away.

useActionState and form Actions, introduced in React 19, replace a chunk of the manual useState + useEffect pattern previously used for form submission and pending states, which removes one entire category of dependency-array mistakes by removing the effect itself. The mistakes in this guide are not obsolete; they shift toward correctness (closures, hook order) rather than performance, since the compiler increasingly handles the performance side.

FAQ

1. What is the most common mistake with useEffect?

Omitting a dependency that the callback actually reads, which produces a stale closure. The effect keeps using the value from whichever render it was created in, not the latest one, until something forces a re-run with the correct dependency array.

2. Why does my useEffect cause an infinite loop?

Usually an object or array literal in the dependency array, or a state setter inside the effect that updates a value the effect also depends on. Each render creates a new reference, which the dependency comparison treats as changed, triggering another run.

3. Can I call a Hook inside a condition or loop?

No. React tracks hook state by call order across renders, and a conditional call shifts that order whenever the condition changes. Call hooks unconditionally at the top level and move conditional logic after the hook calls.

4. What is a stale closure in React Hooks?

A closure that captured a variable's value from a previous render and never updates, because it's missing from the dependency array. It shows up as a callback that logs or uses an outdated value even though the component has re-rendered with new state.

5. When should I use useCallback vs useMemo?

Use useCallback when passing a function to a memoized child component. Use useMemo for expensive computations or to stabilize object/array references passed as props. Skip both when nothing downstream depends on referential stability.

About the Author

Amrendra Kumar is a software engineer and technical writer at Code with Amrendra, where he covers React, Next.js, AI Agents, SaaS architecture, and cloud infrastructure. He has written 15+ technical articles on frontend engineering, system design, and modern web development.

LinkedIn | GitHub

Top comments (0)