Lesson 16 — React Query / TanStack Query (Server State Management)

🧭 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
});

OptionPurpose
staleTimeData freshness
cacheTimeHow long unused cache stays
refetchOnWindowFocusAuto 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 CaseTool
UI stateContext / Redux
Auth UI stateContext
Server dataReact Query
Complex client logicRedux Toolkit
API cachingReact 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)

Leave a Comment