React

15 Common React Mistakes Developers Make (And How to Fix Them)

From stale closures in useEffect to prop drilling and key misuse—these React pitfalls show up in code review every week. Here is how to avoid them.

May 2, 20258 min read
React
React

15 Common React Mistakes Developers Make (And How to Fix Them)

DevPulse AI
Share:

After years of reviewing React pull requests, I notice the same mistakes repeat across junior and senior codebases alike. They are rarely about syntax—they come from mental models that do not quite match how React schedules renders, reconciles trees, and runs effects. This article catalogs the mistakes I flag most often, explains why they bite, and shows fixes you can apply in your next refactor.

You do not need to memorize fifteen rules. Internalize the underlying patterns: state ownership, effect dependencies, referential stability, and where work belongs (event handler vs effect vs server).

Mistake 1: Treating useEffect as "on mount"

useEffect runs after paint when its dependencies change—not only once on mount unless the dependency array is empty, and even then Strict Mode may double-run in development.

// Bug: fetches on every id change but also encodes "mount-only" intent poorly
useEffect(() => {
  fetchUser(id).then(setUser);
}, [id]);

// If you truly need once: document why and handle id changes explicitly
useEffect(() => {
  let cancelled = false;
  fetchUser(id).then((data) => {
    if (!cancelled) setUser(data);
  });
  return () => {
    cancelled = true;
  };
}, [id]);

Fix: Ask "what event causes this work?" If the answer is a button click, put logic in the handler. If the answer is "when id changes," useEffect is appropriate with correct dependencies.

Mistake 2: Missing or lying dependency arrays

Exhaustive-deps warnings exist because stale closures cause bugs that are hard to reproduce.

// Stale: always logs initial count
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, []); // eslint-disable here hides the bug

// Correct
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, [count]);

For callbacks you do not want to re-subscribe on every render, use useRef for mutable values or stabilize with useCallback when justified.

Mistake 3: Deriving state that should be computed

Duplicating props in state creates synchronization bugs.

// Wrong
function FullName({ first, last }) {
  const [full, setFull] = useState(`${first} ${last}`);
  useEffect(() => setFull(`${first} ${last}`), [first, last]);
  return <span>{full}</span>;
}

// Right
function FullName({ first, last }) {
  const full = `${first} ${last}`;
  return <span>{full}</span>;
}

Use state only for user input or async results you cannot derive during render.

Mistake 4: Over-using useEffect for transformations

Filtering, sorting, and formatting belong in render or useMemo—not effects that call setState.

const filtered = useMemo(
  () => items.filter((i) => i.name.includes(query)),
  [items, query]
);

Mistake 5: Unstable keys in lists

Index keys break when items reorder, delete, or insert—React reuses the wrong component instance.

{items.map((item) => (
  <TodoRow key={item.id} item={item} />
))}

Mistake 6: Creating objects and functions inline for memoized children

<MemoizedChart config={{ type: "line", color: "blue" }} />

Every parent render creates a new config reference → child re-renders. Hoist constants, memoize objects, or pass primitives.

Mistake 7: Prop drilling instead of composition

Before Context or Zustand, try component composition:

function Layout({ sidebar, children }) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

// Usage passes data where it is needed without intermediate layers
<Layout sidebar={<UserMenu user={user} />}>
  <Dashboard />
</Layout>

Mistake 8: Context for high-frequency updates

Putting { cart, setCart, theme, setTheme } in one context value means any cart change re-renders theme consumers.

Split providers or use a store with selectors:

const useCartCount = () => useStore((s) => s.items.length);

Mistake 9: Mutating state directly

// Wrong
state.items.push(newItem);
setState(state);

// Right
setState({ ...state, items: [...state.items, newItem] });

With complex nested updates, consider Immer inside reducers or Zustand.

Mistake 10: Ignoring accessibility in interactive components

Custom dropdowns without keyboard support, missing labels, and div buttons are common. Use semantic HTML first; enhance with ARIA when building non-native widgets.

<button type="button" aria-expanded={open} aria-controls="menu-id">
  Menu
</button>

Mistake 11: Fetching in useEffect without deduplication

Parallel mounts and Strict Mode can duplicate requests. Use TanStack Query, SWR, or Remix/Next.js loaders so caching and deduplication are centralized.

const { data, isLoading, error } = useQuery({
  queryKey: ["user", id],
  queryFn: () => fetchUser(id),
});

Mistake 12: Not handling loading and error UI

Happy-path-only components fail in production networks. Co-locate skeletons and error boundaries with the feature.

Mistake 13: Giant components

When a file exceeds ~250 lines and mixes data fetching, form logic, and presentation, extract hooks and presentational components. Tests become easier; renders become more localized.

function useOrderForm(orderId) {
  // state + handlers
  return { fields, submit, isSubmitting };
}

function OrderFormView(props) {
  // JSX only
}

Mistake 14: Fighting the controlled vs uncontrolled decision

Mixing value with defaultValue, or switching mid-lifecycle, causes cursor jumps and lost input. Pick controlled for validated forms; uncontrolled is fine for simple inputs with refs.

Mistake 15: Premature abstraction libraries

Reaching for Redux, MobX, or five context providers on day one adds complexity. Start with colocated state; introduce global tools when pain is proven across features.

Bonus pitfalls: forms, refs, and suspense

Resetting form state incorrectly

When a parent passes a new userId prop, a form may still show the previous user's input because React reuses the component instance. Keys force a clean mount:

<UserForm key={userId} userId={userId} />

Without key, you need controlled resets in useEffect—easy to get wrong and easy to fight with dirty-field tracking.

Refs during render

Reading or writing ref.current during render is undefined behavior in concurrent React. Set refs in effects or event handlers, or use callback refs when integrating non-React widgets.

Suspense without error boundaries

Throwing promises for data without a matching ErrorBoundary leaves users on a stuck fallback when the request fails. Pair route-level error.tsx (in Next.js) or component boundaries with retries and actionable error copy.

Assuming children always re-render with parents

React may bail out of subtrees when props are unchanged—but only when reconciliation allows it. Do not rely on "parent rendered, so child effect ran" unless you understand memo boundaries. Test behavior with Profiler instead of folklore.

Practical tips for code review

  • Ask who owns this state? on every new useState
  • Trace what triggers a re-render when performance complaints appear
  • Require cleanup functions for subscriptions, timers, and abort controllers
  • Prefer discriminated unions for async UI: { status: 'idle' | 'loading' | 'error' | 'success' }

Common mistakes when "fixing" mistakes

"Fix"Actual problem
useEffect(() => {}, []) everywhereHides missing dependencies
Disabling Strict ModeMasks unsafe effects
key={Math.random()}Forces remount, loses state, hurts perf
Global store for all UI stateBroad re-renders
any in TypeScriptRemoves compile-time guardrails

Debugging habits that help

  1. React DevTools → Components → highlight updates
  2. Log render counts temporarily in suspicious components
  3. Reproduce in production build before optimizing
  4. Write a minimal test for the bug—especially effect and form bugs

Building habits that prevent regressions

Add a short "React checklist" to your PR template: effect dependencies verified, list keys stable, no duplicated derived state, loading/error states present for async UI. Pair it with TypeScript strict mode and ESLint react-hooks rules so the toolchain catches stale closures before humans do. Over time the same mistakes stop appearing because the defaults make the right thing easy—not because the team memorized fifteen blog posts.

Conclusion

React's API surface is small; the discipline is in how you structure state and side effects. Most recurring mistakes come from using effects as a catch-all, duplicating state, and spreading updates through the tree without a plan. Fix the mental model—colocate state, derive when possible, stabilize references intentionally, and fetch with proper caching—and the same issues stop reappearing in code review.

Pick two mistakes from this list that show up in your codebase today. Refactor one component each; the patterns will stick faster than reading another hooks cheat sheet.

Team conventions that prevent regressions

Adopt a short React style guide in your repo: where hooks live, naming for event handlers, when to extract components. Add ESLint rules for hooks dependencies and import order. In code review, ask “what re-renders when this state changes?” for every new context provider. Pair design system updates with performance spot checks on Storybook stories that mirror production data density. Mistakes return when conventions erode—refresh them during retros when bugs cluster around the same patterns.

Team conventions that prevent regressions

Adopt a short React style guide in your repo: where hooks live, naming for event handlers, when to extract components. Add ESLint rules for hooks dependencies and import order. In code review, ask “what re-renders when this state changes?” for every new context provider. Pair design system updates with performance spot checks on Storybook stories that mirror production data density. Mistakes return when conventions erode—refresh them during retros when bugs cluster around the same patterns.

Workshop: apply this week

Pick one idea from this article and ship it before Friday. Write a short internal note explaining what changed, what metric you expect to move, and how you will verify the result. Share the note with your team so the learning compounds. If the experiment fails, document the failure mode—it is as valuable as success for the next engineer reading this guide.

Frequently asked questions

Why does my useEffect run twice in development?
React Strict Mode intentionally double-invokes certain lifecycles in development to surface unsafe side effects. It does not happen in production builds. Fix effect cleanup instead of disabling Strict Mode.
Is it wrong to use array index as a key?
Index keys are acceptable for static lists that never reorder, filter, or insert items. For dynamic lists, use stable unique IDs from your data to avoid incorrect component state and unnecessary DOM updates.
When should I lift state up versus use context?
Lift state to the nearest common ancestor that needs it. Reach for context when many distant descendants need the same rarely-changing data (theme, locale). For frequently updating state, context often causes broad re-renders—consider colocation or a store with selectors.

Comments

Discussion is coming soon. Share this article and join the conversation on social media.

Enjoyed this article?

Get weekly engineering guides delivered to your inbox.