Stop Using useEffect for API Calls, Use React Query Instead
If you have been building React apps for some time you probably wrote something like this a hundred times.
useEffect(() => {
fetchData();
}, []);Looks simple. But this simple pattern is causing more problems then most developers realize, specially when your app grows bigger and you start adding more api calls, more state, more loading and error handling.
I am a Mern stack developer and I used useEffect for api calls for years, until i actually sat down and compared it with Tanstack React Query. And honestly after using it, i don’t think i will go back to plain useEffect for data fetching ever again.
Let me explain why.
The Double API Call Problem
One thing that annoys almost every React developer at some point is that their api gets called twice on mount. You open the network tab and see the same request fired 2 times. This happens because of React StrictMode in development, it intentionally mounts your component twice to catch bugs.
Now this is fine in dev but it makes people confuse and also it hides a bigger issue, your useEffect is tightly coupled to component lifecycle so every time your component mounts it fires the call again, even if you already have the data. This means unnecessary load on your backend and slower experience for users specially on pages where the component re mounts often like tabs or modals.
React Query solves this problem out of the box. It has built in caching, so if the same query key already has fresh data it wont even hit the network again, it just gives you the cached result.
Managing Loading and Error State Manually is a Pain
With useEffect you also have to manually manage loading state, error state, and the data itself. So your component ends up looking like this.
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);Now multiply this by every api call in your app. Its repetitive, its boring, and its easy to make mistake, like forgetting to set loading back to false in the catch block.
React Query gives you isLoading, isError, data, error directly from the hook. No need to write this boilerplate again and again.
Dependency Array Headaches
Anyone who has worked with useEffect knows the pain of dependency arrays. You add a value in the effect but forget to add it in the array, and now you have a stale closure bug that took you 2 hours to debug. Or you add everything eslint tells you to add and now your effect runs on every single render because some object reference changed.
Get Hasnain alam’s stories in your inbox
Join Medium for free to get updates from this writer.
With React Query you don’t deal with this at all. You just give your query a key and a function, and it handles when to refetch based on things like component mount, window focus, or the staleTime you set. No dependency array guessing game.
Caching, Stale Time and Invalidation
This is where React Query really shines for me. Few features that changed how i build apps.
Cache: Once data is fetched for a query key, its stored. If you navigate away and come back, you get instant data from cache while it silently refetches in background if needed.
StaleTime: You can tell react query how long data should be considered “fresh”. If data is fresh it wont refetch even if component remounts. This alone reduces so much unnecessary backend load.
InvalidateQueries : When you do a mutation, like updating a user or creating a post, you can just invalidate the related query and react query will automatically refetch it for you. No manual state syncing, no manually calling fetch again after mutation.
These 3 things alone solve most of the pain points that come with manual useEffect based fetching.
A Real Example From My Own Project
I recently worked on a Next js SaaS platform where the dashboard had multiple api calls firing on every tab switch because of useEffect. It was calling the backend way more then it needed to be, and users were complaining about slow load times.
I replaced all of those useEffect calls with React Query, added proper staleTime and used invalidateQueries wherever there was a mutation. The result was a much smoother dashboard, less backend calls, and better user experience overall. You can check that project here, it was a Next.js SaaS Platform i built for managing mineral owner contact data.
So Should You Drop useEffect Completely
Not completely, useEffect is still useful for things that are not data fetching, like subscriptions, dom events, timers etc. But for api calls specially in a growing app, React Query is just a better tool for the job. It reduces backend load, removes boilerplate, and gives you cache and invalidation for free.
If you are still fetching data with plain useEffect i would say give React Query a try on one page first, you will notice the difference quickly.
Final Thoughts
As a Mern stack developer, i have seen a lot of projects suffer from unnecessary api calls just because of how useEffect was used for data fetching. Switching to React Query is one of those small changes that make a big difference in performance and developer experience both.
If you are working on a Mern or Next js project and want help optimizing your api layer or building a full app from scratch, feel free to check my work, i work as a Full stack Mern & Next.js developer and i have built multiple production apps using this exact approach.
I have covered some common issues with Tanstack here, Common React Query Issues, Query Key and Invalidate Queries Problems Explained, where i explained why invalidate queries sometime dont work and how to fix pagination, update and delete cases properly.
Thanks for reading, let me know in comments if you switched to react query and what difference you noticed.








