This repository demonstrates data fetching patterns in React, comparing traditional fetch methods with React Query, and showcasing React Query's powerful caching capabilities.
- Traditional Fetch Component - Manual state management with useState and fetch API
- React Query Component - Simplified data fetching with automatic caching
- Cache Demo Component - Interactive demonstration of React Query's caching system
# Install frontend dependencies
npm install
# Install server dependencies
cd server
npm installTerminal 1 - Start the Backend Server:
cd server
npm startServer runs on http://localhost:3000
Terminal 2 - Start the Frontend:
npm run devFrontend runs on http://localhost:5173
React Query's caching system is one of its most powerful features. Here's a step-by-step guide to understanding how it works:
Every query in React Query has a unique identifier called a queryKey:
useQuery({
queryKey: ["user", userId], // This is the cache key
queryFn: () => fetchUser(userId),
});How it works:
- The
queryKeyacts like a unique ID for your data - Format: An array of values:
["user", 1],["posts"],["profile", userId, "settings"] - React Query uses this key to store and retrieve cached data
- Same key = same cached data, different key = different cached data
Example:
["user", 1]- Cache entry for user 1["user", 2]- Cache entry for user 2["posts"]- Cache entry for all posts
When you first call useQuery:
const { data, isLoading } = useQuery({
queryKey: ["user", 1],
queryFn: () => fetchUser(1),
});What happens:
- React Query checks: "Do I have cached data for
["user", 1]?" - First time: No cache exists β Makes API call β Stores result in cache
- Second time: Cache exists β Returns cached data immediately β No API call!
Visual Flow:
First Call: queryKey β Cache Miss β API Call β Store in Cache β Return Data
Second Call: queryKey β Cache Hit β Return Cached Data (No API call!)
Multiple components using the same queryKey share the same cached data:
// Component A
const { data } = useQuery({
queryKey: ["user", 1],
queryFn: () => fetchUser(1),
});
// Component B (different component, same key)
const { data } = useQuery({
queryKey: ["user", 1], // Same key!
queryFn: () => fetchUser(1),
});Result: Only ONE API call is made! Both components share the cached data.
React Query distinguishes between "fresh" and "stale" data:
useQuery({
queryKey: ["user", 1],
queryFn: () => fetchUser(1),
staleTime: 30000, // 30 seconds
});Cache States:
- Fresh (0-30 seconds): Data is considered current, React Query won't refetch
- Stale (after 30 seconds): Data might be outdated, React Query will refetch in background on certain triggers
Timeline:
0s: Data fetched β Marked as FRESH
ββ No refetch on remount, window focus, etc.
30s: Data becomes STALE
ββ Will refetch on remount, window focus, network reconnect
ββ But old data still shows while refetching (no loading state!)
Even unused cache stays in memory for a while:
useQuery({
queryKey: ["user", 1],
queryFn: () => fetchUser(1),
gcTime: 60000, // 60 seconds (formerly called cacheTime)
});How it works:
- Component unmounts but cache stays for 60 seconds
- If component remounts within 60 seconds β Uses cached data (instant!)
- After 60 seconds of no usage β Cache is garbage collected
Example Scenario:
Time 0s: Component mounts β Fetches data β Stores in cache
Time 5s: Component unmounts
Time 10s: Component remounts β Uses cache (no API call!)
Time 70s: Cache is deleted (no components used it for 60+ seconds)
Time 75s: Component remounts β Cache miss β New API call
React Query keeps your data fresh automatically:
useQuery({
queryKey: ["user", 1],
queryFn: () => fetchUser(1),
refetchOnWindowFocus: true, // Default behavior
});Triggers for background refetch:
- Window regains focus (user returns to tab)
- Network reconnects
- Configurable intervals
- Manual refetch via
refetch()
Important: During background refetch, old data stays visible (no loading spinner!), then updates when new data arrives.
You can manually invalidate cache to force refetch:
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ["user", 1] });
// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: ["user"] });Use cases:
- After mutation (create, update, delete)
- User logs out
- Data becomes outdated
The CacheDemo component provides an interactive playground:
- Open browser console (to see API logs)
- Click "User 1" button
- β
Console shows:
π Making API call for user 1... - β
Server logs:
π‘ API Request: GET /api/user/1
- β
Console shows:
- Click "User 2" button
- β New API call for different data
- Click "User 1" button again
- β NO console log! Data served from cache
- β NO server request!
What you learned: Same queryKey = cached data reused
- Click "User 1" to load data (API call happens)
- Click "π΄ Unmount Component" (component disappears)
- Click "π’ Mount Component" (component appears)
- Data shows INSTANTLY without loading state
- β NO API call! Cache persisted during unmount
What you learned: Cache exists independent of component lifecycle
- Click "User 1" to load data (API call happens)
- Click "Show Child Component" button
- Child component shows same data INSTANTLY
- β NO API call! Parent and child share cache
What you learned: Multiple components with same queryKey share data
- Click "User 1" to load data
- Wait 30+ seconds (staleTime expires)
- Click "User 2" then back to "User 1"
- Notice "π Refetching (Background)" status
- β Old data shows immediately (no loading state)
- β Background API call fetches fresh data
- β UI updates when new data arrives
What you learned: Stale data = instant display + background refresh
- Click "User 1" (cache entry:
["user", 1]) - Click "User 2" (cache entry:
["user", 2]) - Click "User 3" (cache entry:
["user", 3]) - Each creates separate cache entries
- Switching between them uses their respective caches
What you learned: Each unique queryKey maintains separate cache
| Option | Default | Description |
|---|---|---|
staleTime |
0ms | How long data is considered fresh |
gcTime |
5 minutes | How long unused cache is kept in memory |
refetchOnMount |
true | Refetch when component mounts |
refetchOnWindowFocus |
true | Refetch when window regains focus |
refetchOnReconnect |
true | Refetch when network reconnects |
retry |
3 | Number of retry attempts on error |
enabled |
true | Whether query runs automatically |
- Cache Key is Everything: Same key = shared cache, different key = separate cache
- Automatic Cache Management: React Query handles storage, retrieval, and cleanup
- Smart Background Updates: Stale data shows instantly while fresh data loads
- Zero Boilerplate: No useState, useEffect, or manual cache management needed
- Performance Win: Eliminated unnecessary network requests = faster app