I used to reach for useEffect for everything. Data fetching? useEffect. Form validation? useEffect. Syncing state? useEffect. My components looked like chaos, and I couldn’t figure out why my code felt so hard to maintain.
Then I learned something that changed how I write React: useEffect is not the answer for most problems.
The Problem with useEffect
Here’s the thing about useEffect — it runs after every render by default. That means if you’re not careful, you’re creating infinite loops, racing conditions, and state that gets out of sync.
I spent hours debugging why my API was being called 10 times. Or why my form reset unexpectedly. The culprit was almost always useEffect doing something I didn’t intend.
What I Use Instead
1. Data Fetching: React Query or SWR
Stop fetching data in useEffect. Libraries like React Query (or TanStack Query) handle caching, refetching, loading states, and errors much better than anything you can write with useEffect.
// Before (useEffect)
useEffect(() => {
fetch('/api/users').then(setUsers);
}, []);
// After (React Query)
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
2. Derived State: Just Calculate It
If you have state that depends on other state, don’t sync it with useEffect. Just compute it during render.
// Before (useEffect)
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);
// After
const total = items.reduce((sum, item) => sum + item.price, 0);
3. Event Handlers: Handle Events Directly
Don’t use useEffect to respond to user actions. Use the handlers directly.
// Before
useEffect(() => {
const handleKeyDown = (e) => { ... };
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// After - use useEffect only when truly needed for subscriptions
When useEffect Actually Makes Sense
Look, I’m not saying never use useEffect. It has valid uses:
- Subscribing to external systems (WebSockets, browser APIs)
- Manually interacting with the DOM
- Logging analytics (though even that can be debatable)
But for most business logic in your components? There’s usually a cleaner way.
The Shift That Helped Me
Instead of asking “when does this code run?” I started asking “what does this code represent?”
If it’s data, use a data-fetching library. If it’s UI state, keep it simple. If it’s derived from props, compute it.
The result? My components got shorter, bugs got fewer, and I stopped fearing the React DevTools profiler.
Try it. Replace one complicated useEffect with the right tool and see how it feels.
