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.