Next.js 16 was released on October 21, 2025, and it’s not just another incremental update — it’s a paradigm shift in how we think about caching, performance, and developer experience in full-stack React apps.
One of the most powerful and practical new features? Cache Components — powered by React’s cache() and deeply integrated with Next.js’ Data Cache, Router Cache, and Partial Prerendering (PPR).
In this article, we’ll go beyond the announcement and show you 5 real-world, production-ready examples of how Cache Components solve long-standing pain points — with before/after code, performance gains, and best practices.
Let’s dive in.
1. Stop Duplicating Database Calls in Layouts & Pages Problem (Before Next.js 16)
You fetch the same data (e.g. user, announcements, nav items) in both layout.tsx and page.tsx.
Each awaittriggers a separate database or API call — even if the data is identical.
// app/layout.tsx
const announcements = await getAnnouncements(); // DB hit
// app/page.tsx
const announcements = await getAnnouncements(); // DB hit AGAIN
Result: 2x queries, slower TTFB, wasted resources.
Solution (Next.js 16 + cache)
Wrap your data functions with cache() from React:
// lib/cms.ts
import { cache } from 'react';
export const getAnnouncements = cache(async () => {
const res = await fetch('https://cms.example.com/announcements', {
next: { revalidate: 300 } // 5 min stale-while-revalidate
});
return res.json();
});
Now use it anywhere:
// app/layout.tsx
const announcements = await getAnnouncements(); // Cache miss → fetch
// app/page.tsx
const announcements = await getAnnouncements(); // Cache hit → instant
Result: 1 DB call per request, even across nested components.
50%+ reduction in database load.
2. Eliminate Prop Drilling in Dashboards
Problem
You fetch user, posts, and stats in a dashboard page and pass them down:
<Header user={user} />
<Posts posts={posts} />
<Stats stats={stats} />
Manual prop drilling → fragile, hard to scale.
Solution: Shared Cached Functions
// lib/data.ts
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
return await db.user.findUnique({ where: { id } });
});
export const getPosts = cache(async (userId: string) => {
return await db.post.findMany({ where: { authorId: userId } });
});
Use directly in any component:
// app/dashboard/header.tsx
const user = await getUser(session.id); // Same cache key → shared
// app/dashboard/posts.tsx
const posts = await getPosts(session.id); // Reuses result!
No prop drilling.
Same data, same cache key → automatic deduplication.
3. Real-Time Widget? Opt Out of Caching Gracefully
Not all data should be cached.
Example: Live Stock Price
// lib/stock.ts
import { cache } from 'react';
export const getStockPrice = cache(async (symbol: string) => {
const res = await fetch(`https://api.stock.com/${symbol}`, {
cache: 'no-store' // Always fresh
});
return res.json();
});
// components/StockWidget.tsx
const price = await getStockPrice('AAPL');
return <div>Live: ${price}</div>;
Still memoized per render pass (if used 3 times, only 1 fetch).
But never stored in Data Cache → always fresh.
4. Hybrid Static + Dynamic Blog Posts
Use generateStaticParamsfor top 10 posts, dynamic for the rest — with cached data.
// lib/blog.ts
import { cache } from 'react';
export const getPost = cache(async (slug: string) => {
return await db.post.findUnique({ where: { slug } });
});
export const getAllSlugs = cache(async () => {
return (await db.post.findMany({ select: { slug: true } })).map(p => p.slug);
});
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const slugs = await getAllSlugs();
return slugs.slice(0, 10).map(slug => ({ slug }));
}
export default async function Post({ params }) {
const post = await getPost(params.slug); // Cached per slug
return <Article post={post} />;
}
Top 10 posts: Built at deploy time → instant.
Others: On-demand, cached per slug.
All data functions: Reusable & memoized.
5. Cache Third-Party APIs (GraphQL, etc.) with unstable_cache
fetchisn’t always an option.
Example: GitHub GraphQL
// lib/github.ts
import { unstable_cache } from 'next/cache';
export const getRepos = unstable_cache(
async (owner: string) => {
const data = await request(
'https://api.github.com/graphql',
`query { repositoryOwner(login: "${owner}") { repositories(first: 10) { nodes { name } } } }`
);
return data.repositoryOwner.repositories.nodes;
},
(owner) => [`github-repos-${owner}`], // cache key
{ revalidate: 300, tags: ['github'] }
);
// app/repos/page.tsx
const repos = await getRepos('vercel');
Invalidate with:
revalidateTag('github')
Migration Tip: Upgrade Safely
npx @next/codemod@latest upgrade
Then enable Cache Components:
// next.config.ts
const nextConfig = {
experimental: {
cacheComponents: true,
},
};
No breaking changes — opt-in and gradual.
Final Thoughts
Cache Components aren’t just a new API — they’re a new mental model for building fast, scalable Next.js apps.
- No more prop drilling
- No more duplicate fetches
- No more guessing about caching
- Explicit, predictable, and composable
If you're building with Next.js, upgrade to 16 today and start wrapping your data functions in cache().
Your users (and your database) will thank you.
Try it now: https://nextjs.org/blog/next-16
Docs: https://nextjs.org/docs/app/api-reference/functions/cache
Top comments (0)