
Anik Dash AkashLately, I’ve been diving into TanStack Query (formerly React Query) to learn more about its powerful...
Lately, I’ve been diving into TanStack Query (formerly React Query) to learn more about its powerful data fetching and caching features. But before I start relying on tools that abstract everything for me, I wanted to understand the problem caching solves and implement a basic version myself.
This post walks through:
In one of my projects, I display a paginated list of users. Every time I changed the page, an API call was made—even if I had already fetched that page before. This felt inefficient.
Then I thought:
"Why not store the results of each API call in memory, and reuse them if the same request is made again within a short time?"
That's when I got the idea of building a simple in-memory cache:
✅ If I’ve already fetched page 2, don’t fetch it again within 5 minutes.
❌ Otherwise, fetch it from the API and cache it.
This idea led me to implement manual caching logic in my React hook.
Initially, my custom hook looked like this:
export const useGetAllUser = (currentPage, pageSize) => {
const [users, setUsers] = useState([]);
const [totalUsers, setTotalUsers] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async () => {
setLoading(true);
setError(null);
try {
const res = await fetchData("/users", {
_page: currentPage,
_limit: pageSize,
});
setUsers(res.data);
setTotalUsers(Number(res.headers["x-total-count"]));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, [currentPage, pageSize]);
return { users, totalUsers, loading, error };
};
It worked—but made unnecessary API calls whenever a user switched between pages.
To fix this, I added a simple cache:
const userCache = {}; // A JS object to store cached results
const CACHE_DURATION = 5 * 60 * 1000; // Cache is valid for 5 minutes
Then I modified the hook to check the cache before calling the API:
export const useGetAllUser = (currentPage, pageSize) => {
const [users, setUsers] = useState([]);
const [totalUsers, setTotalUsers] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const cacheKey = `page_${currentPage}_limit_${pageSize}`;
const now = Date.now();
const cached = userCache[cacheKey];
if (cached && now - cached.timestamp < CACHE_DURATION) {
// Serve from cache
setUsers(cached.data);
setTotalUsers(cached.total);
setLoading(false);
return;
}
// Fetch from API and store in cache
const fetchUsers = async () => {
setLoading(true);
setError(null);
try {
const res = await fetchData("/users", {
_page: currentPage,
_limit: pageSize,
});
const data = res.data;
const total = Number(res.headers["x-total-count"]);
setUsers(data);
setTotalUsers(total);
userCache[cacheKey] = {
data,
total,
timestamp: Date.now(),
};
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUsers();
}, [currentPage, pageSize]);
return { users, totalUsers, loading, error };
};
Generate a unique key (page_2_limit_10) based on the page and page size.
Check the cache:
Update the cache with new data and a timestamp.
Avoid repeated fetches for already-viewed pages within a short time.
This was the “aha!” moment for me—simple caching reduced redundant network calls and made the app feel snappier.
Here’s how I use the hook in the UI:
const { users, totalUsers, loading, error } = useGetAllUser(currentPage, pageSize);
return (
<>
{loading ? <SkeletonLoader /> : (
<UserGrid users={users} />
)}
<Pagination
current={currentPage}
total={totalUsers}
pageSize={pageSize}
onChange={(page) => setCurrentPage(page)}
/>
</>
);
This is clean and declarative—the hook handles everything behind the scenes.
From this experience, I understood:
How to think like a caching system
How to build basic memoization logic
The trade-offs of manual caching (like global memory usage and no auto-invalidations)
Most importantly, I understood why TanStack Query exists—it handles all this, and much more:
Automatic stale handling
Background refetching
DevTools for cache inspection
Garbage collection
React integration