DEV Community

Teguh Coding
Teguh Coding

Posted on

Stop Using useEffect for Everything: Smarter State Management in React

Every React developer has been there. You need to sync some state, fetch data, or react to a prop change — and almost by instinct, you reach for useEffect. It works. The tests pass. You ship it.

Then six months later, someone (probably future you) opens that component and stares into a tangled web of dependencies, stale closures, and infinite loops. Sound familiar?

The truth is: useEffect is one of the most misused hooks in React. Not because it is bad — it is actually brilliant when used correctly — but because developers treat it as the Swiss Army knife of state management. This post is about learning when NOT to use it, and what to reach for instead.


The Core Misunderstanding

useEffect exists to synchronize your component with an external system — a network request, a browser API, a third-party library. That is it. It is not a general-purpose event handler or a place to calculate derived state.

The React team has been explicit about this in their updated docs: "If you're not connecting to any external system, you probably don't need an Effect."

Let's walk through the most common useEffect overuse patterns and fix them.


Pattern 1: Deriving State from Props

This is probably the #1 misuse I see in code reviews.

// Wrong: Using useEffect to derive state from props
function UserProfile({ userId, users }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const foundUser = users.find(u => u.id === userId);
    setUser(foundUser);
  }, [userId, users]);

  return <div>{user?.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

This causes an extra render cycle every time userId or users changes. React renders once with the stale user, then the effect runs and sets new state, triggering another render. You just doubled your renders for no reason.

The fix: Just calculate it during render.

// Correct: Derive state directly during render
function UserProfile({ userId, users }) {
  const user = users.find(u => u.id === userId);

  return <div>{user?.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

No hook needed. No extra renders. Clean and obvious.


Pattern 2: Resetting State on Prop Change

Another classic trap:

// Wrong: Resetting state inside useEffect
function SearchBox({ query }) {
  const [results, setResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setResults([]);
    setIsLoading(false);
  }, [query]);

  // ... rest of the component
}
Enter fullscreen mode Exit fullscreen mode

Again — extra render, extra complexity, and it's not even obvious why the state is being reset.

The fix: Use the key prop. When key changes, React completely remounts the component, resetting all state automatically.

// In the parent component:
<SearchBox key={query} query={query} />
Enter fullscreen mode Exit fullscreen mode

The component now resets cleanly whenever query changes. No effect needed. React's reconciliation system handles it for you.


Pattern 3: Notifying Parent Components on State Change

// Wrong: Using useEffect to notify parent
function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange]);

  return (
    <button onClick={() => setIsOn(prev => !prev)}>
      {isOn ? 'ON' : 'OFF'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

This creates a weird timing issue: the parent gets notified after the render, not during the event. And if onChange is not memoized, you might trigger infinite loops.

The fix: Call the callback directly in the event handler.

// Correct: Notify parent directly in event handler
function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  const handleClick = () => {
    const nextState = !isOn;
    setIsOn(nextState);
    onChange(nextState);
  };

  return (
    <button onClick={handleClick}>
      {isOn ? 'ON' : 'OFF'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Simpler, synchronous, and no hidden timing issues.


Pattern 4: Fetching Data (Without a Library)

Okay, this one is genuinely a valid use case for useEffect — fetching data is an external system interaction. But most people write it poorly.

// Problematic: No cleanup, no race condition handling
function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Enter fullscreen mode Exit fullscreen mode

This has race conditions if the component unmounts before the fetch resolves, no loading state, no error handling. A better raw implementation uses cleanup:

useEffect(() => {
  let cancelled = false;

  fetch('/api/users')
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setUsers(data);
    });

  return () => {
    cancelled = true;
  };
}, []);
Enter fullscreen mode Exit fullscreen mode

But honestly? In 2026, just use a data fetching library. React Query (TanStack Query), SWR, or if you're on Next.js, server components and fetch with caching. These libraries handle caching, deduplication, revalidation, loading/error states, and race conditions out of the box.

// Using TanStack Query - clean, powerful, battle-tested
function UserList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Enter fullscreen mode Exit fullscreen mode

When useEffect IS the Right Tool

To be fair, there are genuine use cases:

  • Subscribing to browser events: window.addEventListener, ResizeObserver, etc.
  • Integrating with third-party libraries: D3, Mapbox, chart libraries that need direct DOM access.
  • Setting up WebSocket connections
  • Syncing state to localStorage
  • Imperatively focusing an element after render
// Legit useEffect: Syncing with an external system
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}
Enter fullscreen mode Exit fullscreen mode

Notice the cleanup function — this is a sign you're using useEffect correctly. If your effect doesn't need cleanup and isn't connecting to something external, it's probably wrong.


A Simple Mental Model

Before writing useEffect, ask yourself:

  1. Is this connecting to an external system? (browser API, network, third-party lib) — If yes, useEffect is probably right.
  2. Am I just computing a value from existing state/props? — Do it during render or use useMemo.
  3. Am I reacting to a user event? — Put it in the event handler.
  4. Am I resetting state when props change? — Consider using the key prop.
  5. Am I fetching data? — Use TanStack Query or SWR.

The Bigger Picture

Over-relying on useEffect is a symptom of a deeper issue: thinking in terms of lifecycle events ("when this mounts, do X") rather than thinking in terms of synchronization ("keep this external thing in sync with this state").

When you make this mental shift, your components become simpler, your bugs become rarer, and your code reviewers send you coffee instead of Slack messages at midnight.

React's model is: render is the source of truth. Derive as much as you can during render. Reach for effects only when you truly need to step outside React's world.

Start questioning your next useEffect. You might be surprised how often you don't actually need it.


What's the worst useEffect pattern you've encountered in a codebase? Drop it in the comments.

Top comments (0)