useEffectis one of those React hooks that looks convenient until it quietly turns a component into something nobody wants to touch. I have written enough projects that were beaten up by messy effects to become a little cautious. This post is about when I useuseEffect, when I try not to use it, and how I clean things up when an effect is unavoidable.
What A Side Effect Is
In React, a side effect is anything that happens outside the pure rendering process. For example:
- Calling an API
- Mutating the DOM directly
- Subscribing to an external data source
- Setting a timer or event listener
These things are not rendering itself, but they can affect what the user eventually sees. React does not automatically know when they happen, souseEffectgives you an escape hatch.
What useEffect Looks Like
// Fetch data
useEffect(() => {
const fetchUserData = async () => {
const response = await fetch('/api/user')
const data = await response.json()
setUser(data)
}
fetchUserData()
}, []) // Empty dependency array: run once on mount
// Mutate the DOM
useEffect(() => {
const element = document.getElementById('my-element')
element.style.opacity = '1'
return () => {
element.style.opacity = '0'
}
}, [])
// Subscribe to an event
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
Declarative setup, cleanup functions, dependency tracking - it all looks nice. The trouble starts when one component has five or six effects, each watching a different dependency.
The Version That Hurts
I have written code like this before. Looking back, I want to stop my past self.
// Bad example
function UserDashboard() {
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
const [notifications, setNotifications] = useState([])
// Fetch user
useEffect(() => {
fetchUserProfile().then(setUser)
}, [])
// Fetch posts
useEffect(() => {
if (user) {
fetchUserPosts(user.id).then(setPosts)
}
}, [user])
// Fetch settings
useEffect(() => {
if (user) {
fetchUserSettings(user.id)
}
}, [user])
// Poll notifications
useEffect(() => {
if (!user) return
const timer = setInterval(() => {
checkNotifications(user.id).then(setNotifications)
}, 5000)
return () => clearInterval(timer)
}, [user])
}
There are four effects scattered through the component. Each one watches its own dependency. It is hard to see what runs first, what triggers what, and which update causes another render. Add some useCallback and useMemo, and the component enters the "do not touch this" zone.
How I Write It Now
My rule is simple: not every side effect needs to live directly inside the component. If I can move it out, I move it out. The usual options are:
- Put related side effects into a custom hook, so the component calls one line.
- Use React Query, SWR, or server components for data fetching instead of writing fetch logic in effects.
- When an effect is really needed, pay close attention to the dependency array and cleanup function.
Here is a cleaner version of the dashboard example:
// Better: a custom hook
function useUserData(userId) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let mounted = true
const fetchUser = async () => {
try {
setLoading(true)
const data = await fetchUserProfile(userId)
if (mounted) {
setUser(data)
setError(null)
}
} catch (err) {
if (mounted) {
setError(err)
setUser(null)
}
} finally {
if (mounted) {
setLoading(false)
}
}
}
fetchUser()
return () => {
mounted = false
}
}, [userId])
return { user, loading, error }
}
function UserDashboard({ userId }) {
const { user, loading, error } = useUserData(userId)
if (loading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
if (!user) return null
return (
<div>
<h1>Welcome, {user.name}!</h1>
{/* other UI */}
</div>
)
}
The mounted flag prevents state updates after the component unmounts. Without some form of cleanup, the red warning in the console will eventually find you.
Bottom Line
useEffect is not the problem. Using it for everything is the problem. Before I write a new effect, I now ask: "Does this truly need to happen after render?" If the answer is "not really," it probably belongs outside the component or inside a dedicated hook.
References
Follow updates
Use open feeds for new articles, research, and important updates.
- Author:LeoQin
- URL:https://leoqin.com/en/article/avoid-excessive-useeffect
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!