DEV Community

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

Posted on

3 2 1

🔍 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

DevCycle image

Fast, Flexible Releases with OpenFeature Built-in

Ship faster on the first feature management platform with OpenFeature built-in to all of our open source SDKs.

Start shipping

Top comments (1)

Collapse
 
ciphernutz profile image
Ciphernutz

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

DevCycle image

Ship Faster, Stay Flexible.

DevCycle is the first feature flag platform with OpenFeature built-in to every open source SDK, designed to help developers ship faster while avoiding vendor-lock in.

Start shipping

👋 Kindness is contagious

Explore this insightful write-up embraced by the inclusive DEV Community. Tech enthusiasts of all skill levels can contribute insights and expand our shared knowledge.

Spreading a simple "thank you" uplifts creators—let them know your thoughts in the discussion below!

At DEV, collaborative learning fuels growth and forges stronger connections. If this piece resonated with you, a brief note of thanks goes a long way.

Okay