I'm Losing My Mind with useEffect

July 10, 2025 (3d ago)

There must be a better way.

[— Every developer staring at a messy useEffect hook 🥴]

Let me be real with you. One of the most frustrating things I encounter as a developer is opening up a component written by someone else and finding a tangled mess of useEffect hooks for data fetching. I see multiple useEffects, complex dependency arrays, and manual loading and error states. It's a recipe for bugs, unnecessary re-renders, and a whole lot of headaches.

I used to think this was just how you did things in React. But I've come to a firm conclusion: it's time to stop using useEffect for data fetching.

The useEffect Data Fetching Nightmare

On the surface, using useEffect for API calls seems simple enough. You mount the component, you fetch the data. But the reality is so much more complicated.

Let's look at a "simple" data fetching useEffect:

import { useState, useEffect } from 'react';
 
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    // Reset states on userId change
    setLoading(true);
    setError(null);
 
    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
 
    fetchUser();
  }, [userId]); // Don't forget the dependency array!
 
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
 
  return <div>{user.name}</div>;
}

This looks okay, right? But we're already missing so much:

  • No Caching: If you navigate away from this component and come back, it's going to re-fetch the data every single time, even if nothing has changed.
  • No Re-fetching on Window Focus: What if the user leaves the tab open, the data changes on the server, and they come back? They'll be looking at stale data.
  • Race Conditions: What if the userId changes quickly? You could have two requests fire off, and the first one might return after the second one, leading to the wrong user data being displayed. You need to add even more code (a cleanup function with an isMounted check) to handle this.

This is a ton of boilerplate to manage for one simple API call. Now imagine a component with three or four of these. It's unmaintainable.

There is a Better Way: useQuery

This is where libraries like TanStack Query (formerly React Query) come in. They are designed specifically for server state management. They handle all the complexities of data fetching for you, so you can focus on building your UI.

Let's rewrite our UserProfile component using useQuery:

import { useQuery } from '@tanstack/react-query';
 
const fetchUser = async userId => {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};
 
function UserProfile({ userId }) {
  const {
    data: user,
    isLoading,
    isError,
    error,
  } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
 
  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.message}</div>;
 
  return <div>{user.name}</div>;
}

Look at how much cleaner that is! All that complex logic is gone. And what do we get out of the box?

  • Caching: useQuery will automatically cache the data. If you request the same user again, it will return the cached data instantly while re-fetching in the background to keep it fresh.
  • Automatic Re-fetching: It will re-fetch on window focus, on network reconnect, and on an interval you can configure.
  • Loading and Error States: Handled for you.
  • No Race Conditions: The library is built to handle these edge cases.

A Real-World Horror Story

I learned this lesson the hard way on a project at Rastek.id. The codebase was filled with useEffect for data fetching. At first, I didn't think much of it. The app seemed to work fine, and I had other priorities.

But as the app grew, I started noticing performance issues. The UI felt sluggish, and the server costs were climbing. I finally dug into our monitoring tools and was shocked by what I found: our server was being bombarded with hundreds of identical requests. Components were re-rendering, and each time, their useEffect hooks were firing off new API calls without any caching.

It was a nightmare. We were DDoSing ourselves with our own front-end code which is rediciolous.

That was the moment I decided to refactor everything. I ripped out all the useEffect data fetching logic and replaced it with TanStack Query. The results were staggering:

  • The number of requests to our server dropped dramatically.
  • The app's performance increased by what felt like 3x. The UI was snappy and responsive.
  • Our code became so much cleaner and easier to reason about.

That experience taught me that ignoring how you manage server state isn't just a bad practice; it can have a real, measurable impact on your product and your infrastructure.

useEffect Still Has Its Place

I'm not saying useEffect is useless. It's still the perfect tool for many things:

  • Managing subscriptions (e.g., to a WebSocket).
  • Adding and removing event listeners.
  • Triggering animations.

These are the "side effects" it was truly designed for. But when it comes to fetching, manipulating, and caching server state, it's simply the wrong tool for the job.

Conclusion: Let's Move On

As a community, we need to move past the useEffect for data fetching pattern. It was a necessary stepping stone, but we have better, more powerful tools now. Using a dedicated data-fetching library like TanStack Query will make your code cleaner, your app more performant, and your life as a developer so much easier.

So please, the next time you need to fetch data in a React component, reach for useQuery, not useEffect. Your future self (and any developer who has to maintain your code) will thank you.