React Query Caching Struggles with Server-Side Pagination

The Problem I Faced
I was implementing server-side pagination with React Query and kept running into this frustrating issue:
- When I included
paginationModel
in thequeryKey
: pageSize changes worked fine, but page navigation kept resetting to 0 - When I removed
paginationModel
from thequeryKey
: page navigation worked, but pageSize changes didn’t trigger new API calls
My Original (Problematic) Implementation
// ❌ This caused page resets
const { data, isLoading } = useQuery({
queryKey: [
'financialFab',
bonusMonth,
paginationModel, // Including both page and pageSize
],
queryFn: async () => {
const response = await api.get(`/data`, {
params: {
page: paginationModel.page,
size: paginationModel.pageSize,
}
});
return response.data;
},
});
What I Learned About React Query Caching
Key Insight #1: QueryKey Creates Separate Cache Entries
Every unique queryKey
creates a completely separate cache entry. So:
// Different cache entries:
['financialFab', '2024-01', { page: 0, pageSize: 10 }]
['financialFab', '2024-01', { page: 1, pageSize: 10 }]
['financialFab', '2024-01', { page: 0, pageSize: 20 }]
These are three different cache entries with no relationship to each other.
Key Insight #2: Server-Side Pagination ≠ Client-Side Caching
Server-side pagination means:
- Every page is different data that requires an API call
- Every pageSize change is different data that requires an API call
- React Query’s caching works best when you can reuse the same dataset
This creates a fundamental mismatch!
Key Insight #3: Page vs PageSize Have Different Caching Needs
// Page changes: Need new API call, but could conceptually reuse cache
// PageSize changes: Definitely need new API call, cache should be separate
// ✅ Only pageSize in queryKey
queryKey: ['financialFab', bonusMonth, paginationModel.pageSize]
// ✅ Or handle them separately
const { refetch } = useQuery({
queryKey: ['financialFab', bonusMonth],
// ...
});
useEffect(() => {
refetch(); // Manual refetch on pageSize change only
}, [paginationModel.pageSize]);
Better Approaches I Discovered
Approach 1: Include Only PageSize in QueryKey
const { data, isLoading } = useQuery({
queryKey: [
'financialFab',
bonusMonth,
paginationModel.pageSize // Only pageSize
],
queryFn: async () => {
// Use current page from state, not queryKey
const params = {
page: paginationModel.page,
size: paginationModel.pageSize,
};
return api.get('/data', { params });
}
});
Approach 2: Consider Simpler State Management
// Sometimes the simplest solution is the best
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const fetchData = useCallback(async () => {
setIsLoading(true);
try {
const response = await api.get('/data', {
params: {
page: paginationModel.page,
size: paginationModel.pageSize,
}
});
setData(response.data);
} finally {
setIsLoading(false);
}
}, [paginationModel]);
useEffect(() => {
fetchData();
}, [fetchData]);
The Key Realization
React Query is amazing for caching and reusing data, but server-side pagination creates a scenario where every page/pageSize combination is unique data that can’t be meaningfully cached.
Sometimes the “modern” solution (React Query) isn’t always the best fit for every use case. Traditional useEffect
+ useState
can be simpler and more predictable for server-side pagination scenarios.
Lessons Learned
- Understand your queryKey: It’s not just a label, it’s a cache identifier.
- Evaluate caching benefits: For server-side pagination, caching often doesn’t add value.
- Choose the right tool: Don’t force React Query everywhere; simpler state management might be better.
- Handle page and pageSize differently: They have different caching semantics.
The struggle taught me that understanding the tool’s underlying mechanics is more important than just following patterns!