The `use cache` Directive Deep Dive in Next.js
The Next.js Conf announced a very exciting update - a new caching mechanism.
It was announced that there is now a mechanism to control the caching of functions, components as well as entire routes. It is called as use cache
.
Now, this is a very exciting update and it is a game changer. But that leaves questions like where do we use useCallback, useMemo and also different data fetching methods like SSG
(Static Site Generation), SSR
(Server Side Rendering) and ISR
(Incremental Static Regeneration) and what about the unstable_cache
directive?
This blog post will answer all those questions.
If I have to summarize the use cache
directive in one sentence, I would say,
What is use cache?
use cache
is a directive like use client
or use server
. It is used to control the caching of not just functions but also components and routes.
This new mechanism is a successor to the experimental unstable_cache
and offers more flexibility and power. With unstable_cache, you could only cache JSON data but with use cache
, you can cache any serializable data, including components and routes.
We talked about what serialization is and how it works already in a previous lesson in the course .
So, anything RSC (React Server Components) friendly can be cached as long as it is serializable. Why? Since RSC also has the same criteria that you can only pass serializable values to props. Take a look at the React Server Components Serializable Props here .
What are some key features of use cache?
-
Versatile Caching: Unlike
unstable_cache
which could only cache JSON data,use cache
can cache any serializable data, including components and routes. -
Multiple Caching Levels: You can apply caching at different levels:
- Function level
- Component level
- Route level (entire file)
-
Automatic Caching: When you add the
use cache
directive to a file, everything inside that file becomes cacheable, similar to howuse client
works where addinguse client
directive to a file makes everything inside that file a client component.
How do we use use cache?
To implement use cache
, you need to:
- Use the canary version of Next.js
- Enable the experimental flag in your
next.config.ts
file
Here's a basic example of how to enable use cache
experimental feature:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
},
};
export default nextConfig;
Let's take a look at a demo for how it works.
Demo 🔥
In the below demo, you can see how the caching works at different levels. We have a RootLayout
plus a Navbar
, Sidebar
, Post
, Comments
and a Footer
as its children. You can control the caching of each of these components individually, thanks to use cache
directive.
By default, all components are uncached. You can use the 'use cache' toggle to toggle the caching of the components.
But if you want to cache the entire layout, you can do that by adding the use cache
directive at the top of your layout file or in this demo, you can toggle the main "use cache" switch of the RootLayout
component.
After you press the invalidate cache button, you will see that the cached layout will be revalidated and the new data will be fetched from the server.
There are several Cache Profiles you can choose from to control the cache duration. Next.js has offered several options like seconds
, minutes
, hours
, days
, weeks
, months
, years
. You can simply pick one of them from the dropdown and reference the definition of those in the legend.
If you don't specify a cache profile, the default cache duration is 15 minutes and it never expires.
Caching Components Demo
Legend:
Cache Profiles:
Root
Navbar
Sidebar
Post
Comments
Footer
Controlling Cache Behavior
You get granular control caching controls with use cache
directive. As shown in the demo above, you can control the cache duration, cache tag and cache profile.
Cache Tag
You can also tag cached items for selective revalidation which means you can invalidate or revalidate specific cached items after a specific amount of time.
Here's an example where we are caching the fetchProducts
function and tagging it with product-details
cache tag. The first time, fetchProducts is called, it fetches the products from the API and caches the result with the product-details
cache tag.
The next time, fetchProducts
is called, it will return the cached result if the cache is still valid.
import { unstable_cacheTag as cacheTag } from 'next/cache';
export async function fetchProducts() {
'use cache';
cacheTag('product-details');
const response = await fetch(
'https://api.mockapi.io/products'
);
return response.json();
}
Cache Life
You can control the cache duration by using the unstable_cacheLife
function from the next/cache
module. Cache Life is the amount of time the cache will be valid for. It is the life of the cache!
Here's an example where we are caching the fetchProducts
function and setting the cache life to 1 hour. By selecting hours, the cache will be invalidated after 1 hour.
I'd encourage you to try it out in the demo above.
import {
unstable_cacheLife as cacheLife,
unstable_cacheTag as cacheTag,
} from 'next/cache';
export async function fetchProducts() {
'use cache';
cacheLife('hours');
cacheTag('product-details');
const response = await fetch('https://api.mockapi.io/products');
return response.json();
}
Purging and Revalidating Cache
To purge or revalidate cached data, you can use server actions revalidateTag
function.
Just like you can purge the cache of a component using the revalidateTag
function when the cache is tagged by adding { next: { tags: ['a', 'b', 'c'] } }
as a parameter to fetch
. You then purge the cache of a route using the revalidateTag
function.
In the case of use cache
, you use the cacheTag
function to tag the cache and still use the revalidateTag
function to purge the cache.
So, revalidateTag
is a function that can be used to purge the cache of a component or a route.
Here's an example where we are purging the product-details
cache tag.
'use server';
import { revalidateTag } from 'next/cache';
export async function purgeProductDetailsCache() {
revalidateTag('product-details');
}
Caching Components
You can also cache components as long as their props are serializable (i.e. React Server Components friendly). A cache key is allocated based on the serialized props.
Here's an example:
import Image from 'next/image';
import { Dog } from '@/types';
import DogCard from '@/components/dog-card';
// Simulate an API call with caching
export async function fetchDogs(): Promise<Dog[]> {
const urls = ['https://http.dog/100.json', 'https://http.dog/200.json', 'https://http.dog/401.json'];
const responses = await Promise.all(urls.map(url => fetch(url)));
const data = await Promise.all(responses.map(response => response.json()));
console.log('Hi Fetching Dogs');
return data;
}
export default async function DogList() {
'use cache';
const dogs = await fetchDogs();
return (
<div className="max-w-2xl mx-auto p-6 bg-pink-200 shadow-md rounded-lg">
<h1 className="text-3xl font-bold text-left mb-6">Dogs</h1>
<ul className="grid grid-cols-3 gap-4">
{dogs.map((dog: Dog) => <DogCard key={dog.id} dog={dog} />)}
</ul>
</div>
);
}
Route-level Caching
To cache an entire route, add the use cache
directive at the top of your page file. This makes the entire file cacheable but also makes all the components inside the file cacheable i.e. it's children components are also cached.
In the below example, the entire page.tsx
file is cached and so are the DogList
and ProductsList
components as they are children of the page.tsx
file.
'use cache';
import DogList from '@/components/dog-list';
import ProductsList from '@/components/products-list';
import { Suspense } from 'react';
export default async function Page() {
return (
<div className="p-12">
<DogList />
<Suspense fallback={<div>Loading...</div>}>
<ProductsList />
</Suspense>
</div>
);
}
Considerations
use cache
is still an experimental feature, so keep an eye on the official Next.js documentation for updates.- When caching at the route level, be mindful of client interactivity and re-rendering needs.
- The default cache duration is 15 minutes if not specified.
- Be cautious when caching dynamic content or frequently changing data.
By leveraging use cache
, you can significantly improve the performance of your Next.js applications by reducing unnecessary computations and network requests. However, it's crucial to implement caching thoughtfully to ensure data freshness and correctness in your application.
I also want to clarify that use cache
is not a replacement for use server
or use client
. It is a new directive that allows you to control the caching of different parts of your application.
What about useCallback, useMemo and other performance optimizations?
Well, remember, with React compiler, we are going to use useCallback
and useMemo
less as React Compiler will optimize the code for us. Try checking out this video to learn more about how React Compiler works and how it will optimize the code for us.
How is use cache different from React Server Components and from data fetching methods like SSG, SSR and ISR?
React Server Components helped avoid the network roundtrip for data fetching as we can delegate the data fetching to the server. With data fetching methods like SSG, SSR and ISR, we can cache by default at build time or revalidate at runtime or refetch at runtime respectively. But these capabilities were mainly provided to the fetch
function. This meant that a lot of apps that were not using fetch suffered.
So where does use cache
fit in?
Well, the world of Next.js is heading towards Partial Pre-rendering as the future. With PPR, we can have a static shell while dynamic parts of the apps are rendered on the client. This allows Next.js to leverage the benefits of both worlds - the static shell can be cached while the dynamic parts are rendered on the client. How does the dynamic parts get rendered? Well, our good old friend Suspense
boundary comes to the rescue!
use cache
directive is the key to unlocking the power of PPR.
You can add use cache
at different levels - function, component and route level depending on your use case but add a Suspense
boundary around the dynamic parts of the apps. This unlocks a lot of power as a developer to control the caching of different parts of the apps without having to depend on the fetch
function.
Now, you can still use the 3 data fetching methods like SSG
(Static Site Generation), SSR
(Server Side Rendering) and ISR
(Incremental Static Regeneration) but you can also use use cache
directive to cache the data at different levels. It gives you more control as a developer to cache the data at different levels. Which is why we get cacheTag
and cacheLife
functions to control the cache duration and cache tag.
If you think about it, a lot of our apps are dynamic in nature. We won't just have static content and not every app is a blog or a documentation site or a E-commerce site. use cache
unlocks the power to do so.
Tip of the iceberg!
In this lesson, we talked about the use cache
directive and how it works. But there is a lot more to Caching in Next.js that we haven't covered.
I've an entire section on Next.js Caching filled with full of interactive demos and exercises that goes way deeper into the topic.
Now, if you liked this post, you'll love the course. It's called The Modern Full Stack Next.js Course .
It's meant for folks who are looking to ship more real world applications with Next.js. Folks that are looking to know when to use what caching method, how to control caching at different levels and more.
If you'd like to improve the skills needed to land a great job as a Next.js developer or grow your career as a Next.js developer, you should check it out.
You can learn more here 👇
I hope you found this post helpful. 💖