How On-Demand Revalidation works in Nextjs?

In Next.js 14, both default revalidation and on-demand revalidation can be combined to keep your pages fresh. Default revalidation allows pages to update at fixed intervals, while on-demand revalidation ensures immediate updates when content changes. Using server actions, we can securely update the database and trigger revalidation from the server side, ensuring the best performance and real-time updates.

Here’s how the folder structure looks in the Recipe App, using server actions for updates and revalidation:

src/
└── app/
    β”œβ”€β”€ recipes/
       β”œβ”€β”€ [id]/
       β”‚   └── page.tsx       # Recipe detail page with default and on-demand revalidation
       └── [id]/
           └── edit/
               └── page.tsx   # Edit page calling a server action
    β”œβ”€β”€ server-actions
	    β”œβ”€β”€recipe.ts  

Default Revalidation

The recipe detail page will revalidate every hour by setting the revalidate property. This ensures that if no edits are made, the page is updated at regular intervals.

Recipe Detail Page Code:

// src/app/recipes/[id]/page.tsx
import { fetch } from 'next/navigation';

// Revalidate the page every hour (3600 seconds)
export const revalidate = 3600;

export default async function RecipeDetail({ params }) {
  const recipe = await fetch(`https://api.example.com/recipes/${params.id}`).then(res => res.json());

  return (
    <section>
      <h2>{recipe.name}</h2>
      <p>{recipe.description}</p>
    </section>
  );
}

Explanation:

  • Revalidate Every Hour: export const revalidate = 3600; triggers automatic revalidation every hour.
  • Static Generation: The page is pre-generated but will refresh at the specified interval.

Recipe Edit Page (Client Component)

When a user edits a recipe, we want the recipe detail page to update immediately. To achieve this, we’ll use a server action to handle the recipe update in the database and trigger revalidation on the server.

The client component (the edit page) submits the form, calling a server action to update the recipe and trigger the revalidation.

// src/app/recipes/[id]/edit/page.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { updateRecipe } from '@/server-actions/recipe';  // Server action imported

export default function EditRecipe({ params }) {
  const [recipeName, setRecipeName] = useState('');
  const [recipeDescription, setRecipeDescription] = useState('');
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Call the server action to update the recipe and trigger revalidation
    await updateRecipe({
      id: params.id,
      name: recipeName,
      description: recipeDescription,
    });

    // Navigate back to the recipe detail page after updating
    router.push(`/recipes/${params.id}`);
  };

  return (
    <section>
      <h2>Edit Recipe</h2>
      <form onSubmit={handleSubmit}>
        <label>
          Recipe Name:
          <input
            type="text"
            value={recipeName}
            onChange={(e) => setRecipeName(e.target.value)}
          />
        </label>
        <label>
          Description:
          <textarea
            value={recipeDescription}
            onChange={(e) => setRecipeDescription(e.target.value)}
          />
        </label>
        <button type="submit">Save Recipe</button>
      </form>
    </section>
  );
}
  • Form Submission: The form sends the data to the server action updateRecipe.
  • On-Demand Revalidation: The server action handles both the database update and triggering revalidatePath().

Server Action

Updating the Recipe and Triggering Revalidation

The server action will handle the recipe update in the database and trigger the revalidatePath function to revalidate the recipe detail page immediately.

// src/app/server-actions/recipe.ts

'use server'
import { revalidatePath } from 'next/cache';

export async function updateRecipe({ id, name, description }) {
  // Placeholder: Update the recipe in your database
  await fetch(`https://api.example.com/recipes/${id}`, {
    method: 'POST',
    body: JSON.stringify({ name, description }),
  });

  // Revalidate the recipe detail page
  await revalidatePath(`/recipes/${id}`);
}
  • Database Update: This is a placeholder for the actual database update logic, which could be a call to your API or directly updating your database.
  • Revalidate Path: After the recipe is updated, revalidatePath() is called to trigger on-demand revalidation for the updated recipe page.

How Default and On-Demand Revalidation Work Together?

Here’s how both strategies work together:

  1. Default Revalidation: The recipe detail page is set to revalidate every hour (export const revalidate = 3600). This ensures the page is updated periodically, even if no edits are made.
  2. On-Demand Revalidation: When the recipe is edited through the edit page, the server action updates the recipe in the database and immediately triggers revalidation for the specific recipe page. This ensures that the changes are reflected immediately without waiting for the next scheduled revalidation.

Example Scenario:

  • At 12:00 PM, the recipe page for "Vegan Burger" is generated and cached. It is set to revalidate at 1:00 PM.
  • At 12:30 PM, the user edits the recipe. The server action updates the recipe and immediately revalidates the "Vegan Burger" page.
  • Now, any user visiting the recipe page at 12:35 PM will see the updated recipe.
  • The recipe page will still revalidate again at 1:00 PM, ensuring regular updates in the absence of further edits.

revaldiatePath()

revalidatePath is used to programmatically trigger revalidation for a specific page or a group of pages under a layout. This function works on the path level, and it allows you to selectively refresh content when needed.

revalidatePath() for Pages

Use Case: You have a dynamic page, like a recipe detail page, and when the content of that page changes (e.g., the recipe is edited), you want to immediately refresh that page so users see the updated content without waiting for scheduled revalidation.

Example:

await revalidatePath('/recipes/vegan-burger', 'page');

This triggers revalidation for the specific path /recipes/vegan-burger. The next request for this page will return the updated content.

revalidatePath() for Layouts

Use Case: If you have a shared layout for a section of your app, like all recipe pages, and you update something in the layout (e.g., navigation or a header), you can trigger a revalidation for all pages under that layout. This ensures all nested pages are refreshed.

Example:

await revalidatePath('/recipes','layout');

This triggers revalidation for all pages under the /recipes path, ensuring that all pages sharing the layout get refreshed.

Page vs. Layout

You can differentiate between revalidating a specific page and revalidating a layout by specifying the type of content to revalidate:

Page-Specific Revalidation:

await revalidatePath('/recipes/vegan-burger', 'page');

This revalidates only the /recipes/vegan-burger page, leaving other pages or layout-level content untouched. Layout-Specific Revalidation:

await revalidatePath('/recipes', 'layout');

This revalidates the layout for /recipes and affects all pages nested under this layout.

revalidateTag()

Let’s rethink the recipe example to make better use of revalidateTag in a practical scenario.

Scenario: Recipe Ingredients Update

Imagine you have a Recipe App where each recipe page lists not only the recipe details but also common ingredients that are shared across multiple recipes. Let’s say there is a central database for ingredients, and some of these ingredients change (e.g., an ingredient's availability, nutritional info, or brand). In this case, you want to revalidate all pages that use that specific ingredient to reflect the latest updates.

For example, many recipes use olive oil. If the information about olive oil changes (e.g., it's out of stock, or its nutrition information is updated), you would want to revalidate all recipes that include olive oil.

src/
└── app/
    β”œβ”€β”€ ingredients/
    β”‚   └── [id]/
    β”‚       └── page.tsx        # Ingredient detail page (olive oil)
    β”œβ”€β”€ recipes/
    β”‚   └── [id]/
    β”‚       └── page.tsx        # Recipe detail page

Associating a Tag with Ingredients in Recipe Pages

Each recipe that includes olive oil should be tagged with the olive-oil tag. When the ingredient's details change, you can revalidate all recipes that use it.

// src/app/recipes/[id]/page.tsx
export default async function RecipeDetail({ params }) {
  const recipe = await fetch(`https://api.vercel.app/recipes/${params.id}`, {
    next: { tags: ['/ingredients/olive-oil'] }, // Tagging recipe with 'olive-oil' tag
  }).then(res => res.json());

  return (
    <section>
      <h2>{recipe.name}</h2>
      <p>{recipe.description}</p>
      <ul>
        {recipe.ingredients.map((ingredient) => (
          <li key={ingredient.id}>{ingredient.name}</li>
        ))}
      </ul>
    </section>
  );
}
  • Tags: The recipe page is tagged with /ingredients/olive-oil because it uses olive oil as an ingredient.
  • Multiple Recipes: If many recipes use olive oil, all these pages will be tagged the same way, allowing batch revalidation.

Triggering Revalidation for Ingredient Updates

When the olive oil ingredient is updated in the central ingredients database, you’ll want to trigger revalidation for all recipes that include it.

Example Code for Revalidating Pages Using Olive Oil:

import { revalidateTag } from 'next/cache';

export async function updateIngredient() {
  // Placeholder: Update olive oil details in the database
  await fetch(`https://api.vercel.app/ingredients/olive-oil`, {
    method: 'POST',
    body: JSON.stringify({
      name: 'Olive Oil',
      availability: 'Out of Stock',
      nutritionalInfo: 'Updated information',
    }),
  });

  // Revalidate all recipe pages that use 'olive-oil'
  await revalidateTag('/ingredients/olive-oil');
}
  • Ingredient Update: The ingredient "olive oil" is updated in the database (e.g., it is marked as out of stock).
  • Revalidate Recipes: revalidateTag('/ingredients/olive-oil') triggers revalidation for all recipes that are tagged with olive-oil.
  • This ensures that any recipe using olive oil will reflect the latest ingredient information.

When to Use

This approach is ideal for cases where multiple pages are affected by a common piece of data. In our case, all recipes that use the same ingredient will be updated together, ensuring consistency across your app.

Example Use Case:

  • You update the nutritional information or availability of olive oil.
  • Use revalidateTag('/ingredients/olive-oil') to revalidate all recipes that include olive oil as an ingredient.
await revalidateTag('/ingredients/olive-oil');

This ensures that all relevant pages are updated simultaneously.


By using revalidatePath and revalidateTag, you can build a flexible revalidation strategy that ensures your app stays up-to-date with minimal performance overhead. Both approaches give you granular control over which parts of your app are refreshed and when.

Summary

In this tutorial, we’ve shown how to combine default revalidation and on-demand revalidation in Next.js 14 App Router using server actions:

  • Default Revalidation: Ensures that pages are updated at regular intervals (e.g., every hour).
  • On-Demand Revalidation: Allows immediate page updates after content is edited, using the revalidatePath() function in a server action.
  • Server Actions: Securely handle database updates and trigger revalidation without exposing the logic to the client.

This approach provides both performance (via static generation) and flexibility (via real-time updates).

Next.js
Clap here if you liked the blog