Next.js App Router Caching: Explained!

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Vercel
Views: 38,376
Rating: undefined out of 5
Keywords:
Id: VBlSe8tvg4U
Channel Id: undefined
Length: 25min 22sec (1522 seconds)
Published: Mon Jan 15 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.