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:
- 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. - 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).