Why Fetching Data Inside useEffect Is Not a Good Idea
Introduction
If you've ever sprinkled a fetch call inside a useEffect
and thought "hey, this works," you're not alone. I did the same thing when I first learned React. But after a while, I realized it's like trying to cook pasta in a coffee machine. Sure, it technically works, but it's messy and leaves a weird aftertaste.
Let’s talk about why using useEffect
for data fetching is usually not the best move and what you can do instead.
Why It Feels Natural (But Isn't)
When you start with React, the mental model is simple: component mounts, run useEffect
, fetch the data, set state, done. It feels clean, but behind the scenes, things get messy.
Problems You’ll Run Into
- Double fetching in Strict Mode
React’s development Strict Mode runs effects twice on mount. That means your fetch runs twice too, and suddenly you’re staring at duplicate API requests.
- Race conditions
Imagine navigating between pages quickly. One useEffect
kicks off a fetch, then another kicks off right after. If the slower one finishes last, it can overwrite your newer data. That's like getting last week's weather update instead of today’s.
- No built-in caching
Fetching in useEffect
means every mount re-fetches, even if the data hasn’t changed. It’s like asking a waiter for the menu every five minutes.
- Complicated cleanup
You might add AbortControllers or flags to prevent memory leaks. At that point, you’re spending more time babysitting effects than building features.
What to Do Instead
Option 1: React Query (TanStack Query)
A library like React Query is like hiring a personal assistant for your data. It caches, deduplicates, retries, and even keeps things fresh automatically.
1import { useQuery } from "@tanstack/react-query";
2
3function Todos() {
4 const { data, isLoading, error } = useQuery({
5 queryKey: ["todos"],
6 queryFn: () => fetch("/api/todos").then((res) => res.json()),
7 });
8
9 if (isLoading) return <p>Loading...</p>;
10 if (error) return <p>Something went wrong</p>;
11
12 return (
13 <ul>
14 {data.map((todo) => (
15 <li key={todo.id}>{todo.text}</li>
16 ))}
17 </ul>
18 );
19}
No more worrying about stale data or duplicate requests. React Query has your back.
Option 2: Next.js Server Components
If you’re in a Next.js project, server components are the shiny new toy. Fetch your data on the server, send it down with the HTML, and skip the client-side fetching headache.
1// app/page.tsx
2async function getTodos() {
3 const res = await fetch("https://jsonplaceholder.typicode.com/todos");
4 return res.json();
5}
6
7export default async function Page() {
8 const todos = await getTodos();
9 return (
10 <ul>
11 {todos.slice(0, 5).map((todo) => (
12 <li key={todo.id}>{todo.title}</li>
13 ))}
14 </ul>
15 );
16}
This way your users see content instantly, and search engines love you for it.
A Quick Analogy
Fetching in useEffect
is like making toast by holding bread over a candle. It works, but why would you do that when you’ve got a toaster sitting right there?
Conclusion
Yes, you can fetch data in useEffect
, but it quickly leads to bugs, wasted requests, and unnecessary complexity. Modern React has way better solutions like React Query or server components in Next.js. Once you switch, you’ll never want to go back.