DEV Community

Cover image for React.js ~use() hook for Caching Problem~
Ogasawara Kakeru
Ogasawara Kakeru

Posted on

React.js ~use() hook for Caching Problem~

This is where most tutorials stop. But if you try to use use() with a promise created inside a Client Component, you will hit a subtle and frustrating bug.

// Bug: creates a new promise on every render
function UserProfile({ userId }: { userId: string }) {
  const user = use(fetchUser(userId)); // new promise every render
  return <ProfileCard user={user} />;
}
Enter fullscreen mode Exit fullscreen mode

fetchUser(userId) returns a new Promise object on every render. React sees a new promise, suspends again, and the component re-renders, creates another new promise, suspends again, infinite loop.

use() does not fetch data. It reads a promise. The promise must have a stable identity across renders. If you create a new promise on every render, you get an infinite suspension loop.

How to Stabilize the Promise
There are several approaches, each suited to a different archtecture:

1. Create the promise in a parent component or Server Component

// Server Component - promise created once, stable across renders
export default function UserPage({ params }: { params: { id: string } }) {
  const userPromise = fetchUser(params.id);

  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

No async/await needed, the promise is passed down unresolved. The Client Component unwraps it with use(). Server Components don't re-render, so the promise reference is inherently stable.

2. Use a module-level cache

For Client Components that need to initiate fetches, cache the promise so the same reference is returned on subsequent calls:

const cache = new Map<string, Promise<User>>();

function fetchUserCached(id: string): Promise<User> {
  if (!cache.has(id)) {
    cache.set(id, fetchUser(id));
  }
  return cache.get(id)!;
}

function UserProfile({ userId }: { userId: string }) {
  const user = use(fetchUserCached(userId));
  return <ProfileCard user={user} />;
}
Enter fullscreen mode Exit fullscreen mode

Same arguments produce the same promise reference. No infinite loop.

Avoid async in Cache Wrappers

Do not mark your cache function as async. The async keyword always creates a new promise, even if you return a cached value. Use a synchronous function that stores and returns the original promise object.

3. Use a data fetching library

Libraries like TanStack Query or SWR handle caching, deduplication, and revalidation out of the box. They predate use() and solve a much broader problem - but they also add ~13kB gzipped and a provider wrapper. For a simple "fetch once, display result" pattern, use() with a 5-line cache function (option 2 above) does the job without the extra dependency. The library earns its keep when your UI has long-lived client state that needs to stay fresh: think dashboards that refetch on tab focus, lists with pagination, or mutations that should optimistically update related queries.

4. Use React's cache() in Server Components
React provides a built-in cache() function for Server Components. It memoizes a function's return value for the lifetime of a single server request:

import { cache } from "react";

const getUser = cache(async (id: string): Promise<User> => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

Enter fullscreen mode Exit fullscreen mode

Multiple components calling getUser("123") during the same render will share one fetch. The cache is scoped to the request, it resets on every new page load.

cache() vs. useMemo
Both memoize. But cache() works across components in a server render (deduplication), while useMemo works within a single component across re-renders. cache() is for data fetching. useMemo is for computations. Different tools, different jobs.

Top comments (0)