In the new cache components model, caching is done using the "use cache" directive. Use this directive to cache the return values of:
- Async functions = data-level caching
- Components = UI-level caching
- Routes
This is new terminology but it is very similar to the old caching model. UI-level caching corresponds to the full route cache (with PPR added) while data-level caching corresponds to data cache (though it works differently).
We start by covering caching of functions. Initially, caching functions confused me. I couldn't quite wrap my head around when to use it and how it works. Let me walk you through some of the things that confused me.
Caching functions
Caching the return value of functions in Next is new - you couldn't do this before. To cache a function, simply add the directive:
export async function getPost() {
'use cache';
//...
}
Async confusion
We can only cache async functions. Caching synchronous functions doesn't work and will trigger an error in dev and prod mode:
"use cache" functions must be async functions.
Why is this? Because checking or retrieving an entry from the cache is an asynchronous operation. If called from a synchronous function, it would return a pending promise rather than the actual cached data. Therefore, only asynchronous functions can be cached.
A first test
Let's cache an async function. Here is a very simple function. Obviously, we wouldn't cache this in a real application; it's just a test. Note the console.log. Also note that using the directive without a lifespan (see later) will trigger some defaults.
Note: the examples are available on GitHub.
// app/lib/sayHello.ts
export async function sayHello(name: string) {
'use cache';
console.log('Running sayHello with arg ', name);
return `Hello, ${name}`;
}
We call sayHello inside component <Hello />:
// app/components/Hello.tsx
import { sayHello } from '@/app/lib/sayHello';
export async function Hello({ name }: { name: string }) {
const res = await sayHello(name);
return <div>{res}</div>;
}
And finally we put it in this route: /chapter-13/hello:
// app/chapter-13/hello/page.tsx
export default function HelloPage() {
return (
<>
<Hello name='Bob' />
<Hello name='Bob' />
<Hello name='Fred' />
</>
);
}
A cached function includes the arguments it was called with as part of the cache key. Obviously, calling sayHello('Bob') and being served the cached result of sayHello('Fred') would be quite useless. Since we called sayHello twice with "Bob" and once with "Fred", we expect one log with "Fred" and only one with "Bob" (since the second call was cached). We run next build.
// terminal
Running sayHello with arg Bob
Running sayHello with arg Fred
This proves our "use cache" directive worked:
- The first time sayHello('Bob') was called, Next.js checked the cache, found no cache entry (a MISS), ran the function, and cached the return value.
- The second time sayHello('Bob') was called, Next.js checked the cache, found a match (a HIT), and returned the value without executing the function.
This is easy and as expected.
Prerendered cache files confusion
The old caching model gave us the data cache for the fetch API: actual prerendered files in the Next build folder. I half expected prerendered files for our sayHello cache entry as well. However, this assumption was wrong! Caching functions does NOT generate prerendered files. When using "use cache", the caching happens in-memory.
Next states that in-memory caching is sufficient for most application needs, but it also acknowledges limitations such as memory capacity limits and cache not persisting. We will return to this topic later.
It is possible to persist cached data so that it is no longer in-memory only. To do this, use the "use cache: remote" directive and/or cache handlers. This enables shared storage systems like Vercel Data cache or Redis. However, remote cache is outside the scope of this series.
Caching function happens in-memory - that is the main takeaway here.
Why do we need to cache functions?
Heavy computational functions
We cached a trivial sayHello function above, but imagine a real-world function with heavy computational overhead such as processing an array of 100,000 items, executing complex recursion, or performing image manipulation. If we call this function twice with identical arguments in different parts of an application, caching the result prevents redundant execution both at build time and runtime. Just remember: only asynchronous functions can be cached.
Data fetching functions
This is the second, more common and familiar use for caching functions: data fetching. Here is an example:
// app\lib\getTodo.ts
type TodoT = {
id: number;
title: string;
completed: boolean;
};
export async function getTodo(id: number) {
'use cache';
console.log('Running getTodo with id: ', id);
const data = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
const post: TodoT = await data.json();
return post;
}
We grab a todo from jsonplaceholder padding an ID as argument. Since we want to cache the output, we add the "use cache" directive. We then call this function inside a component that renders the data:
// components\todo\Todo.tsx
type Props = {
id: number;
};
export async function Todo({ id }: Props) {
const todo = await getTodo(id);
return (
<div className='flex gap-2'>
<div className='font-bold'>{todo.id}.</div>
<div className='italic'>{todo.title}</div>
</div>
);
}
Finally, we use the component multiple times in a route: chapter-13/todo:
// app\chapter-13\todo\page.tsx
import { Todo } from '@/components/todo/Todo';
export default async function TodoPage() {
return (
<>
<h1>Todo</h1>
<Todo id={1} />
<Todo id={1} />
<Todo id={2} />
</>
);
}
What do we expect when we run build here?
- First
getTodo(1)checks cache, finds nothing, function runs and result is cached. - Second
getTodo(1)checks cache, finds match, returns cached data, function doesn't run. -
getTodo(2)checks cache, finds no cache entry, function runs and result is cached.
We run next build:
Running getTodo with id: 1
Running getTodo with id: 2
Everything here behaves as expected. We successfully cached getTodo.
Summary
In this section, we explored caching functions within the cache components model. We learned how to apply function caching and verified its behavior through practical examples.
Only asynchronous functions can be cached. The return value is stored alongside the specific arguments (or scope) passed during execution. This cached data isn't stored in files but lives in memory. Finally, we learned that caching functions can be used to cache heavy running (async) functions or data fetching functions.
In the next chapter, we will revisit Suspense and how it relates to caching and partial prerendering.
If you want to support my writing, you can donate with paypal.
Top comments (0)