What about Client-Side fetching for use() hook?
All examples so far start the fetch in a Server Component and pass the promise down. But what if you are already deep inside a client component, say a button opens a popup that needs fresh data?
You can't render a Server Component inside a Client Component. But use() + Suspense still works, you just have to manage promise identity yourself.
Start the Fetch in the Event Handler
The most straightforward approach: create the promise in the click handler, store it in state, and let use() read it.
"use client";
function DetailPopup({ dataPromise }: { dataPromise: Promise<ItemDetail> }) {
const detail = use(dataPromise);
return <div>{detail.description}</div>;
}
function ItemCard({ itemId }: { itemId: string }) {
const [promise, setPromise] = useState<Promise<ItemDetail> | null>(null);
const handleOpen = () => {
setPromise(fetchItemDetail(itemId)); // fetch starts immediately
};
return (
<>
<button onClick={handleOpen}>Show Details</button>
{promise && (
<Suspense fallback={<Skeleton />}>
<DetailPopup dataPromise={promise} />
</Suspense>
)}
</>
);
}
This is responsive, the fetch fires the instant the user clicks, not after React schedules a render. Each click creates a new promise, so you always get fresh data.
Use a Module-level Cache for Repeated Access
If the same popup might be opened multiple times with the same ID, a cache avoids redundant requests:
const cache = new Map<string, Promise<ItemDetail>>();
export function getItemDetail(id: string) {
if (!cache.has(id)) {
cache.set(
id,
fetch(`/api/items/${id}`).then((r) => r.json()),
);
}
return cache.get(id)!;
}
const handleOpen = () => {
setPromise(getItemDetail(itemId));
};
This is exactly the same caching pattern from the section above, it works identically whether the promise is created in a Server Component or a click handler. What matters is that use() always receives the same promise object for the same request.
When is use() Enough, and When Do You Need More?
For the popup scenario above, user clicks, data loads, popup displays it. use() with a simple cache function is all you need. No extra dependency, no provider, no configuration. The 5-line Map cache from above handles deduplication just fine.
Consider TanStack Query or SWR when the data has a lifecycle beyond a single display:
- The same data is shown in multiple places and a mutation in one place should update all of them.
- Data goes stale and should silently refetch when the user returns to the tab.
- You need paginated or infinite-scroll lists with cursor tracking.
- You want optimistic UI that rolls back on server error.
If none of those apply, use() + a cache function is the simpler choice. You can always add a library later when the caching requirements grow, the mental model (promise in, data out, Suspense handles the wait) stays the same.
Top comments (0)