Let's talk about caching in Next.js. This
is probably one of the most requested videos that I've had, and I know you all want to see
some practical examples of how to cache data, revalidate data, and also just walk
through how the model works from the ground up. So that's what we're going to do
today. We're going to show some examples, show some demo apps that will have
the code open source as well, too, and kind of walk through the foundations of
how caching works in the Next.js App Router. Okay, let's start by looking at an example of
an application that uses caching. It revalidates data and it has mutations, so we can work
backwards from a real product. So, on the right, I have this example of kind of a roadmap feature
prioritization app, and if I go in here and I say new feature, I hit enter, this data that is
now shown on the page is being cached. Anytime I make a mutation to this data, like uploading a
feature or adding another new feature, the Next.js App Router is able to revalidate that data. It's
able to mutate the cache and update it with the latest features. So now, if I reload the page, I
still see that we have our cache data on the page. Now, this is a simple example, but it really
is probably the most common way of interacting with the App Router. And we're going to
break down how every bit of this happens, from static pages to the cache pages,
and finally to revalidating data. Before we jump in, I want to talk a bit about
the state of caching in Next.js today. So, the App Router has been out for about a year
now, and a lot of you all have shared a lot of great feedback with us on the parts of caching
that you like, the parts of caching that you want us to change, and how we can improve the
developer experience, and even helped report and solve some bugs along the way, too. And
maybe the last time you all tried out caching, some parts of this model might not have been
ready yet, like server actions or revalidateTag or revalidatePath. So if you haven't checked it out
in a while, definitely come into this video with a fresh set of eyes because it might look a little
different than the last time you checked it out. As you'll see as I talk through the video, there's still ways that we want to make
this even more simple. So I'll highlight a few things that we're still working on
along the way to simplify this even further. Okay, so now let's talk about some of the
foundations of caching in the App Router. We can understand how the pieces fit together.
We'll talk a little bit about static and dynamic pages or static and dynamic rendering. We'll
talk about some differences with the pages router model that you might have used before,
and we'll also walk through some examples of different use cases where you might want to
use caching and what that would look like. So let's start with static and dynamic rendering.
In my application, in my editor on the left here, I have an App Router app, and I have a page at
the index route. This page is just rendering out some JSX, and it's rendering out the date of
right now. In my terminal on the bottom left, I'm running next dev. So on the right
in the browser, if I reload the page, I can see I'm making requests to this page,
and I'm getting a new date on every request. Now, this functionality of being able to refresh
the page when you're running your local dev server and see up-to-date data is how Next.js has worked
since the pages router as well. I also have, just to show an example, this /test which is in
the pages router that does basically the exact same thing, except it's passing the date from
getStaticProps. And when I'm running in my local dev environment and I reload the page, I see that
exact same behavior, too, with the pages router. Now, what you're witnessing is that when you're
running local dev and you reload the page, even though this page is going to be cached,
we're going to talk about that here in a second, the data is fresh on every reload. And I think
sometimes that foundational bit gets a little bit confused between folks trying out this new
model. So we have to understand that bit first. For example, if I were to go and run the
build and then start my application locally, you're going to witness a different behavior. What
you're going to see is, when we run a production build, when we take this example of our App
Router page where it had a date, this code, while it is a server component, is going to be
evaluated during the build. So there's nothing in this component that's saying that it needs
to access any information on demand or from the request, so it can be pre-rendered, and
it doesn't need to be computed on the fly. So, if I reload the page, I now see this
date that has been generated, the page has been pre-rendered, and nothing changes. So
this is a foundational bit to understand, which is that even though we're using server
components, the default is that pages get prerendered when you run a build. It's also worth
mentioning that the same thing applies to route handlers that use a GET and do not read anything
from the incoming request. So if you try to use a route handler and you want to just print out a
date, you might notice the same behavior. Route handlers are built on the same foundation as
pages, so they share the same default behavior. Now, what if this isn't the behavior
that we want? What if we do actually want our page to be computed dynamically,
to run dynamically on every request? Well, we want it to be dynamically
rendered and not statically rendered. Now, I've updated my code on the left here to
read something from the incoming request. So, I'm using this cookies() function, which allows
me to look at the incoming request. I could have also used a headers() function,
or we also have one called noStore(), which allows you to do basically the exact same
thing to opt into dynamic rendering. And now, when I reload the page on the right,
you're seeing that this component is getting a fresh date on every reload. So
now, the page is using dynamic rendering. There are a number of ways, when
we talk about fetching and caching, of how you can opt out of using the
default behavior where things are cached. But it's important to understand
this difference from the start. Okay, so with an understanding
of static and dynamic, and what is opting a page into dynamic rendering,
and an understanding that the local next dev environment is different than
running your production server, let's talk about some specific examples of how you
would fetch data and how caching will affect that. We're going to start with the most simple
example, which is using the fetch API. So here, I have the fetch API, I'm making a request
to some external URL, I get back JSON, and then I'm showing the stock on the page. So
in the right, my browser, if I reload the page, I see in my console that this is a
cache hit. I can reload the page, still seeing a cache hit. Now, I have an
option turned on in my next.config that allows me to see more information about the
URLs that I'm fetching from. If you all think this is helpful and you want us to do more like
this in the terminal, definitely let us know, but that's where you're seeing that, and then it's
getting rendered out on the page as well, too. Now, let's say when I'm testing locally, I
want to empty the cache, and I want to get fresh data. You can use Command + Shift + R like
a hard reload in the browser. If I do that now, we see in the console that it was a cache
skip, and now if I reload, I'm back to cache data again. So if you need to clear that
out, Command + Shift + R is your friend. What about if you have a fetch but you don't
want it to be cached? Well, going back to the foundational part I talked through with
dynamic rendering, we want to tell this component actually we don't want to store this
into cash. We want to use no store, or cookies, or headers, or looking at search prams, or a
number of different ways you can opt into dynamic rendering. noStore() is the easiest, so here
I've opted this into dynamic rendering. I said, you know what, don't do this. So if I reload my
browser on the right, you see every single time I'm seeing cache skip. So this is a really easy
way to opt out. Now, your next question might be, that's great, the fetch API is super helpful if
I'm using some external API, but what about if I'm trying to build my full stack app in Next.js?
I want to just go directly to the database, or I want to use some library that doesn't expose
the fetch API. The answer for that is a function called unstable_cache(). So let's take a look at
this. In this example, I have an unstable_cache() route here, pretty much the same exact thing where
I have this product quantity component, but I'm making this call to this getProduct() function.
Let's take a look at what this looks like. So, I have this unstable_cache() function, and it
takes in this function here where I'm going to go to my database using a Postgres database. I'm
going to select from the products table, and I'm going to find the one with the ID of one, and then
I'm tagging this cache to say that this is for all of the products that I'm fetching. So if I go
back to my browser, I reload the page, this is being cached every single time. And if I wanted
to clear the cache again, I can do a similar thing where I go and fetch that from the origin
source. Now, you might be wondering, is it okay to use this? Can I use this as unstable_cache()?
Fine, the API structure here might change a little bit and we're looking for your feedback on the
overall structure of this. We've already heard feedback that it's a little confusing why there
are the parts that make up the cache key here, and then how you're specifically tagging the cache to
revalidate the cache here. So we plan to simplify this a little bit, but the general idea is still
very solid. So if you have your data layer in your application that has your database and it has
your fetches and your functions, and you want to use unstable_cache(), this is totally fine.
You can still kind of build out your application in this model, and then over time, as we improve
this cache in the coming months, we'll provide code mods and documentation and examples to move
from this world to the newer world. Now, if you're like, you know what, actually I want to go to my
database, I want to grab new data, I don't want to cache anything here, that's fine. You don't need
to use the unstable_cache() function. Instead, you can write your component like this. I've just
kind of inlined that database fetch here, so go to my database, I select all the products, and then
I have a list of products. So on every reload, I'm going and fetching that new list of products.
Now, the counter to this is how we actually invalidate the cache when that data changes, and
we're going to talk about that a little bit more here in a minute. Alright, let's take this a
little bit further. So I have a route here, it's a dynamic route that's a list of products,
and inside of that page, I'm getting a product based on the ID, and then I'm showing the name
and the price. So I have /product/2, /product/1, and I have different data depending on what I'm
reading from the parameter from params.id, and this getProduct call. If I go look at this file,
getProduct again, it's using unstable_cache(), it's fetching from the database, and it's
tagging it with products. Now let's show what the mutation side of this looks like. And
to show that example, I have a route here that is /dashboard. Let's take a look at this code. So
/dashboard, it has some JSX, it has products, it has a link back to all products, it has a list of
the products, and it has a form to actually create a product. So I can go back here, I can look at
all the products, the same list is here, and then I also can add in a new product here. Now when I
submit this form, I'm going to use a server action So I have this action called createProduct.
createProduct, I'm just putting it inline here, but it's probably best practice to move this
to its own file where you can have your own data layer, where you can add in security checks
as well too. For the sake of simplicity in this example, I have it here. So I mark it with
the "use server" directive to mark this as a server action. I do a mutation on the database
where I insert a new field into the database, and then I'm going to revalidate the tag of
products. Now what I want to show is when I add a new product and I'm able to revalidate this
data, how it revalidates data that's been tagged across different pages as well too. So when you
call revalidateTag, it's actually purging that entire cache. So let's say I want a new product
here, that's going to be beef, and let's say the price is 50. I hit create, you're going to
see not only does this list of products update because we've revalidated the tag, also if I
go back to all products and I fetch this data, we also see this updated with the latest list. So
even going kind of cross-navigation as well too, I had a mutation that I was controlling in
my application. I said this data change, my database had a new item, I call revalidateTag
or revalidatePath, and now I can see that latest information updated. Now the follow-up to that is
what do I do when there's a mutation that someone else made on my application? So maybe I went to a
CMS and I updated the data. Maybe the data changed in my database, this is where webhooks can come in
very handy. So I have a webhook in my application using a route handler, and what I'm going to do
to demonstrate this is I have my application here, /product/1, and right now if I reload the
page, the price is 9. So what I want to do instead is if I call this webhook, simulating
some external event in my system changing, something in my database has changed, I want to
go and I want to revalidate the tag products and I want to return back. So this takes a post
request to my application, so I'm just going to open up a new terminal here, going to make
a post request, and I get success back. So now if I go here, I reload the page, I get this new
data fetched from the database. So even even if that mutation is happening outside of my my user
interaction, clicking a button, it's happening from an external system, revalidateTag
can still revalidate that whole data. Now the last bit here is what happens if neither
of those are possible? What happens if it's not something that I control, or it's something I
can listen to from my database or my CMS? And this is where we're still helping to get feedback
and iterate on this experience today. I'll link a thread down below where we have a deep dive
into how caching works today, and walks through a lot of these bits, as well as our caching
documentation. And if you're in this third camp right here, we'd love to hear your feedback
on how to make this better. We do have an option that we're proposing to configure this a bit more,
so we love to hear your use cases around this. If you have cache data that you want
to periodically revalidate using the incremental static regeneration-like behavior,
you can also do that, either using the fetch API or using unstable_cache(). So today, the
way this works with the fetch API is that you can pass additional options by extending the
fetch API. So maybe I want something like this, we're actually looking to move away from
extending the fetch based on your feedback and the community's feedback. So in the future,
we'll have a way to code mod or to upgrade this where you can have something like revalidate
after, and you can put time in or an API that looks something similar to this. But for today,
it's totally okay to use it this way, we'll give some more guidance as that change in the future.
But I wanted to just mention that as well too. And also on the unstable_cache() function,
if I go back to getProduct, inside of here, this option not only has the ability to tag
the cache but also to set a revalidate time when you're using unstable_cache() as well
too. So that's how you can use incremental static regeneration-like ability to update
cache data based on some time interval. The last one I want to talk about, walking through
the foundational examples of how these bits work, is the React cache function. So different from the
Next.js cache function, the React cache function, you can kind of think about it like
memorization on the server. So I call the same thing with the same inputs, I
can essentially dup that. Now this is important because there are some instances
in the Next.js App Router that kind of are pushing you towards a model where you're
calling the same function multiple times, and that's okay. We've given you these helpers so
that you can have control over the cache state of how I call a phone function and have it not
get called multiple times. So for example, on this page, I have this call to get
products, so it's going to my database, and I'm using the same call both to generate the
initial metadata for the page, like the title, and I'm using it to display the name of the product
on the page. And when I go look at get products, I'm wrapping that function with the React
cache function, so it's only going to be ever called once. Go to the database and I get
the information. So, I reload the page here on the right in the console, I see get products one,
and then I see this page get displayed. I reload again, I only see one call to get products,
even though you can see Apple's displayed in the title and it's displayed on the page.
So, I wanted to mention that one as well too. Alright, now I want to show another example of
a CRUD app, or a Create, Read, Update, Delete, that's using forms, showing some of this
in practice a little bit more. But first, I want to talk about the architecture a little
bit. So, coming back to the differences between the Pages and the App Router, I think this diagram
hopefully explains things well. So with the Pages Router model, let's start there. You would
write code in a client component. Everything was client components in the Pages Router, and
you would have some event handler like onSubmit that would call an API route where you would
go update your database or update some source, and then that API route could return back
some JSON, where on the client you could handle this response and update the UI. In the
App Router, it's actually entirely different, it's server-based. So the App Router, I have
a form submission and that form submission can call a server action which runs on the server. It
goes to our server, it can update our database, it can revalidate our cache, and then it
can return back the new UI as well as the data in the same network roundtrip in this
RSC payload back to our UI. So, there are some foundational differences here between how
these two models work. Let's look at an example. Okay, so let's look at this to-do list example.
It's a bit more simple example from the first one that we showed. I want to walk through a couple of
different bits here. So, let me make a new to-do, enter, that's going to submit the form. You'll
see that the form went into that loading state, and then the new form was added. So, on this
page, we're fetching the to-dos from our Postgres database. Those get cached, and then I have
this "Add Todo" form that uses this hook called useFormState, which allows me to take some error
back from my server action or some success message and display it on the page. And in this instance,
I'm using the response back from the server action to send a message to a screen reader. So, in
this live area, and also inside of here, I have this submit button. The submit button is using
another hook from React called useFormStatus, and useFormStatus is going to know when a mutation
is pending. So when we're adding in a new to-do, another to-do, when I hit enter, and this
goes into a pending state, you're going to notice that the add button gets disabled, and
we're able to then see that reflected in the UI. So let's take a look at what this action
actually does. If I go into createTodo here, basically all it's doing is going to our
SQL database, it's inserting some data, and then it's revalidating the path, and
then it's returning that message back to the UI in the RSC payload, the React Server
Components payload. So, you can use this now. The cool thing about this whole model is that it
works even if JavaScript hasn't loaded yet. So, for example, let's say that I want to go in
here, let's say disable JavaScript, and I'll reload the page. And I want to delete this to-do,
great, that still works. I want to add a to-do, that also still works. And it also still works
with useFormState. So, you'll notice there was that browser loading spinner at the top because
it's doing the normal HTML form reload where the page is reloading. So, I think that's pretty cool.
I also think it's worth showing what the network tab looks like when you're doing a mutation, when
you're calling a server action, and you're going to revalidate that cache data. So, let's say
I want to do another to-do here. I hit add, and we're going to see just this one network
request. So, I see in here that another to-do pops up. I click on this value, and I see that
basically the server action is just posting back to itself, it's posting back to the same route.
There's some additional information inside of here about the request. We can even see if we go into
the payload, we can see more about what's in here, what we sent along to the server action, what the
to-do was, it's called "Another Todo". And then the server, in this instance, returns back the UI
as well as that updated state in that RSC payload. Now, going back to our first example with the
roadmap product, I want to show another cool React hook called useOptimistic. What I'm going
to do is actually slow down our network here. So, we're going to go to slow 3G. And when I showed
it for the first time, everything felt really, really fast. But what happens if I was on a slower
internet connection? Would this still feel really snappy? So, I'm going to click upvote, and
I'm going to notice that I immediately see the 4 on the right side, but it took awhile for
the information to get updated. And if I look down in the network tab, I see that even though
this waterfall, even though this request hadn't completed yet, I'm able to provide feedback
to the user right away using this optimistic UI pattern. And this also works too if I add a
new item. So let's say, "Another Another One", and I hit enter. I immediately see it show up, I
immediately see the form field get cleared out, and then down below I see, okay,
that network request has completed. Now, you might be thinking, well, what if
my internet is really, really slow? So, "really really slow", and a user submits this form
but then they want to close the browser tab. So, they hit enter and then they try to do a Command +
W to close. Well, something I've added in here is just a little bit of JavaScript code that prevents
them from leaving the site if this mutation hasn't completed. So it says, "Hey, are you sure you
want to leave the site?" In this instance, I'm going to say no, and then I see that my
mutation goes through. And this only takes just a little bit of code here. So, I have
this useEffect that is basically listening for this beforeunload event, and I can
use that based on the pending state to know whether I want to pause and ask the user
whether they want to confirm leaving the page. Now, going back up to my useOptimistic pattern,
I want to show something that was subtle but very important, which is that this pattern still
supports progressive enhancement. On the right, in my browser, I've disabled JavaScript, and when
I'm loading the page, if there's no JavaScript, or more commonly, the JavaScript hasn't
yet loaded, this can still work. So, in that instance, we're going to call the server
action through the native form submission, through the action. So if I click on "Upvote
New Feature", I see the browser reload, and I see "New Feature" here, the value goes
up to six. And because I don't have JavaScript, I don't get to have the niceties
of the optimistic UI updates. But that's okay, because once the JavaScript has
loaded, then I can use the client event handler, which is going to give me some, uh,
additional enhancements to the flow. Now, it's worth mentioning that you might want
to have a completely different pattern here, a completely different UX. If having JavaScript
disabled is actually a very important requirement for your application, you might want a
different UX, where this redirects to some different page instead. But it's nice that
you have the ability to craft that experience. Now, okay, the last thing I want to
talk about, as it relates to caching, and static and dynamic rendering, is Partial
Prerendering. Now, this is still experimental, so this is just an early look. But previously,
the way the Next.js pages' App Router worked was, you had All or Nothing static or dynamic
pages. Either you could run your whole page and pre-render as static, or you had to run it
on demand. And I'll link a video down below, and some additional information
about partial pre-rendering. But just to show an example of what this looks
like, I have my website on the right, and the majority of this page is static. But then I have
this component where I'm listing out the number of subscribers, which, it's a great time to subscribe
to the Vercel YouTube channel, if you haven't. And in this component, I want these numbers to be
dynamic. I want them to be fresh. So, in this application, I have the partial pre-rendering
flag flipped on. The shell of my application, or the majority of this page, was pre-rendered
as static. But then, this specific component, I'm able to say, "You know what, actually use
noStore, and we want this one to run dynamically." So, if I reload the page, I'm able to run just
that bit dynamically. And since the subscribers haven't updated, I can show a different
example with my blog, which is where, if I click in here, I'm able to get that new
value and see it update in the top right. Okay, so that wraps up this video. We talked
about fetching, caching, and revalidating with the Next.js App Router. Hopefully, these demos
and these examples help solve and answer some of your questions. If you still have open questions,
please leave comments down below, or if there's things you want to see us make more content
about, also just let us know. But hopefully, this puts you on the right path to understanding
caching in your next Next.js application. Peace.