Profile Photo

Caching in Nextjs

Created on: Aug 23, 2024

Let's clone a initial app from github.

git clone https://github.com/keshav-repo/nextjs/tree/master cd caching/complete npm install

Start mongodb and create a db test. We can use docker to quickly start mongodb using below

docker run --name mongodb -p 27017:27017 -d mongodb/mongodb-community-server:latest

Insert some data in employees collection.

use test; db.employees.insertOne({ "id": 1, "name": "Anna", "address": "somewhere in Venus", "department": "HR" }); db.employees.insertOne({ "id": 2, "name": "John", "address": "somewhere in Mars", "department": "IT" });

We can run app now.

npm run build npm run start

Now let's fetch root page. click on http://localhost:3000/ to fetch data. you can see two rows in employee table. Now let's add one more row in mongodb.

db.employees.insertOne({ "id": 3, "name": "Ana", "address": "somewhere in Jupiter", "department": "IT" });

Again refresh the page. You wouldn't see the new row. Because root page is a static page which is build during compilation. This is the default behavior. We can change by adding cookies or headers in home page.

Try replacing the code with below and build again.

import { cookies } from "next/headers"; import { fetchEmployees } from "./lib/EmployeeLibrary"; import { Employee } from "./lib/defination"; export default async function Home() { cookies(); const employees: Employee[] = await fetchEmployees(); return ( <main> <table> <thead> <tr> <th>Name</th> <th>Department</th> <th>Address</th> </tr> </thead> <tbody> {employees.map((employee, index) => ( <tr key={index}> <td>{employee.name}</td> <td>{employee.department}</td> <td>{employee.address}</td> </tr> ))} </tbody> </table> </main> ); }

Now we can add more entries in collection and it will reflect.

Caching in fetch

Let's browse http://localhost:3000/employee/1

After this, update the collection for above employee using below update. And browse the page again. You will not find any change.

db.employees.updateOne( { "id": 1 }, { $set: { "name": "Anna", "address": "somewhere in Saturn", "department": "HR" } } );

This is possible because Next.js extends the native fetch API to allow each request on the server to set its own persistent caching semantics.

Now add Nostore before call of this function. This will stop caching the result and we can fetch the endpoint and get the data.

// src/app/lib/EmployeeLibrary.ts import { unstable_noStore as nostore } from "next/cache"; export const fetchEmployeesById = async (id: string): Promise<Employee> => { try { nostore(); const res = await fetch(`http://localhost:3000/api/employee?empId=${id}`); const employees: Employee = await res.json(); console.log(`calling api to fetch data`); return employees; } catch (err) { console.error(`error fetching article`); throw err; } };

If we want to cache data and wants to revalidated, there are two way.

  1. Time-based Revalidation
  2. On-demand Revalidation

Time-based Revalidation

To revalidate data at a timed interval, you can use the next.revalidate option of fetch to set the cache lifetime of a resource (in seconds).

fetch('https://...', { next: { revalidate: <seconds> } })

let's change the code and give 1 minute of lifetime.

export const fetchEmployeesById = async (id: string): Promise<Employee> => { try { const res = await fetch(`http://localhost:3000/api/employee?empId=${id}`, { next: { revalidate: 60 }, }); const employees: Employee = await res.json(); console.log(`calling api to fetch data`); return employees; } catch (err) { console.error(`error fetching article`); throw err; } };

You can update the collection and check after 1 minute.

On-demand Revalidation

There are two way to revalidate on demand.

  1. revalidatePath
  2. revalidateTag

revalidatePath

revalidatePath(path: string, type?: 'page' | 'layout'): void;

Docs

import { fetchEmployeesById } from "@/app/lib/EmployeeLibrary"; import { Employee } from "@/app/lib/defination"; import { revalidatePath } from "next/cache"; export default async function EmployeePage({ params, }: { params: { empId: string }, }) { revalidatePath("/api/employee"); // this will revalidate the path const employee: Employee = await fetchEmployeesById(params.empId); return ( <main> <table> <tr> <th>Name</th> <th>Department</th> <th>Address</th> </tr> <tr> <td>{employee.name}</td> <td>{employee.department}</td> <td>{employee.address}</td> </tr> </table> </main> ); }

revalidateTag

We can set some tags for the data we cached and revalidate when required.

// Cache data with a tag fetch(`https://...`, { next: { tags: ["a", "b", "c"] } }); // Revalidate entries with a specific tag revalidateTag("a");

Db or data fetch from Non fetch api

In case of data fetched from db, We can use unstable_cache to cache data and configure accordingly. Below is syntax according to official docs

const data = unstable_cache(fetchData, keyParts, options)();

Let's apply this logic for / page. Firstly we will use cookies to stop determining content at run time. And then we will use unstable_cache for a lifetime of 1 minute.

Below is the updated code.

// src/app/page.tsx import { cookies } from "next/headers"; import { fetchEmployees } from "./lib/EmployeeLibrary"; import { Employee } from "./lib/defination"; export default async function Home() { cookies(); const employees: Employee[] = await fetchEmployees(); return ( <main> <table> <thead> <tr> <th>Name</th> <th>Department</th> <th>Address</th> </tr> </thead> <tbody> {employees.map((employee, index) => ( <tr key={index}> <td>{employee.name}</td> <td>{employee.department}</td> <td>{employee.address}</td> </tr> ))} </tbody> </table> </main> ); } // src/app/lib/EmployeeLibrary.ts import { Employee } from "./defination"; import { EmployeeModel } from "../model"; import { connectDB } from "./mongodb"; import { unstable_cache } from "next/cache"; export const fetchEmployees = unstable_cache( async () => { try { await connectDB(); const employees: Employee[] | null = await EmployeeModel.find({}); console.log(`data fetched from db `); return employees; } catch (err) { console.error(`error fetching article`); throw err; } }, undefined, { revalidate: 60, // this is to set lifetime of cache } );

You can add or update employees collection and see the changes.

Full Route Cache

Static routes are cached by default, whereas dynamic routes are rendered at request time, and not cached. By default, the Full Route Cache is persistent. This means that the render output is cached across user requests.

There are two ways you can invalidate the Full Route Cache:

  1. Revalidating Data
  2. Redeploying

Route Segment Config

The Route Segment options allows you to configure the behavior of a Page, Layout, or Route Handler by directly exporting the variables. Check Docs

Let's export revalidate to 30 seconds and check. Do change the code and update in mongodb to see changes. You can see the changes after 30 seconds.

import { cookies } from "next/headers"; import { fetchEmployees } from "./lib/EmployeeLibrary"; import { Employee } from "./lib/defination"; export const revalidate = 30; // this will enable revalidation after 30 seconds. export default async function Home() { const employees: Employee[] = await fetchEmployees(); return ( <main> <table> <thead> <tr> <th>Name</th> <th>Department</th> <th>Address</th> </tr> </thead> <tbody> {employees.map((employee, index) => ( <tr key={index}> <td>{employee.name}</td> <td>{employee.department}</td> <td>{employee.address}</td> </tr> ))} </tbody> </table> </main> ); } // we will remove using of unstable_cache to check the behavior export const fetchEmployees = async () => { try { await connectDB(); const employees: Employee[] | null = await EmployeeModel.find({}); console.log(`data fetched from db `); return employees; } catch (err) { console.error(`error fetching article`); throw err; } };

We can change other default behavior such as fetchCache, runtime and so on.

You can check the full code github