Join Treasure Hunt, get $1000 off
Progress: 0/5
Read the rules
Why don't you learn a little bit about us (hint) next?
intermediate
10 min read
React
10/14/2025
#react-pitfalls #react-mistakes #react-best-practices #performance-issues

Common React pitfalls and solutions

The Problem

React developers often encounter common pitfalls that lead to performance issues, bugs, and maintenance nightmares. These include unnecessary re-renders, state management anti-patterns, component coupling, and improper useEffect usage. These mistakes can slow down applications, cause memory leaks, and make code difficult to maintain and debug.

Why This Matters

These pitfalls directly impact user experience and development velocity. Performance issues lead to slow, unresponsive applications. State management mistakes cause bugs that are hard to trace. Poor component architecture creates technical debt that slows down feature development. Understanding and avoiding these pitfalls is crucial for building robust, scalable React applications.

The Solution: Proactive Pattern Recognition

The solution is to recognize common anti-patterns and apply proven solutions. By understanding the root causes of these pitfalls, you can write more efficient, maintainable React code. This involves understanding React’s rendering behavior, proper state management patterns, and component architecture best practices.

How to Implement

Pitfall 1: Unnecessary Re-renders

// Bad: Creating new objects in render
const BadComponent = ({ items }) => {
  const style = { color: 'blue', fontSize: '16px' }; // New object every render
  const handleClick = () => console.log('clicked'); // New function every render

  return (
    <div style={style}>
      {items.map((item) => (
        <Item key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
};

// Good: Memoize values and functions
const GoodComponent = ({ items }) => {
  const style = useMemo(
    () => ({
      color: 'blue',
      fontSize: '16px',
    }),
    []
  );

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <div style={style}>
      {items.map((item) => (
        <Item key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
};

Pitfall 2: State Updates in Render

// Bad: State update during render
const BadCounter = () => {
  const [count, setCount] = useState(0);

  if (count < 5) {
    setCount(count + 1); // Causes infinite loop
  }

  return <div>{count}</div>;
};

// Good: Use useEffect for side effects
const GoodCounter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count < 5) {
      setCount(count + 1);
    }
  }, [count]);

  return <div>{count}</div>;
};

Phase 2: State Management Pitfalls

Pitfall 3: Direct State Mutation

// Bad: Direct mutation
const BadTodoList = () => {
  const [todos, setTodos] = useState([{ id: 1, text: 'Learn React', completed: false }]);

  const toggleTodo = (id) => {
    const todo = todos.find((t) => t.id === id);
    todo.completed = !todo.completed; // Direct mutation
    setTodos([...todos]); // Still references mutated object
  };

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => toggleTodo(todo.id)}>{todo.completed ? 'Undo' : 'Complete'}</button>
        </li>
      ))}
    </ul>
  );
};

// Good: Immutable updates
const GoodTodoList = () => {
  const [todos, setTodos] = useState([{ id: 1, text: 'Learn React', completed: false }]);

  const toggleTodo = (id) => {
    setTodos(todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)));
  };

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => toggleTodo(todo.id)}>{todo.completed ? 'Undo' : 'Complete'}</button>
        </li>
      ))}
    </ul>
  );
};

Pitfall 4: Stale State in Closures

// Bad: Stale state closure
const BadCounter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1); // Always uses initial count (0)
    }, 1000);

    return () => clearInterval(timer);
  }, []); // Empty dependency array

  return <div>{count}</div>;
};

// Good: Use functional updates
const GoodCounter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount((prevCount) => prevCount + 1); // Uses latest state
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>{count}</div>;
};

Phase 3: Component Architecture Pitfalls

Pitfall 5: Prop Drilling

// Bad: Prop drilling through multiple levels
const App = () => {
  const [theme, setTheme] = useState('light');

  return (
    <div>
      <Header theme={theme} />
      <Main theme={theme} setTheme={setTheme} />
      <Footer theme={theme} />
    </div>
  );
};

const Main = ({ theme, setTheme }) => (
  <div>
    <Content theme={theme} setTheme={setTheme} />
  </div>
);

const Content = ({ theme, setTheme }) => (
  <div>
    <ThemeToggle theme={theme} setTheme={setTheme} />
  </div>
);

// Good: Use Context API
const ThemeContext = createContext();

const App = () => {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <div>
        <Header />
        <Main />
        <Footer />
      </div>
    </ThemeContext.Provider>
  );
};

const ThemeToggle = () => {
  const { theme, setTheme } = useContext(ThemeContext);
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>;
};

Pitfall 6: Large Component Files

// Bad: Monolithic component
const BadUserProfile = () => {
  const [user, setUser] = useState(null);
  const [editing, setEditing] = useState(false);
  const [formData, setFormData] = useState({});

  // 200+ lines of component logic

  return <div>{/* Complex JSX with lots of inline logic */}</div>;
};

// Good: Component composition
const UserProfile = () => {
  const [user, setUser] = useState(null);
  const [editing, setEditing] = useState(false);

  return (
    <div>
      <UserHeader user={user} />
      {editing ? (
        <EditUserForm user={user} onSave={setUser} onCancel={() => setEditing(false)} />
      ) : (
        <UserDetails user={user} onEdit={() => setEditing(true)} />
      )}
      <UserActivity user={user} />
    </div>
  );
};

Phase 4: useEffect Pitfalls

Pitfall 7: Missing Dependencies

// Bad: Missing dependencies
const BadSearch = ({ query }) => {
  const [results, setResults] = useState([]);

  useEffect(() => {
    searchAPI(query).then(setResults);
  }, []); // Missing 'query' dependency

  return (
    <ul>
      {results.map((result) => (
        <li key={result.id}>{result.title}</li>
      ))}
    </ul>
  );
};

// Good: Include all dependencies
const GoodSearch = ({ query }) => {
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (query) {
      searchAPI(query).then(setResults);
    }
  }, [query]); // Include 'query' dependency

  return (
    <ul>
      {results.map((result) => (
        <li key={result.id}>{result.title}</li>
      ))}
    </ul>
  );
};

Pitfall 8: Memory Leaks

// Bad: Not cleaning up subscriptions
const BadComponent = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const subscription = api.subscribe((data) => {
      setData(data);
    });

    // No cleanup - memory leak!
  }, []);

  return <div>{/* ... */}</div>;
};

// Good: Proper cleanup
const GoodComponent = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const subscription = api.subscribe((data) => {
      setData(data);
    });

    return () => {
      subscription.unsubscribe(); // Cleanup
    };
  }, []);

  return <div>{/* ... */}</div>;
};

Results You Can Expect

  • 60-80% reduction in unnecessary re-renders
  • 50% fewer bugs related to state management
  • Improved maintainability with better component architecture
  • Better performance through proper optimization patterns

Common Questions

Q: How do I know if I’m over-optimizing? If your optimization makes the code harder to read and maintain without measurable performance benefits, you’re probably over-optimizing. Profile first, optimize second.

Q: Should I always use useCallback and useMemo? No, only use them when you have measurable performance issues or when passing callbacks/memoized values to optimized child components.

Q: How do I choose between Context and prop drilling? Use Context for truly global state (theme, auth, user preferences). Use prop drilling for component-specific state that’s only needed by a few levels of components.

Tools & Resources

  • React DevTools Profiler - Identify performance bottlenecks and unnecessary re-renders
  • React Hook Form - Avoid form-related state management pitfalls
  • React Query - Handle server state without common pitfalls
  • ESLint React Hooks Plugin - Catch hook-related issues early

Performance & Optimization

State Management & Architecture

Debugging & Problem Solving

Error Handling

Need Help With Implementation?

Avoiding these pitfalls requires understanding React’s internals, performance patterns, and best practices. Built By Dakic specializes in helping teams identify and fix React anti-patterns, building robust, performant applications that scale. Get in touch for a free consultation and discover how we can help you avoid common React pitfalls and build better applications.