7 min read

Cutting Redundant Data Fetches with React’s `cache` in React 19

React 19’s cache memoizes server calls per request. Wrap a fetch or DB query, call it from multiple server components, and React runs it once. Fewer duplicate hits, faster pages, consistent data. The cache clears after render. Use it in Next.js 15 with RSC.
Cutting Redundant Data Fetches with React’s `cache` in React 19
Photo by Ahmet Kurt on Unsplash

React 19 introduced new features that improve data fetching in server-side React apps. One of them is the cache function, a built-in mechanism for memoizing the results of function calls on the server. In simple terms, the cache function allows you to avoid repeating expensive computations or data fetches when the same inputs occur again during a single render cycle. It does this through memoization, storing the results of function calls and reusing them if the same arguments are passed in the future.

This behavior saves resources and boosts speed.

The cache API is only available in React Server Components (RSC). In client-side React, you would use other techniques (like hooks useMemo and useEffect) for caching, but on the server, the cache function is the right tool.

The problem of redundant data fetches

In a framework such as Next.js 15, it’s common to break pages into components or even use parallel routes (multiple segments rendered concurrently). These components might need the same data. For example, two different parts of the page both require a user’s profile or a list of products.

Without caching, this often means making duplicate database (DB) or API calls. For instance, if two sibling components (or parallel route segments) each query the DB for the current user’s info, you’d normally end up hitting the DB twice for the same query. This slows your app, increases server load, and potentially leads to inconsistent data if one request finishes before the other.

In the past, avoiding this redundancy required workarounds like lifting data fetching up to a common parent and passing props down or implementing a manual caching layer. However, lifting state up makes components less modular. Custom caching logic also gets messy. What we want is a way for each component to request the data it needs independently while automatically deduplicating identical requests during the render cycle. This is exactly the use case React’s cache function is designed to handle.

How React’s `cache` works

React’s cache function creates a memoized version of any asynchronous function. When you wrap a function with cache, React will store its result in memory the first time it's called. Subsequent calls to the cached function with the same arguments will return the cached result instantly, skipping the actual function execution. In other words, the first call is a cache miss (triggers the real fetch or computation), and later calls (with identical parameters) are cache hits that reuse the result.

This caching is scoped to a single server render lifecycle. During a server-side render, if multiple components invoke the same cached function with the same arguments, React runs the underlying operation (e.g., the DB query) only once. It prevents redundant, expensive operations by reusing the cached result. That avoids unnecessary network requests. This means fewer DB hits or API calls for that page render, leading to better performance and consistency. All components get the same result object, so they stay in sync.

Equally important, the cache is automatically cleared after the render is complete. React clears the cache after every server request. It invalidates all cached results at the end of each server request/render, so the next time you render that page (or another user requests it), React fetches fresh data.

There’s no need to worry about manual cache invalidation or stale data in subsequent requests. By design, each new request gets a new, empty cache. This makes the cache function a per-request memoization. Each call to cache(fn) returns a new memoized function with its own storage, and errors are cached the same way as successful results.

It differs from persistent caches (like data cached to disk or long-lived in-memory caches). Those would need explicit invalidation strategies, but this built-in cache is transient and lives only for the duration of the render. As the Next.js docs note, the cache (for memoized fetches or functions) "lasts the lifetime of a server request until the React component tree has finished rendering" and is not shared across requests.

In practice, this means each page load or navigation gets fresh data, which is ideal for most cases where you want real-time updated info on a new request.

Using React’s `cache` in a Next.js App (Supabase Example)

Let’s walk through a concrete example to see the cache function in action. With a Next.js app using the App Router, you have two parallel route segments on a dashboard page, say, a user profile section and a sidebar with user stats. Both segments need to retrieve the user's profile data from the DB. We'll use Supabase as the DB client for this example (Supabase provides a JS client to query your DB directly from server code).

First, we can set up a Supabase client (for example, using the project URL and anon key in an environment config). Then we write a data-fetching function to get the user profile. We’ll wrap this function with the cache function:

Next.js 15 defaults fetch to cache: 'no-store'. Identical calls are still deduplicated in one render, but each new request fetches fresh data unless you opt in with cache: 'force-cache' or revalidation.
// utils/dataFetcher.ts 
import { createClient } from '@supabase/supabase-js'; 
import { cache } from 'react'; 
 
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); 
 
// Define a cached function to fetch a user profile by ID 
export const getUserProfile = cache(async (userId: string) => { 
  const { data, error } = await supabase 
    .from('profiles') 
    .select('*') 
    .eq('id', userId) 
    .single(); 
 
  if (error) throw error; 
 
  return data; 
});

In the code above, getUserProfile is a memoized function. The first time you call getUserProfile('abc123') for a given request, it will actually run the query against Supabase. If you call getUserProfile('abc123') again (anywhere else in the React tree during the same render), it will instantly return the cached result instead of querying the DB a second time. This pattern can be seen in real projects. For example, a Supabase utility might export cached queries like this to prevent duplicate calls.

Now, suppose our Next.js page uses parallel routes or multiple components that need this data:

// app/dashboard/@main/page.tsx 
import { getUserProfile } from '@/utils/dataFetcher'; 
 
export default async function MainSection({ userId }) { 
  const user = await getUserProfile(userId); 
  return <ProfileDetails user={user} />; 
} 
 
// app/dashboard/@sidebar/page.tsx 
import { getUserProfile } from '@/utils/dataFetcher'; 
 
export default async function Sidebar({ userId }) { 
  const user = await getUserProfile(userId); 
  return <UserStats user={user} />; 
}

In this simplified example, MainSection and Sidebar are two parallel segments (or could simply be two sibling server components) that both fetch the user’s profile. Thanks to React’s cache, the database is hit only once for getUserProfile(userId) during the server render. The first call (say, in MainSection) will query the DB (cache miss), and the second call (in Sidebar) will find the result already in cache (cache hit), avoiding another DB round-trip. Without the cache, we would have executed two identical queries to Supabase. With cache, React makes sure those components share the work and render the same snapshot of data.

It’s worth noting that this caching works for any kind of function, not just DB calls. You could use it to memoize expensive calculations or calls to other APIs or CMS systems. The key is that the function should be called within the React server render cycle (e.g., inside an async server component or during data fetching in a Next.js route).

If you try to call the cached function outside of rendering (for example, at the top level of your module when defining it or in a non-React context), it will still execute but won’t use React’s cache. React only provides cache access during the rendering of components. In practice, you usually call the cached function inside your server components as shown above, which is the correct usage.

Benefits of Using React’s `cache`

  • Fewer DB/API calls: By deduplicating identical requests, you drastically cut down on redundant calls. The same function called multiple times with the same arguments will hit the server or DB only once. This reduces server workload and network traffic.
  • Improved Performance: Reusing cached data results in faster response times and a smoother user experience. The page can render faster since subsequent components don’t have to wait for repeat fetches. In our example, the second component gets the data almost instantly from memory.
  • Consistent Data: When multiple parts of the UI request the same info, using a single cached result means they all render with the same data. There’s no risk of one component showing stale data while another fetches fresh data moments later. During that render, they share the exact result.
  • Simplified Code: React’s cache lets each component fetch what it needs without complex prop drilling or higher-level coordination. This keeps components more independent and readable, while the caching happens transparently under the hood. You don’t need ad hoc context providers or singletons to share data. Just call the cached function wherever needed.
  • No Manual Invalidation Needed: Because the cache resets on every new request, you get fresh data on the next render by default. This ephemeral caching means you don’t have to write extra logic to invalidate or update the cache for new requests. (If you do want to cache across requests, you’d use other Next.js opt-in caching features, but that’s a different mechanism beyond our scope here.)

Conclusion

React’s built-in cache function is a useful feature for data fetching in server-rendered React apps. It allows you to write straightforward code in each component or route while automatically preventing duplicate DB or API hits during that render. In our example with parallel route segments, we saw how using cache with a Supabase query ensures only one DB hit occurs instead of many. As of React 19, this feature is stable and ready for production use, empowering developers to build leaner apps without reinventing caching logic.

By leveraging React’s cache function, you get better performance, reduced server load, and cleaner code. Each new request still gets fresh data (since caches don’t persist between requests), so users always see up-to-date information. Whether you’re building a simple page with a couple of components or a complex dashboard with many data-driven widgets, cache helps make your data fetching simpler. Embrace this pattern in your Next.js App Router or any React 19 server component setup to make sure your app only fetches what it truly needs, when it needs it, and never twice in the same render.

V

Subscribe to our newsletter.

Become a subscriber receive the latest updates in your inbox.