Stop Using useEffect for Data Fetching
Cover photo by Team Nocoloco on Unsplash
The Problem With useEffect
Most of us learned, years ago, that useEffect is the place for side effects, including fetching data. It seems logical at first. You trigger a request when a component mounts, store the response in state, and handle the loading or error states manually. However, this approach quickly leads to a mess of boilerplate code, race conditions, and poor performance.
Dealing with Race Conditions
When you use useEffect to fetch data, you often run into race conditions. If a user triggers a request and then updates a filter before the first request finishes, the old request might resolve after the new one. This causes your UI to display outdated data. To fix this, you have to write cleanup logic, like this:
useEffect(() => { let ignore = false; fetchData().then(result => { if (!ignore) { setData(result); } }); return () => { ignore = true; };}, [id]);This is a band-aid, not a solution. It adds cognitive load to every single fetch operation.
The Lack of Caching
useEffect does not know anything about caching. Every time your component re-mounts, the request fires again. If you have two components that need the same user data, you end up making two separate network requests unless you jump through hoops to lift that state up to a complex context provider. This is inefficient and makes your application feel sluggish.
Moving to Dedicated Tools
Modern React development has moved toward dedicated data fetching libraries like TanStack Query. These tools handle caching, background refetching, deduplication, and loading states out of the box. Instead of managing state manually, you declare your intent.
const { data, isLoading, error } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id),});With just a few lines, you get built-in error handling, automatic retries, and shared state across the entire component tree. You stop worrying about whether the data is fresh or if a race condition will break your UI.
Why You Still Might Hesitate
Some developers prefer keeping their dependencies low. They worry about adding another library to their package.json. While that is a valid concern, the cost of maintaining custom data-fetching logic in useEffect far outweighs the cost of adding a stable, battle-tested library. You end up writing fewer lines of code, and the lines you do write are significantly easier to read and debug.
When is useEffect actually okay?
useEffect has its place. Use it for syncing with non-React systems, like manually interacting with the DOM or subscribing to external event listeners that your library doesn’t handle. But when it comes to fetching data from an API, your application will be more reliable, faster, and easier to maintain if you use a tool built specifically for the job. Stop reinventing the wheel and start focusing on the actual features that your users care about.