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(() => {