srvjha

TanStack Query: Deep Dive into Modern Server State Management

08/05/2025

7 min read

tanstack

state-managment

react

source: Banner

What is TanStack Query?

TanStack Query (formerly React Query) is a powerful asynchronous state management library specifically designed for managing server-side data in modern applications. It handles data fetching, caching, synchronization, and updates with minimal boilerplate code, solving many pain points that developers face when working with remote data.

Core capabilities:

  • Automatic caching with intelligent cache management
  • Background data refetching and synchronization
  • Request deduplication
  • Optimistic updates
  • Pagination and infinite scroll support
  • Built-in loading and error states
  • Automatic garbage collection of unused data

Why is TanStack Query Required?

The Traditional Approach and Its Problems

Before TanStack Query, developers typically managed server data using one of these approaches:

1. The useState + useEffect Pattern

*// Traditional approach - lots of boilerplate and issues*
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

Problems with this approach:

  • useEffect wasn't designed for data fetching: useEffect was built for handling side effects (like subscriptions, DOM manipulation), not for managing complex asynchronous data fetching logic
  • Massive boilerplate: Every component needs its own loading, error, and data states
  • No caching: If you navigate away and come back, the data is fetched again from scratch
  • Race conditions: If userId changes rapidly, older requests might resolve after newer ones, causing stale data to overwrite fresh data
  • No refetching logic: You need to manually implement logic for refetching when the window regains focus or when the network reconnects
  • Memory leaks: Forgetting to clean up can cause updates to unmounted components

2. Using General-Purpose State Management (Redux, Zustand, etc.)

// Redux approach - treating async data as client state
const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, loading: false, error: null },
  reducers: {
    fetchUserStart: (state) => { state.loading = true; },
    fetchUserSuccess: (state, action) => { 
      state.data = action.payload; 
      state.loading = false; 
    },
    fetchUserFailure: (state, action) => { 
      state.error = action.payload; 
      state.loading = false; 
    }
  }
});

*// Thunk for fetching*
const fetchUser = (userId) => async (dispatch) => {
  dispatch(fetchUserStart());
  try {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    dispatch(fetchUserSuccess(data));
  } catch (error) {
    dispatch(fetchUserFailure(error.message));
  }
};`

Problems with this approach:

  • Treating server state as client state: Libraries like Redux and Zustand are excellent for managing client state (UI state, user preferences, form data), but they treat asynchronous server data the same way, which creates problems
  • No automatic caching strategy: You need to manually implement cache invalidation logic
  • No background refetching: Data can become stale without you knowing
  • No request deduplication: Multiple components requesting the same data will trigger multiple API calls
  • Excessive boilerplate: Lots of actions, reducers, and middleware just to fetch and cache data

Server State vs Client State: The Fundamental Difference

The core issue is that server state and client state are fundamentally different and require different management strategies.

Client State

  • Owned by your application: You have complete control
  • Synchronous: Updates happen immediately in your app
  • Examples: Form inputs, modal open/close state, UI theme, selected tabs

Server State

  • Persisted remotely: Lives on a server you may not control or own
  • Asynchronous: Requires async APIs (fetch, axios) for fetching and updating
  • Shared ownership: Can be changed by other users or systems without your knowledge
  • Can become outdated: The data you fetched 5 minutes ago might already be stale

Real-World Example: E-commerce Product Inventory

 Imagine an e-commerce site showing product inventory
 Multiple users are viewing the same product page
 User A sees: "10 items in stock"
 User B buys 3 items
 User C buys 2 items
 User A still sees: "10 items in stock" ❌ (outdated!)
 Actual inventory: 5 items
 Without proper server state management, User A might try to buy
 10 items and face an error at checkout*`

This is why server state needs:

  • Cache invalidation: Knowing when data is stale
  • Background refetching: Automatically updating when data changes
  • Real-time or periodic synchronization: Keeping the UI in sync with the server

How TanStack Query Solves These Problems

*// TanStack Query approach - clean and powerful*
import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
    staleTime: 5 * 60 * 1000, *// Consider data fresh for 5 minutes*
    refetchOnWindowFocus: true, *// Refetch when user returns to tab*
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

What TanStack Query provides automatically:

  1. Intelligent caching: Data is cached by query keys and reused across components
  2. Automatic refetching: Refetches data when the window regains focus, network reconnects, or at specified intervals
  3. Query deduplication: Multiple components requesting the same data trigger only one network request
  4. Background updates: Updates stale data in the background without blocking the UI
  5. Optimistic updates: Update the UI immediately before the server confirms
  6. Built-in loading and error states: No need for manual state management
  7. Garbage collection: Automatically cleans up unused cached data

Summary

TanStack Query exists because server state is fundamentally different from client state and requires specialized tools. Traditional approaches like useState + useEffect or general-purpose state managers like Redux weren't designed for the unique challenges of async server data: remote persistence, shared ownership, asynchronous operations, and the constant risk of data becoming stale.

TanStack Query provides a purpose-built solution that handles caching, synchronization, refetching, and all the complexity of server state management, letting you focus on building features instead of managing infrastructure.

Next js best practices

in next config ts

images: {
  remotePatterns: [...],
  formats: ["image/avif", "image/webp"],
  deviceSizes: [...],
  imageSizes: [...],
  minimumCacheTTL: 60,
}

remote patterns allow nextjs to optmize external image domains so for exmaple cloudinary image url

inside remote pattern obj

{
  protocol: "https",
  hostname: "res.cloudinary.com",
  pathname: "/sauravjha/**",
}

Only images from https://res.cloudinary.com/sauravjha/... will be optimized.

format ,devicesize , images size will be given basd on different size to optmize bs cache wala smjh lo minimumCacheTTL → Minimum cache time (in seconds) for optimized images — here, 60 seconds.

Compression

compress: true

This enables Gzip/Brotli compression to reduce the size of responses sent to the client.

Disable “X-Powered-By” Header

poweredByHeader: false

Removes the X-Powered-By: Next.js header from responses — a small security and performance best practice.

React Strict Mode

reactStrictMode: true

Enables React’s Strict Mode in development to help detect side effects, deprecated APIs, and potential bugs.

Experimental Optimizations

experimental: {
  optimizePackageImports: ["lucide-react", "framer-motion"],
}

This tells Next.js to optimize imports for these packages, so it only bundles what’s actually used.

Example: instead of importing all icons from lucide-react, only the ones you use get bundled — improving performance and bundle size.

sbse jyda hum packages use krte hai toh optmizepackageimport should be there in all

sample next.config.ts for learning

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // Image optimization
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "res.cloudinary.com",
        pathname: "/sauravjha/**",
      },
      {
        protocol: "https",
        hostname: "www.pinecone.io",
        pathname: "/images/**",
      },
    ],
    formats: ["image/avif", "image/webp"],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60,
  },
  // Compression
  compress: true,
  // Production optimizations
  poweredByHeader: false,
  // React strict mode
  reactStrictMode: true,
  // Experimental features for better performance
  experimental: {
    optimizePackageImports: ["lucide-react", "framer-motion"],
  },
};

export default nextConfig;