← all posts

useEffect is not a lifecycle

·2 min read ·React · Frontend · JavaScript

Most React developers I've worked with treat useEffect as componentDidMount with a different syntax. Empty dependency array means "on mount", cleanup means "on unmount", and everything in between is a mystery you patch with more dependencies.

That mental model is wrong, and it produces most of the effect bugs I've had to debug.

Synchronization, not lifecycle

An effect answers one question: how do I keep something outside React in sync with my component's state? A WebSocket connection. A subscription. A third-party chart library. The document title. That's the whole job.

The dependency array is not a trigger list. It's a declaration: "this synchronization depends on these values." When they change, React re-synchronizes. When the component goes away, it cleans up. Mount and unmount are just the first and last synchronization. Once you see it that way, [] stops being a magic incantation and starts being a statement: this effect depends on nothing.

Most effects shouldn't exist

When I review React code, the majority of effects fall into two buckets, and both are unnecessary.

Derived state. If you can compute a value from props or state, compute it during render. No effect, no extra state, no render where the two are out of sync.

// Wrong: state that chases other state
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(first + ' ' + last);
}, [first, last]);

// Right: just compute it
const fullName = first + ' ' + last;

Reacting to events. If something should happen because the user did something, put it in the event handler. An effect that watches a state flag so it can fire a request is an event handler with extra steps and a race condition.

The test I apply

Before writing an effect I ask: what external system am I synchronizing with? If the answer is "none, I just want code to run when X changes", the answer is almost always render logic or an event handler.

Working on admissions dashboards, applying this rule deleted about half the effects in the codebase. The remaining ones were real: subscriptions, focus management, integration with non-React libraries. Those are what the hook is for.

If you can't name the external system, don't write the effect.