🧭 Introduction
So far, you’ve learned how to manage client-side state using:
- Context API
- Redux Toolkit
- Normalized state
But modern React applications deal with a different kind of state that behaves very differently:
👉 Server State
This includes:
- API data
- Cached responses
- Pagination results
- Background refetching
- Syncing UI with backend changes
Trying to manage server state with Redux or Context leads to:
❌ Excessive boilerplate
❌ Manual loading & error handling
❌ Cache invalidation bugs
This is exactly why **React Query (now called TanStack Query) exists.
🎯 What You’ll Learn in This Lesson
By the end of this lesson, you will understand:
- What server state really is
- Why Redux is not ideal for server state
- Core concepts of React Query
- How caching, refetching, and invalidation work
- When to use React Query vs Redux
🧠 Client State vs Server State (Critical Difference)
🟢 Client State
- Lives only in the browser
- Controlled fully by your app
- Examples: theme, modal state, auth UI
🔵 Server State
- Lives on the backend
- Can change without user interaction
- Can become stale
- Requires syncing
👉 React Query is designed ONLY for server state.
❌ Why Redux Is Not Ideal for Server State
With Redux, you must manually handle:
- Loading flags
- Error states
- Caching
- Refetching
- Background updates
This leads to:
- Large reducers
- Repeated logic
- Bugs related to stale data
Redux is excellent for client state, not server state.
✅ What React Query Solves
React Query gives you:
✅ Automatic caching
✅ Background refetching
✅ Built-in loading & error states
✅ Request deduplication
✅ Pagination & infinite queries
✅ Mutation handling
All without reducers or actions.
🧩 Installing React Query
npm install @tanstack/react-query
🏗️ Setting Up QueryClient
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
👉 Similar to Redux Provider, but simpler.
🔍 Your First Query (useQuery)
import { useQuery } from "@tanstack/react-query";
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ["users"],
queryFn: () =>
fetch("/api/users").then(res => res.json())
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
✅ No useEffect
✅ No local loading state
✅ Clean component
🧠 queryKey — The Heart of React Query
queryKey: ["users"]
Think of queryKey as:
A unique cache identifier
Different keys = different cache entries.
Examples:
["users"]
["users", userId]
["posts", { page: 1 }]
🔁 Caching & Refetching (Magic Explained)
By default:
- Data is cached
- React Query refetches when:
- Window regains focus
- Network reconnects
This keeps UI always fresh.
⚙️ Important Query Options
useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
staleTime: 5000,
cacheTime: 60000,
refetchOnWindowFocus: false
});
| Option | Purpose |
|---|---|
| staleTime | Data freshness |
| cacheTime | How long unused cache stays |
| refetchOnWindowFocus | Auto refetch |
✍️ Mutations — Changing Server Data
Creating a Mutation
import { useMutation, useQueryClient } from "@tanstack/react-query";
function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: newUser =>
fetch("/api/users", {
method: "POST",
body: JSON.stringify(newUser)
}),
onSuccess: () => {
queryClient.invalidateQueries(["users"]);
}
});
return (
<button onClick={() => mutation.mutate({ name: "Avni" })}>
Add User
</button>
);
}
👉 invalidateQueries triggers automatic refetch.
🧠 Why Invalidation Is Powerful
Instead of manually updating Redux state:
- You tell React Query the data is stale
- It refetches automatically
This eliminates:
❌ Manual syncing bugs
❌ Inconsistent UI
🔄 Pagination Example (Conceptual)
queryKey: ["posts", page]
Change page → React Query fetches new data
Old pages remain cached.
🆚 React Query vs Redux Toolkit (Clear Answer)
| Use Case | Tool |
|---|---|
| UI state | Context / Redux |
| Auth UI state | Context |
| Server data | React Query |
| Complex client logic | Redux Toolkit |
| API caching | React Query |
👉 They are complementary, not competitors.
🚨 Common Mistakes
❌ Using React Query for Local UI State
It’s not meant for that.
❌ Disabling Caching Without Reason
Caching is the biggest benefit.
❌ Mixing Redux & React Query Incorrectly
Each has a clear role.
🧠 Real-World Architecture (Recommended)
React Query → Server State
Redux Toolkit → Client State
Context → Global UI State
This is how large production apps are built.
🎯 Best Practices (Senior-Level)
✅ Use React Query for all API data
✅ Keep queryKeys consistent
✅ Let React Query manage loading/error
✅ Invalidate instead of manually updating
✅ Combine with Redux thoughtfully
❓ FAQs — React Query
🔹 Is React Query a replacement for Redux?
No — it replaces Redux for server state only.
🔹 Is React Query production-ready?
Yes — widely used in enterprise apps.
🔹 Should beginners learn React Query?
Yes, after understanding basic state.
🔹 Does it work with Axios?
Yes, perfectly.
🧠 Quick Recap
✔ Server state ≠ client state
✔ React Query handles server state beautifully
✔ Caching & refetching are automatic
✔ Mutations invalidate queries
✔ Redux & React Query work together
🎉 Conclusion
React Query changes how you think about data.
Instead of:
“How do I store this API response?”
You start thinking:
“How fresh should this data be?”
That shift is what separates junior apps from professional systems ⚛️🧠
👉 Next Lesson
Lesson 17 — Memoization Deep Dive (React.memo, useMemo, useCallback)