Here is a pattern I see in almost every React codebase I read:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
It works. It also renders twice every time a name changes, shows a stale value for one frame, and is a bug waiting to happen when someone adds a third dependency and forgets to list it.
The fix is to delete the effect. Here are four cases where that is the right move.
Why These Effects Cause Bugs
An effect runs after React has painted the screen. So any state you set inside it means:
- React renders with the old value and paints it.
- The effect runs and calls
setState. - React renders again with the new value.
The user can see step 1. That is the flicker. And every effect you add is another place where the dependency list can silently drift out of date.
Case 1: Derived Values
If you can calculate something from props or state you already have, do not store it:
// Before
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// After
const fullName = firstName + ' ' + lastName;
No state, no effect, no extra render, and it can never be out of sync, because it is recalculated every time.
The same goes for filtering and sorting:
// Before: two sources of truth
const [visible, setVisible] = useState(items);
useEffect(() => {
setVisible(items.filter((i) => i.active));
}, [items]);
// After
const visible = items.filter((i) => i.active);
If the calculation is genuinely expensive, wrap it in useMemo. Measure first. For a few hundred items, plain code is fine.
Case 2: Resetting State When a Prop Changes
This is the one people struggle with most:
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment(''); // reset when the user changes
}, [userId]);
return <textarea value={comment} onChange={(e) => setComment(e.target.value)} />;
}
When userId changes, the old comment shows for one frame, then clears. If you have several pieces of state, you need one reset line for each, and you will forget one.
The fix is the key prop:
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState('');
return <textarea value={comment} onChange={(e) => setComment(e.target.value)} />;
}
When the key changes, React treats it as a different component: it throws away the old one, including all its state, and mounts a fresh one. Every piece of state resets, with no effect and no flicker. Remember this one, because it solves a whole category of "why is the old data still there" bugs.
Case 3: Logic That Belongs in an Event Handler
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendOrder(cart);
setSubmitted(false);
}
}, [submitted, cart]);
return <button onClick={() => setSubmitted(true)}>Buy</button>;
This uses an effect as a delayed event handler. It is hard to follow, and it can fire again if cart changes while submitted is still true.
Put the logic where the event happens:
function handleBuy() {
sendOrder(cart);
}
return <button onClick={handleBuy}>Buy</button>;
The rule of thumb: if something happens because the user did something, it belongs in an event handler. If it happens because the component appeared on screen, it can be an effect.
Case 4: Notifying a Parent
useEffect(() => {
onChange(value);
}, [value, onChange]);
This calls the parent after the render, so the parent and child briefly disagree, and it can trigger extra renders in both. Call the parent in the same handler that changes the value:
function handleChange(next: string) {
setValue(next);
onChange(next);
}
Both updates are batched into one render.
So When Is an Effect Right?
Effects are for synchronizing with something outside React:
- Subscribing to a browser event, a WebSocket or a timer
- Starting and cleaning up a third-party widget
- Sending an analytics event because a page was shown
- Fetching data on mount (with a cleanup, so a slow response cannot overwrite a newer one)
That last point deserves care, because an effect that fetches can show old results if responses arrive out of order. I wrote about that race condition in a separate article about search inputs. If your library offers one, a data-fetching library or a framework's server data loading removes the need for the effect altogether.
Quick Checklist Before You Write an Effect
| Question | If yes |
|---|---|
| Can I calculate it from existing props or state? | Calculate it during render |
| Am I resetting state when a prop changes? | Use key
|
| Does it happen because the user did something? | Put it in the event handler |
| Am I telling the parent something changed? | Call the parent in the handler |
| Am I connecting to something outside React? | An effect is right |
The Rule
An effect that only calls setState based on other state or props is almost always a bug waiting to happen. Compute it during render, reset with key, and handle events in handlers.
Quick audit: search your project for useEffect and read the body of each one. Count how many do nothing but set state. Each one is a candidate for deletion, and the component usually gets shorter and faster when you remove it.
If you build with Next.js and React and want a clean starting point, you can browse my templates on pixelanas.com.
Get the templates: https://pixelanas.gumroad.com
What is the strangest useEffect you have ever found in a codebase? Share it in the comments.
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (1)
What about the case where you need to sync state with an external store? Like a Zustand or Redux store? That's technically 'syncing state,' but it's also the legitimate use case for effects (or better yet, useSyncExternalStore). Would be curious to hear your take.