DEV Community

Alejandro
Alejandro

Posted on

Part 2: Stop Fetching Data Inside useEffect (what modern React apps do instead)

Part 2 of the Modern React Patterns series

In the previous article (Part 1: Why I Rarely Use useEffect Anymore (and what I use instead)) we looked at one of the most common reasons developers overuse useEffect: deriving state that React can already calculate during rendering.
This time I'd like to talk about another pattern that I see in almost every React codebase I've worked on: fetching data inside useEffect.
Just like derived state, this isn't technically wrong. In fact, it was the standard recommendation for years. Most of us learned React by writing data fetching exactly this way. The problem is that what starts as a perfectly reasonable solution, often becomes increasingly difficult to maintain as an application grows.


The way we learned

If you've been writing React for a while, you've probably written something very similar to this.

function UsersPage() {
    const [users, setUsers] = useState([]);
    useEffect(() => {
        fetch("/api/users")
            .then(res=> res.json())
            .then(setUsers);
    }, []);
    return <UsersList users={users} />;
}
Enter fullscreen mode Exit fullscreen mode

There's nothing fundamentally wrong with this component, but the request is made when the component mounts so the response is stored in state and the UI updates automatically. This works perfectly for a small application, but production applications rarely stays this simple.


The complexity doesn't appear immediately

One reason this pattern became so popular i that the first implementation is incredibly small. You write ten lines of code, the data appears on the screeen and you move on to the next feature. A few weeks later somebody asks for a loading spinner and then the backend starts returning validation errors so you need to retry failed requests.
Eventually somebody reports that users sometimes navigate away before the request finishes and little by little, the component starts accumulating responsibilities that have nothing to do with rendering. This is an example of problems that I've seen and lived, all this story, in code language, ends up in something like this:

const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
    const controller = new AbortController();
    fetch("/api/users", {
        signal: controller.signal,
    })
        .then(res = > {
            if (!res.ok){
                throw new Error("Request failed");
            }
            return res.json();
        })
        .then(setUsers)
        .catch(err => {
            if (err.name !== "AbortError"){
                setError(err);
            }
        })
        .finally(() => {
            setLoading(false);
        });
    return() => controller.abort();
}, []);
Enter fullscreen mode Exit fullscreen mode

This alone isn't particularly complicated but the issue is that almost every API request in your application starts looking exactly like this.


Same pattern everywhere

This is usually the point where the maintenance cost begins to show. Every page has a loading state, error state, request cancellation, retry logc and a duplicated fetch code.
If your application has twenty different API endpoints, you'll probably have twenty very similar Effects. At first this feels harmless because each component only knows about its own request. Over time you realize you're solving the same problems over and over again. The code isn't becoming more complex because your business logic is difficult, but because every component is independently trying to solve data synchronization.


Shared data exposes the problem

Imagine your application has an authenticated user. The navigation bar, the profile page and also the settings page needs it. A common implementation looks something like this:

useEffect(() => {
    fetch("/api/me")
        .then(res=> res.json())
        .then(setUser);
}, []);
Enter fullscreen mode Exit fullscreen mode

The component works, the next component copies the same Effect and then another one. Eventually three differents parts of the application are requesting exactly the same resource, even if the requests are fast, they're still duplicated work. More importantly, every component now owns its own loading state, its own error handling and its own copy of the fetched data; keeping everything synchronized becomes surprisingly difficult.


Server state behavior

This was probably the biggest shift in the way I think about React applications. For a long time I assumed I was managing React state but in reality I was managing server state. Those are completely different problems. React state belongs to your application and server state belongs somewhere else. I can become stale.
Several components can depend on it simultaneously. It might already exist in memory but it may need background refreshes, retries after network failures and an optimistic updates after a mutation.
None of those concerns are really about rendering components. They're about synchronizing your application with an external system.


That's why dedicated libraries exist

Once I understood that distinction, my approach changed completely. Instead of treating every API call as another useEffect, I started using tools that are specifically designed for server state. For example, the following code:

const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
    fetch("/api/users")
        .then(res=> res.json())
        .then(setUsers)
        .catch(setError)
        .finally(() => setLoading(false));
}, []);
Enter fullscreen mode Exit fullscreen mode

can become this other code:

const{
    data: users,
    isLoading,
    error,
} = useQuery({
    queryKey: ["users"],
    queryFn: fetchUsers,
});
Enter fullscreen mode Exit fullscreen mode

The shorter syntax isn't the interesting part, but is everything that disappears. All the problems: Caching, deduplication, background refetching, retries, request cancellation and stale data management, have been solved by libraries such as TanStack Query. Instead of rebuilding thhose features in every component, you can focus on your application's actual behavior.


Use of useEffect

useEffect is not wrong at all. I still fetch data inside an Effect from time to time. If I'm building an internal tool with one request, a quick prototype or a page that will probably never grow beyond a few hundred lines of code. In this cases adding another dependency often isn't worth it. The problem it's not using useEffect, is continuing to scale the same pattern after the application has clearly outgrown it.


My personal rule

Nowadays I try to separate two completely different situations. If I'm synchronizing React with something external (a WebSocket connection, a timer, a browser event, a third-party SDK, etc.), useEffect is almost always the correct tool. But if I'm fetching data that lives on a server, I immediately think about server-state management instead.
Making than distinction has dramatically reduced the number of Effects I write. More importantly, it has made my components much easier to read. Most of them are now focused almost entirely on rendering UI instead of coordinating network requests.


Conclusion

One thing I've noticed over the years is that developers rarely overuse useEffect because they misunderstand React. Most of us simply learned React at a time when fetching data inside Effects was considered the normal approach. The ecosystem evolved, our applications became larger, libraries specialized in solving server-state problems became mature and our habits, however, often stayed the same.
Looking back, reducing the amount of data fetching I perform inside useEffect has probably been one of the biggest improvements I've made to the way I structure React applications.


Next article in this series

Stop Syncing Props Into State


Previous articles in this series

Part 1: Why I Rarely Use useEffect Anymore (and what I use instead)



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)