Lucas
Lucas

guide

Common Mistakes to Avoid in Your React Projects

14 min read

Common Mistakes to Avoid in Your React Projects

Overview

Explore common pitfalls and mistakes in React development and gain insights into how to avoid them for more successful projects.

ReactBest PracticesCommon Mistakes

Even experienced React developers can fall into common pitfalls that lead to bugs, performance issues, and maintainability problems. This comprehensive guide covers the most frequent mistakes in React development, explains why they're problematic, and provides clear solutions to help you write better, more robust React code.

Understanding React Mistakes

React mistakes often stem from misunderstanding how React works under the hood. Understanding React's rendering cycle, state management, and component lifecycle is crucial for avoiding these pitfalls. Let's explore the most common mistakes and how to fix them.

1. Mutating State Directly

This is one of the most common and critical mistakes in React development.

The Mistake

Directly mutating state objects or arrays instead of creating new references. React relies on reference equality to detect changes, so mutating existing objects won't trigger re-renders.

Why It's a Problem

  • React won't detect the change and won't re-render
  • Can lead to stale UI that doesn't reflect current state
  • Breaks React's immutability principles
  • Can cause issues with React DevTools and debugging

The Solution

Always create new objects or arrays when updating state. Use spread operators, array methods that return new arrays, or libraries like Immer for complex updates.

2. Missing Dependencies in useEffect

The dependency array in useEffect is crucial for correct behavior, but it's easy to get wrong.

The Mistake

Forgetting to include all dependencies in the dependency array, or including unnecessary dependencies, leading to stale closures or infinite loops.

Why It's a Problem

  • Stale closures: Effect uses outdated values
  • Missing updates: Effect doesn't run when it should
  • Infinite loops: Effect runs continuously
  • Hard-to-debug issues that only appear in production

The Solution

Include all values from component scope that change between renders. Use ESLint's exhaustive-deps rule to catch missing dependencies. For values that shouldn't trigger re-runs, use useRef or move them outside the component.

3. Creating Functions Inside Render

Creating new function instances on every render can cause performance issues and unnecessary re-renders.

The Mistake

Defining functions directly in the component body or in JSX, creating new function instances on every render.

Why It's a Problem

  • Child components receive new function references every render
  • Even with React.memo, children will re-render unnecessarily
  • Wastes CPU cycles creating functions
  • Can cause infinite loops in useEffect if used as dependencies

The Solution

Use useCallback to memoize functions that are passed as props or used in dependency arrays. For simple event handlers, define them outside the component if they don't need component scope.

4. Not Using Keys in Lists

Keys are essential for React's reconciliation algorithm, but they're often misused or forgotten.

The Mistake

Missing keys entirely, using array indices as keys for dynamic lists, or using non-unique keys.

Why It's a Problem

  • React can't efficiently update lists
  • Components may maintain incorrect state
  • Performance degradation with large lists
  • UI bugs when list order changes

The Solution

Always provide stable, unique keys. Use IDs from your data when available. Only use indices as keys when the list is static and items never reorder. Generate stable keys if your data doesn't have unique identifiers.

5. Overusing useEffect

useEffect is powerful but often overused for things that can be computed during render.

The Mistake

Using useEffect to compute derived state or synchronize state when you can compute values directly during render.

Why It's a Problem

  • Unnecessary complexity and code
  • Potential for bugs with dependency arrays
  • Extra renders and performance overhead
  • Harder to reason about component logic

The Solution

Compute derived values during render. Only use useEffect for side effects (API calls, subscriptions, DOM manipulation). Use useMemo for expensive calculations, not useEffect.

6. Prop Drilling

Passing props through multiple component levels unnecessarily makes code harder to maintain.

The Mistake

Passing props through components that don't use them, just to get data to deeply nested children.

Why It's a Problem

  • Makes components tightly coupled
  • Harder to refactor and maintain
  • Components become bloated with unused props
  • Difficult to track data flow

The Solution

Use Context API for shared data that many components need. Consider state management libraries (Redux, Zustand) for complex global state. Use component composition to avoid prop drilling when possible.

7. Not Memoizing Expensive Calculations

Performing expensive calculations on every render wastes CPU cycles and can cause performance issues.

The Mistake

Running expensive operations (filtering large arrays, complex computations) directly in the component body without memoization.

Why It's a Problem

  • Wastes CPU cycles on every render
  • Can cause UI lag and jank
  • Battery drain on mobile devices
  • Poor user experience

The Solution

Use useMemo to memoize expensive calculations. Only recompute when dependencies change. For very expensive operations, consider Web Workers or debouncing.

8. Forgetting Cleanup in useEffect

Not cleaning up side effects can lead to memory leaks and bugs.

The Mistake

Creating subscriptions, timers, or event listeners in useEffect without providing cleanup functions.

Why It's a Problem

  • Memory leaks from uncleaned subscriptions
  • Timers continue running after unmount
  • Event listeners attached to removed elements
  • Can cause errors and performance issues

The Solution

Always return cleanup functions from useEffect when you create subscriptions, timers, or event listeners. React will call the cleanup function when the component unmounts or before the effect runs again.

9. Using State for Derived Values

Storing values in state that can be computed from other state or props.

The Mistake

Creating separate state for values that are always derived from other state, leading to synchronization issues.

Why It's a Problem

  • State can get out of sync
  • Extra state updates and re-renders
  • More complex state management
  • Potential for bugs when state doesn't match

The Solution

Compute derived values during render. Use useMemo if the computation is expensive. Only use state for values that change independently.

10. Not Handling Loading and Error States

Forgetting to handle async operation states leads to poor user experience.

The Mistake

Not tracking loading, error, and success states for async operations like API calls.

Why It's a Problem

  • Users don't know if data is loading
  • Errors go unnoticed
  • Poor user experience
  • Hard to debug issues

The Solution

Always track loading, error, and data states. Use libraries like React Query or SWR for server state management. Provide loading indicators and error messages to users.

Best Practices Summary

To avoid these common mistakes:

  • Always create new objects/arrays when updating state
  • Include all dependencies in useEffect arrays
  • Memoize functions passed as props with useCallback
  • Use stable, unique keys for list items
  • Compute derived values during render, not in useEffect
  • Use Context or state management to avoid prop drilling
  • Memoize expensive calculations with useMemo
  • Always clean up side effects in useEffect
  • Don't store derived values in state
  • Handle loading and error states for async operations

Tools to Help

Several tools can help catch these mistakes:

  • ESLint with react-hooks plugin: Catches dependency array issues
  • React DevTools Profiler: Identifies performance issues
  • TypeScript: Catches type-related mistakes
  • React Strict Mode: Highlights potential problems

Conclusion

Awareness of these common mistakes will help you write better React code. Always think about performance, state management, and component lifecycle when building React applications. Remember: React is designed to be predictable—when things go wrong, it's usually because we're fighting against React's design rather than working with it. Learn these patterns, use the right tools, and your React code will be more robust, performant, and maintainable.

Code Samples

State Mutation - Wrong vs Right

Common mistake of mutating state directly

typescript
1 2 3 4 5 6 7 8 9 // ❌ Wrong - Mutating state directly const [state, setState] = useState({ items: [] }); state.items.push(newItem); // This won't trigger re-render! // ✅ Correct - Creating new state object setState({ items: [...state.items, newItem] }); // ✅ Also correct for objects setState({ ...state, newProperty: value });

useEffect Dependencies

Proper dependency array usage

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // ❌ Wrong - Missing dependencies useEffect(() => { fetchData(userId); updateCounter(count); }, []); // Missing userId and count // ✅ Correct - All dependencies included useEffect(() => { fetchData(userId); updateCounter(count); }, [userId, count]); // ✅ Correct - Empty array for mount-only effect useEffect(() => { setupSubscription(); return () => cleanupSubscription(); }, []);

useMemo for Expensive Calculations

Optimizing expensive computations

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 // ❌ Wrong - Recalculates on every render function ExpensiveComponent({ data }: { data: number[] }) { const result = data.reduce((acc, n) => acc + n * n, 0); return <div>{result}</div>; } // ✅ Correct - Memoized calculation function ExpensiveComponent({ data }: { data: number[] }) { const result = useMemo(() => { return data.reduce((acc, n) => acc + n * n, 0); }, [data]); return <div>{result}</div>; }