After shipping three production apps with useEffect-driven fetching, I switched entirely to React Query. Here is what changed, what broke, and what I will never go back to.
The moment I understood why useEffect is the wrong tool for data fetching was when I spent four hours debugging a race condition in our search bar.
Two rapid keystrokes fired two concurrent requests. The slower one resolved last — overwriting the correct result with stale data. The fix was ugly: a cleanup function with a cancelled flag, a ref to track the latest request, and a comment explaining why this code looks the way it does. It worked. But every new engineer who touched that file needed a walkthrough.
Switching to React Query solved this class of problem entirely. Automatic request deduplication, stale-while-revalidate caching, and background refetching came for free. The mental model shifted from "I manage side effects" to "I declare what data I need." That distinction matters enormously at scale.
What I gave up: the illusion of control. What I gained: data that was always fresh, loading states that were always accurate, and error handling that was actually handled — not optimistically ignored.