Most effects in a struggling codebase synchronize with nothing external. They are ordinary logic pushed into a callback that runs one render too late, costing an extra render pass and a moment in which the screen shows inconsistent values.
// unnecessary: an extra render pass, and fullName is stale for one paint
const [fullName, setFullName] = useState('');
useEffect(() => { setFullName(first + ' ' + last); }, [first, last]);
// correct: one render, and the two can never disagree
const fullName = first + ' ' + last;| An Effect that... | What to do instead |
|---|---|
| Computes a value from props or state | Calculate it during rendering |
| Runs because the user clicked or submitted | Put the logic in the event handler |
| Caches an expensive calculation | useMemo (useMemo) |
| Resets all state when a prop changes | Pass that prop as key (Lifting State and Keys) |
| Updates state that another Effect then reads | Compute both in one event handler |
| Notifies a parent after a state change | Call the parent's callback in the handler |
| Subscribes to an external store | useSyncExternalStore (useSyncExternalStore) |
One question separates the two groups. If a user did something, the code belongs in an event handler; if the component being on screen is what requires the work, it belongs in an effect. "Send an analytics event when the Buy button is clicked" is a handler. "Keep the analytics session open while this page is visible" is an effect.