TanStack Query: Deep Dive into Modern Server State Management
Server state is not client state, and treating it as though it were is where most data fetching code goes wrong. A look at what TanStack Query is actually doing underneath.
- tanstack
- state-management
- react

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:
useEffectwas 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, anddatastates - No caching: If you navigate away and come back, the data is fetched again from scratch
- Race conditions: If
userIdchanges 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:
- Intelligent caching: Data is cached by query keys and reused across components
- Automatic refetching: Refetches data when the window regains focus, network reconnects, or at specified intervals
- Query deduplication: Multiple components requesting the same data trigger only one network request
- Background updates: Updates stale data in the background without blocking the UI
- Optimistic updates: Update the UI immediately before the server confirms
- Built-in loading and error states: No need for manual state management
- 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: trueThis enables Gzip/Brotli compression to reduce the size of responses sent to the client.
Disable “X-Powered-By” Header
poweredByHeader: falseRemoves the X-Powered-By: Next.js header from responses — a small security and performance best practice.
React Strict Mode
reactStrictMode: trueEnables 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;