DEV Community

Cover image for useEffect Is Not a Lifecycle Method
Yasin Demir
Yasin Demir

Posted on • Originally published at ysndmr.com

useEffect Is Not a Lifecycle Method

I spent longer than I should have mentally mapping useEffect onto componentDidMount and componentDidUpdate. That model works just enough to feel right. Then it breaks in ways that are genuinely hard to debug.

The actual model is simpler: useEffect synchronizes something outside React with your current state. That's it. Not "run when the component mounts." Synchronize an external thing.

The clearest version of this is a subscription. If your effect subscribes to something and your cleanup unsubscribes, React runs that cycle every time the dependency changes. Subscribe, unsubscribe, subscribe again. This feels wrong through the lifecycle lens. Through the synchronization lens it makes complete sense: whenever userId changes, I need to be subscribed to the right user's data.

Where this actually matters in practice: data fetching.

useEffect(() => {
  fetch(`/api/user/${userId}`)
    .then(r => r.json())
    .then(setUser);
}, [userId]);
Enter fullscreen mode Exit fullscreen mode

This has a race condition. Two fetches fire, the slower one resolves second, you display stale data. The lifecycle model doesn't explain why. The synchronization model does: you started a new sync cycle, you never cancelled the old one.

useEffect(() => {
  let cancelled = false;
  fetch(`/api/user/${userId}`)
    .then(r => r.json())
    .then(data => {
      if (!cancelled) setUser(data);
    });
  return () => { cancelled = true; };
}, [userId]);
Enter fullscreen mode Exit fullscreen mode

Cleanup doesn't mean "clean up when the component unmounts." It means "clean up before the next sync cycle starts."

Most of the time when useEffect feels weird or wrong, it's because you're thinking about when, not what. The question isn't "when does this run?" It's "what am I keeping in sync, and with what?"


Originally published on ysndmr.com.

Top comments (0)