When I started learning React, I thought useEffect was the solution to almost everything. To derive state, to fetch data, to synchronize props or calculate a value, the answer for all this was useEffect.
After working on larger applications, i realized most of those effects weren't needed at all.
Today, I probably write 80% fewer useEffects than I did a few years ago. Not because it's a bad hook but because most problems have simpler solutions.
This article is the first part of my Modern React Patterns series, where I go over patterns that worked fine in small demos but became painful in production.
What useEffect is actually for
The React documentation describes effects as a way to synchronize your component with external systems.
That means things like:
- network requests
- WebSockets
- timers
- browser APIs
- subscriptions
- third-party libraries
If your effect isn't interacting with something outside React, there's a good chance you don't need one.
1. Don't use useEffect for
One of the most common examples I still see is this:
const [fullName, setFullName]=useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
It works but React now renders twice. The process that now is doing is the following:
render --> effect runs --> state changes --> render again
and there's no reason for that.
Instead i do this:
const fullName=`${firstName} ${lastName}`;
2. Don't synchronize props into state
const [user, setUser] = useState(props.user);
useEffect(() => {
setuser(props.user);
}, [props.user]);
Most of the times this creates two sources of truth. Eventually one of them gets updated and the other doesn't. Unless you intentionally want a local editable copy, just use the prop directly. It's much simpler
function prodile({user}){
return <h2>{user.name}</h2>;
}
3. Don't calculate values inside an effect
I've seen code like this many times.
const[filteredUsers, setFilteredUsers]=useStat([]);
useEffect(() => {
setFilteredUsers(
users.filter(user=> user.active)
);
}, [users]);
That's unnecessary state, i'ts way better:
const filteredUsers=users.filter(
user=> user.active
);
If the computation is actually expensive:
const filteredUsers=useMemo(() => {
return users.filter(user=> user.active):
}, [users]);
Notice that useMemo is an optimization, not a replacement for every calculation.
4. Debugging is one of the few places where I still use effects a lot
When debugging state updates I often do this:
useEffect(() => {
console.log(user);
}, [user]);
It's incredibly useful while debugging but before merging the code, delete it.
5. Fetching data with useEffect isn't always the best choice anymore
Years ago almost every React project looked like this:
useEffect(() => {
fetch("/api/users")
.then(res=> res.json())
.then(setUsers);
}, []);
Nowadays we have much better alternatives. Libraries like TanStack Query or SWR solve things that you eventually end up building yourself:
- caching
- retries
- background refetching
- loading state
- error state
- deduplication And instead of all of this, you can:
const {data, isLoading}= useQuery({
queryKey: ["users"]
queryFn: getUsers
});
That's less code and you get fewer bugs and better developer experience.
If you're interested in API reliability, I wrote another article about common integration mistakes:
"Building Reliable API integrations in Modern Web Applications"
6. Effects ofter hide architecture problems
Something I've noticed over time is whenever I find myself writing lots of effects indife the same component and it's usually because the component is trying to do too much. For example:
- fetching
- filtering
- sorting
- formatting
- validation
- event handling
- rendering
and everything together. Splitting responsabilities into smaller hooks or components ofter removes half of those effects automatically.
When you SHOULD use useEffect
None of this means "never use useEffect". There are plenty of legitimate use cases to use it, and let me tell some of them.
WebSocket connections:
useEffect(() => {
const socket = new WebSocket(url);
return() => socket.close();
}, []);
Timers:
useEffect(() => {
const id= setInterval(fetchData, 5000);
return() => clearInterval(id);
}, []);
Browser APIs:
useEffect(() => {
window.addEventListener("resize", onResize);
return() => {
window.removeEventListener("resize", onResize);
};
}, []);
Third-party libraries:
useEffect(() => {
const chart= new Chart(canvas);
return()=> chart.destroy();
}, []);
These are exactly the kinds of external systems effects were designed for.
My personal rule
Whenever I'm about to write an effect, I stop for a second and ask myself if am I synchronizing with an external system or am I compensating for my component design. That question alone has removed a surprising amount of unnecessary code from my projects.
Conclusion
useEffect isn't bad. It's just much easier to overuse than most React hooks. Today I try to use:
- derived values instead of derived state
- props instead of synchronized state
- TanStack Query for server state
- custom hooks for reusable logic
- effects only when I actually need to synchronize with something outside React
And you can get fewer renders, less state, fewer bugs and components that are easier to understand months later.
Next article in this series
part 2: Stop Fetching Data Inside useEffect
Thanks for reading!! If you're looking for help with an existing project or need someone to solve a tricky frontend/backend issue, you can also find my freelance profiles through my Dev.to profile.
Top comments (0)