You’re Using useEffect Too Much. Most of It Belongs in Render, Not an Effect.
UseEffect is one of the most commonly misunderstood React hooks.
Developers often reach for it whenever they need to calculate something, update state, respond to a change, or run some logic after rendering.
The code usually looks reasonable:
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
It works.
But the question isn’t whether it works.
The better question is:
Does this actually need an effect?
In many React components, the answer is no.
A large amount of useEffect code exists only because developers are using effects to derive values that React can calculate directly during render.
That creates unnecessary state, extra renders, synchronization problems, and code that is harder to reason about.
The mental model should be simple:
Render is for calculating the UI. Event handlers are for responding to user actions. Effects are for synchronizing with external systems.
Let’s look at what people commonly write — and what it should be.
1. Don’t Use an Effect to Calculate Derived Values
What people write:
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(price * quantity);
}, [price, quantity]);
What it should be:
const total = price * quantity;
total isn’t really state.
It is a value derived from price and quantity.
The first version creates an unnecessary sequence:
Render
↓
Effect runs
↓
setTotal()
↓
Render again
The second version simply calculates the value when React renders.
There is no synchronization problem because there is nothing to synchronize.
2. Filtering Data Doesnt Usually Need useEffect
What people write:
const [filteredUsers, setFilteredUsers] = useState([]);
useEffect(() => {
setFilteredUsers(
users.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase()
)
);
}, [users, search]);
What it should be:
const filteredUsers = users.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase())
);
The filtered list is completely determined by users and search.
So why store it separately?
You’re creating a second source of truth for information that already has a source of truth.
If the calculation is genuinely expensive, you can consider:
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(search.toLowerCase())
);
}, [users, search]);
But don’t reach for useMemo automatically either.
First write the straightforward version. Optimize when there is an actual performance problem.
3.Mapping and Formatting Data Belongs in Render**
What people write:
const [itemsWithLabels, setItemsWithLabels] = useState([]);
useEffect(() => {
setItemsWithLabels(
items.map(item => ({
...item,
label: ${item.name} - $${item.price}
}))
);
}, [items]);
What it should be:
const itemsWithLabels = items.map(item => ({
...item,
label: ${item.name} - $${item.price}
}));
This is just a transformation.
React already has the data.
You don’t need an effect to transform data that exists inside the component.
4. Don’t Use Effects to Keep State in Sync
This is another common pattern:
const [selectedUser, setSelectedUser] = useState(null);
useEffect(() => {
setSelectedUser(
users.find(user => user.id === selectedId)
);
}, [users, selectedId]);
It may look like you’re keeping selectedUser synchronized.
But there is nothing to synchronize.
You can simply write:
const selectedUser = users.find(
user => user.id === selectedId
);
Now there is one source of truth.
selectedId determines the selected user.
That relationship is obvious from the code.
The more state you create unnecessarily, the more state you eventually have to synchronize.
5. Don’t Use an Effect for Logic Caused by a User Action
Imagine a button that submits an order.
A developer might write:
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendAnalytics();
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
}
But why introduce state just to trigger an effect?
The action already happened inside handleSubmit.
What it should be:
function handleSubmit() {
sendAnalytics();
submitOrder();
}
This makes the reason for the action obvious.
When something happens because the user clicked a button, submitted a form, selected an option, or triggered another interaction, the event handler is usually the right place for that logic.
Don’t turn an event into state just so an effect can react to it.
6. So When Should You Actually Use useEffect?
This doesn’t mean useEffect is bad.
It means useEffect has a specific job.
Use it when your component needs to synchronize with something outside React.
For example:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Here, an external connection exists outside React.
The component needs to connect when the relevant values change and clean up the connection when necessary.
That’s a legitimate effect.
Other examples can include:
- WebSocket connections
- Browser APIs
- Subscriptions
- Timers
- Third-party widgets
- External libraries
- Other systems whose lifecycle React needs to synchronize with
The important question is not:
“Do I need to run this after render?”
The better question is:
“What external system am I synchronizing with?”
If you can’t identify one, take another look at the effect.
7. Dont Confuse useEffect With “After Render Logic”
One reason developers overuse effects is the mental model:
“If something needs to happen after rendering, put it in useEffect.”
That’s too broad.
A component can contain normal calculations that don’t need to wait for an effect.
For example:
const greeting = Hello, ${name};
There is no reason to wait until after rendering to calculate this.
Likewise:
const isAdult = age >= 18;
Or:
const cartTotal = cart.reduce(
(total, item) => total + item.price,
0
);
These are normal render-time calculations.
They describe what the UI should look like based on the current inputs.
That’s exactly what rendering is for.
8. Unnecessary Effects Make Data Flow Harder to Understand
Consider:
const [message, setMessage] = useState("");
useEffect(() => {
setMessage(`${firstName} ${lastName}`);
}, [firstName, lastName]);
To understand where message comes from, you now have to look in two places:
Where the state is declared.
Where the effect updates it.
With:
const message = ${firstName} ${lastName};
the relationship is immediately visible.
This is one of the biggest benefits of avoiding unnecessary effects:
The data flow becomes easier to follow.
Good React code should make it obvious where a value comes from and why it changes.
9. More Effects Can Mean More Synchronization Problems
Suppose a component has several pieces of derived state:
const [total, setTotal] = useState(0);
const [filteredItems, setFilteredItems] = useState([]);
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
setTotal(...);
}, [items]);
useEffect(() => {
setFilteredItems(...);
}, [items, search]);
useEffect(() => {
setSelectedItem(...);
}, [items, selectedId]);
Now you’ve created multiple synchronization relationships.
Every new dependency can affect the behavior of these effects.
Instead, you might have:
const total = ...;
const filteredItems = ...;
const selectedItem = ...;
Now those values are derived directly from the current state and props.
No synchronization layer is required.
10. Render First. Optimize Later.
Another reason developers put calculations into effects is performance anxiety.
They think:
“I don’t want this calculation to happen on every render.”
But moving a calculation into an effect doesn’t automatically make the application faster.
In fact, it can make the update flow more complicated.
Start with:
const result = calculateSomething(data);
If the calculation is expensive and profiling shows that it matters, then consider:
const result = useMemo(
() => calculateSomething(data),
[data]
);
The important distinction is:
**Render logic calculates values.
useMemo can optimize expensive calculations.
useEffect synchronizes with external systems.**
These are three different responsibilities.
A Simple React Decision Tree
Before writing useEffect, ask yourself:
**
Is this value derived from props or state?**
If yes:
Calculate it during render.
Is this happening because the user clicked, submitted, selected, or interacted with something?
If yes:
**Put it in the event handler.
Is this an expensive calculation?**
If yes:
**Start with normal render logic and consider useMemo only if optimization is actually needed.
Are you connecting to, subscribing to, or controlling something outside React?**
If yes:
useEffect may be the right tool.
This simple decision tree can eliminate a lot of unnecessary effects.
Our Take
At Qodors, the approach is simple.
We don’t treat useEffect as the default place for logic that needs to happen after render.
If a value can be calculated from props or state, we calculate it during render. If something happens because of a user interaction, we handle it in the event handler. We use useEffect when the component genuinely needs to synchronize with something outside React.
Derived data does not need its own state.
Filtering, sorting, formatting, calculating totals, combining values, and selecting items can usually happen directly during render.
Using an effect for these cases often creates an unnecessary render cycle:
render → effect → setState → render again
The code may work, but it introduces additional complexity and creates another piece of state that can become out of sync.
For expensive calculations, useMemo can be considered when there is an actual performance reason. But useMemo should optimize a calculation — it should not be used as a replacement for understanding where that calculation belongs.
The goal isn’t to use fewer hooks just for the sake of using fewer hooks.
The goal is to make the data flow obvious.
Modern React development is not about putting every piece of logic into useEffect. It’s about understanding what belongs in render, what belongs in an event handler, and what genuinely needs synchronization with an external system.
Quick Reference
- Derived value from props/state → calculate it during render
- Filtering / sorting / mapping data → calculate it during render
- Expensive calculation → consider useMemo only when there is a real performance need
- User interaction → handle it in the event handler
- API / subscription / timer / external system synchronization → useEffect may be appropriate
- Setting state inside an effect just to derive another value → usually a sign that you don’t need the effect
- useEffect is not a general-purpose “run this after render” mechanism → use it for synchronization and side effects
- If your component has useEffect everywhere → stop and ask whether each effect is actually synchronizing with something outside React
So, should you stop using useEffect?
No. You should stop using it for things that don’t need it.
If React can calculate something during render, let React calculate it.
If the user triggers an action, handle it where the action happens.
And when you genuinely need to synchronize with something outside React, that’s where useEffect earns its place.
Less effect-driven code usually means fewer synchronization problems, clearer data flow, and React components that are much easier to understand.
Written by the team at Qodors — we build and simplify modern React systems for a living. → www.qodors.com