DEV Community

Cover image for 🔍 Learning TanStack Query — But First, Manual Caching
Anik Dash Akash
Anik Dash Akash

Posted on

🔍 Learning TanStack Query — But First, Manual Caching

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:

  • The problem I noticed in my app
  • How I came up with the idea of caching
  • How I built a simple in-memory cache
  • What I learned from the process

🧠 The Problem That Sparked the Idea

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.


🔄 First, The Hook Without Caching

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 }; }; 
Enter fullscreen mode Exit fullscreen mode

It worked—but made unnecessary API calls whenever a user switched between pages.

💡 Adding Manual Caching (In-Memory)

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 
Enter fullscreen mode Exit fullscreen mode

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 }; }; 
Enter fullscreen mode Exit fullscreen mode

🧩 How It Works – Step by Step

  1. Generate a unique key (page_2_limit_10) based on the page and page size.

  2. Check the cache:

    • If the key exists and the data is fresh (within 5 minutes), use it directly.
    • If not, fetch new data from the API.
  3. Update the cache with new data and a timestamp.

  4. 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.

💻 Example Usage in Component

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)} />  </> ); 
Enter fullscreen mode Exit fullscreen mode

This is clean and declarative—the hook handles everything behind the scenes.

📘 What I Learned

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

Top comments (1)

Collapse
 
ciphernutz profile image
Ciphernutz

Great mindset! Manual caching first helps you truly appreciate TanStack Query’s power.