React Query (TanStack Query) Server State Management

Zaheer Ahmad 5 min read min read
Python
React Query (TanStack Query) Server State Management

Introduction

React applications often need to fetch and manage data from APIs—whether it's user profiles, product listings, or dashboards. Traditionally, developers used tools like useEffect and useState to handle this, but this approach becomes messy and hard to maintain as apps grow.

This is where React Query (now known as TanStack Query) comes in. It is a powerful library for managing server state in React applications. Server state refers to data that lives on a server (like a database or API) and is fetched, cached, and updated in your app.

With React Query v5, developers can:

  • Automatically cache API data
  • Keep UI in sync with server data
  • Perform background updates
  • Handle loading and error states easily

For Pakistani students building real-world apps (like e-commerce platforms in Karachi or student portals in Lahore), mastering TanStack Query can significantly improve performance and developer experience.

Prerequisites

Before starting this tutorial, you should be comfortable with:

  • Basic JavaScript (ES6+)
  • React fundamentals (components, props, state)
  • React Hooks (useState, useEffect)
  • API calls using fetch or axios
  • Basic understanding of async/await

Core Concepts & Explanation

Server State vs Client State

In React, there are two types of state:

  • Client State → Managed locally (e.g., form inputs, UI toggles)
  • Server State → Comes from an API (e.g., user data, products)

React Query focuses on server state.

Example:

  • Ahmad builds an app in Islamabad to show weather data.
  • The weather data comes from an API → this is server state.

React Query helps manage:

  • Fetching data
  • Caching it
  • Keeping it fresh

Query Client and Provider

Before using React Query, you need to set up a QueryClient.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyComponent />
    </QueryClientProvider>
  );
}

Explanation:

  • QueryClient → Creates a central store for all queries
  • QueryClientProvider → Makes it available across the app
  • client={queryClient} → Passes the instance

Fetching Data with useQuery

The useQuery hook is used to fetch data.

import { useQuery } from '@tanstack/react-query';

function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error occurred</p>;

  return <div>{data.length} users found</div>;
}

Explanation:

  • queryKey → Unique identifier for caching
  • queryFn → Function that fetches data
  • data → API response
  • isLoading → Loading state
  • error → Error state

Mutations with useMutation

Mutations are used for creating, updating, or deleting data.

import { useMutation } from '@tanstack/react-query';

function AddUser() {
  const mutation = useMutation({
    mutationFn: addUser
  });

  return (
    <button onClick={() => mutation.mutate({ name: 'Ali' })}>
      Add User
    </button>
  );
}

Explanation:

  • useMutation → Handles POST/PUT/DELETE
  • mutationFn → API call function
  • mutate() → Triggers the mutation

Caching and Background Refetching

React Query automatically:

  • Caches data
  • Refetches in the background
  • Keeps UI updated

Example:

  • Fatima opens a product list in Lahore
  • Data is cached
  • When she revisits → instant load + background update


Practical Code Examples

Example 1: Fetching Students Data

import { useQuery } from '@tanstack/react-query';

async function fetchStudents() {
  const response = await fetch('https://api.example.com/students');
  return response.json();
}

function StudentsList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['students'],
    queryFn: fetchStudents
  });

  if (isLoading) return <p>Loading students...</p>;
  if (error) return <p>Error loading students</p>;

  return (
    <ul>
      {data.map((student) => (
        <li key={student.id}>{student.name}</li>
      ))}
    </ul>
  );
}

Line-by-line Explanation:

  • fetchStudents() → Fetches data from API
  • await fetch() → Makes async request
  • useQuery() → Calls API and caches result
  • queryKey: ['students'] → Unique identifier
  • data.map() → Loops through students
  • key={student.id} → Required for React list

Example 2: Real-World Application (E-commerce Orders)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

const fetchOrders = async () => {
  const res = await fetch('/api/orders');
  return res.json();
};

const updateOrderStatus = async (orderId) => {
  await fetch(`/api/orders/${orderId}`, {
    method: 'PUT'
  });
};

function Orders() {
  const queryClient = useQueryClient();

  const { data } = useQuery({
    queryKey: ['orders'],
    queryFn: fetchOrders
  });

  const mutation = useMutation({
    mutationFn: updateOrderStatus,
    onSuccess: () => {
      queryClient.invalidateQueries(['orders']);
    }
  });

  return (
    <div>
      {data?.map((order) => (
        <div key={order.id}>
          <p>{order.name} - PKR {order.price}</p>
          <button onClick={() => mutation.mutate(order.id)}>
            Mark as Delivered
          </button>
        </div>
      ))}
    </div>
  );
}

Line-by-line Explanation:

  • useQueryClient() → Access cache
  • fetchOrders() → Get orders
  • useQuery() → Load and cache orders
  • useMutation() → Update order
  • onSuccess() → Runs after mutation
  • invalidateQueries() → Refetch updated data
  • mutation.mutate(order.id) → Trigger update


Common Mistakes & How to Avoid Them

Mistake 1: Not Using queryKey Properly

❌ Wrong:

useQuery({ queryKey: 'users', queryFn: fetchUsers });

✅ Correct:

useQuery({ queryKey: ['users'], queryFn: fetchUsers });

Fix Explanation:

  • Always use an array for queryKey
  • Helps React Query manage caching better

Mistake 2: Forgetting to Invalidate Queries

❌ Wrong:

mutation.mutate(data);

(No refetch after update)

✅ Correct:

onSuccess: () => {
  queryClient.invalidateQueries(['users']);
}

Fix Explanation:

  • Without invalidation, UI shows stale data
  • Always refetch after mutation


Practice Exercises

Exercise 1: Fetch Products

Problem:
Fetch product data from an API and display product names.

Solution:

const fetchProducts = async () => {
  const res = await fetch('/api/products');
  return res.json();
};

function Products() {
  const { data } = useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts
  });

  return (
    <ul>
      {data?.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

Explanation:

  • API fetch
  • useQuery handles caching
  • map renders products

Exercise 2: Add New User

Problem:
Create a form to add a new user.

Solution:

const addUser = async (user) => {
  await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(user)
  });
};

function AddUser() {
  const mutation = useMutation({ mutationFn: addUser });

  return (
    <button onClick={() => mutation.mutate({ name: 'Fatima' })}>
      Add User
    </button>
  );
}

Explanation:

  • addUser() → API POST request
  • useMutation() → Handles submission
  • mutate() → Sends data

Frequently Asked Questions

What is React Query (TanStack Query)?

React Query is a library for managing server state in React applications. It handles data fetching, caching, and synchronization automatically.

How do I install React Query v5?

You can install it using npm:
npm install @tanstack/react-query. Then wrap your app with QueryClientProvider.

What is queryKey in React Query?

queryKey is a unique identifier for each query. It helps React Query cache and manage data efficiently.

How do I refetch data manually?

You can use refetch() returned by useQuery or use queryClient.invalidateQueries().

Is React Query better than useEffect?

Yes, for server state management. It reduces boilerplate and provides caching, background updates, and better performance.


Summary & Key Takeaways

  • React Query (TanStack Query) simplifies server state management
  • useQuery is used for fetching data
  • useMutation is used for updating data
  • Automatic caching improves performance
  • Query invalidation ensures fresh data
  • React Query reduces need for manual state management

To continue your learning journey, explore these tutorials on theiqra.edu.pk:

  • Learn how to connect APIs in React & API Integration
  • Master state handling with React Hooks
  • Understand performance with React Performance Optimization
  • Build full apps with Full-Stack Web Development Guide

These will help you become a confident React developer ready to build real-world applications in Pakistan and beyond 🚀

Practice the code examples from this tutorial
Open Compiler
Share this tutorial:

Test Your Python Knowledge!

Finished reading? Take a quick quiz to see how much you've learned from this tutorial.

Start Python Quiz

About Zaheer Ahmad