ProNextJS
    Loading
    lesson

    Data Caching and Revalidation with React Server Components

    Jack HerringtonJack Herrington

    You might have noticed that Next.js caches data from fetches made inside of React Server Components. It's called the data cache, and we can experiment with it using a simple time API.

    For this example, we'll use a small express server located in the same directory as our Next.js project. The server exposes a /time endpoint on port 8080 that returns the current time, updating every second.

    Here's the code:

    // inside time-api/index.ts
    
    import express from "express";
    import cors from "cors";
    
    const port = process.env.PORT || 8080;
    const app = express();
    
    app
      .disable("x-powered-by")
      .use(cors())
      .get("/time", (_, res) => {
        return res.json({ time: new Date().toLocaleTimeString() });
      })
      .listen(port, () => {
        console.log(`REST api running on ${port}`);
      });
    

    Let's start the API server:

    cd api-time
    pnpm start
    

    Now, let's create a new route in our Next.js app at src/app/api-time. This route will fetch the current time from our API and display it on the page:

    // app/api-time/page.tsx
    
    export default async function APITime() {
      const timeReq = await fetch("http://localhost:8080/time");
      const { time } = await timeReq.json();
    
      console.log(`Render /api-time ${new Date().toLocaleTimeString()}`);
    
      return (
        <div>
          <h1 className="text-2xl">Time From API</h1>
          <p className="text-xl">{time}</p>
        </div>
      );
    }
    

    After building and starting our Next.js app, we can see that the /api-time route is being treated as a static page.

    Even though our API is constantly updating, navigating to http://localhost:3000/api-time will always display the same time. This is because Next.js statically caches the response by default.

    Using the noStore Cache Option

    To make the time update dynamically, we can tell Next.js not to cache the response. We can do this using the cache: 'no-store' option in our fetch request:

    // app/api-time/page.tsx
    
    export default async function APITime() {
      const timeReq = await fetch("http://localhost:8080/time", {
        cache: "no-store",
      });
    
      ...
    

    Now, when we build and start our app, the /api-time route will be marked as dynamic, and the time will update on each request!

    Revalidating the Cache

    As an alternative to the no-store option, we can still take advantage of static site generation while keeping our content up-to-date using revalidation.

    Next.js provides a revalidate option that allows us to specify how often a statically generated page should be regenerated.

    Let's set our API time route to revalidate every 2 seconds:

    // app/api-time/page.tsx
    
    export default async function APITime() {
      const timeReq = await fetch("http://localhost:8080/time", {
        next: {
          revalidate: 2
        },
      });
    
      ...
    

    With this change, our /api-time route remains statically generated, but Next.js will regenerate the page every 2 seconds, ensuring our displayed time is relatively up-to-date.

    On-Demand Revalidation with Server Actions

    We can also trigger revalidation on demand using Server Actions. Let's add a button to our app/page.tsx that will revalidate our /api-time route when clicked.

    Here's the code for the RevalidateAPITimeButton:

    // inside app/api-time/RevalidateAPITimeButton.tsx
    
    "use client";
    import { Button } from "@/components/ui/button";
    
    export default function RevalidateAPITimeButton({
      onRevalidate,
    }: {
      onRevalidate: () => Promise<void>;
    }) {
      return (
        <Button onClick={async () => await onRevalidate()} className="mt-4">
          Revalidate API Time
        </Button>
      );
    }
    

    Then inside of app/page.tsx, we can import and use the RevalidateAPITimeButton:

    // app/page.tsx
    
    import RevalidateAPITimeButton from "./RevalidateAPITimeButton";
    
    export default async function Home() {
      // ...existing code
        async function onRevalidate() {
        "use server";
        revalidatePath("/api-time");
      }
    
    
      console.log(`Render /api-time ${new Date().toLocaleTimeString()}`);
    
      return (
        <div>
          <h1 className="text-2xl">Time From API</h1>
          <p className="text-xl">{time}</p>
          <RevalidateAPITimeButton onRevalidate={onRevalidate} />
        </div>
      );
    }
    

    Now, when we click the button, Next.js will revalidate the /api-time route, fetching the latest time from our API. However, other statically generated content on our site, like our home page at /, will remain cached.

    Revalidating with Tags

    We can also control which pieces of content get revalidated using tags.

    Let's update our /api-time route to include a tag.

    // app/api-time/page.tsx
    
    export default async function APITime() {
      const timeReq = await fetch("http://localhost:8080/time", {
        next: {
          tags: ["api-time"],
        },
      });
    
      async function onRevalidate() {
        "use server";
        revalidateTag("api-time");
      }
    

    Now, we can revalidate everything associated with the api-time tag using revalidateTag.

    the revalidate time button

    Clicking the button will now revalidate any content tagged with api-time, ensuring our home page also displays the updated time.

    This tagging mechanism offers a flexible way to manage data invalidation across our application, especially when working with databases and filesystems. We'll explore these concepts further in the next lesson.

    Transcript

    Another thing you might have noticed is that Next.js caches fetches inside of React server components. It's called the data cache. I'll experiment with this right now. So let's go and try out this time API. It's a small Express server that is in the same directory as our next caching example.

    It puts up a time API on port 8080. It's exactly the slash time that we had with our app, but it's just constantly running in the background. So let's actually run it. So in the API time directory, we'll run pmpm start. So now we've got our time and it updates every second as you expect.

    Now let's go use the service in a new route called API time. So in my IRC I'm going to fetch that time from localhost 8080 and I'm going to get the time out of it and I'm going to render it on the page. Let's go build and start. Now we can see that this has been evaluated as a static page, API time. So let's try it out.

    So even though our time is changing in our API, if we go back to our app and then go to the API time route, we always get the same value and that's because it's statically cached. One way to get around that to make this dynamic is to specify that on the cache key we say no store. Now, if we build and start, we can see that the API time route has been marked as dynamic. If I go to my arc, we can see that the time updates. But we also have the option to do revalidation and to keep it as static.

    To do that, we can use the next key and then give it revalidate and amount time. So let's give it, again, say two seconds or so. And now the API time route is static. So let's give it a try. And every two seconds, we get an update.

    Nice. But of course, we can also do on-demand revalidation. So let's create a Revalidate API Time button. It's going to take a server action and then wrap that in a button where we revalidate the API time. We'll use revalidate path.

    We'll create a server action called onRevalidate that revalidates just API time. Then we'll bring in our button and call it. And of course we need to build and start. And now we've got our Revalidate API Time button. I can refresh as many times as I want.

    Click Revalidate API Time and now I get a new page. That's great. You can see over here that we were rendering API time, but have we revalidated everything? Let's go back up to our slash route and we can see that we did not render slash, so we are getting the static version of slash. Now there's also another way to revalidate called revalidate tag.

    So now we need to tag what data we want to invalidate. So we'll bring a tags in here. It's an array, we'll call this API time. Then down in our code, you can simply say revalidate tag, and give it the API time as the tag name. Let's try it again.

    All right, let's go take a look. And we can now revalidate home, get the static, revalidate again. Awesome, okay. So that tagging is symmetric with path, it just gives you a different way of managing the different parts of data inside of your system that you want to revalidate on a chain. This revalidate tag is going to help us a lot as we take a look at how to do caching and revalidation of non-fetched data sources, like for example, databases or files.

    Check that out in the next video.