Quick overview of caching in Next.JS.
A caching problem I encountered.
Recently, while I was working on a NextJs 14/TailwindCSS/Typescript project, I created a function that fetches data from an API route using the fetch API. The function is used on a page(a server component). Below is an example code snippet:
const getPosts = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", );
console.log(response);
if (response.ok) {
return response.json();
}
} catch (err) {
console.log(err);
}
};
export default async function Home() {
const response = await getPosts()
return (
<main className="flex min-h-screen flex-col justify-between p-24">
<h1>Posts</h1>
{
response.map((post : any, index: number) => {
return (<h2 key={post.id}> {index+1} {post.title}</h2> )
})
}
</main>
)
}
Everything looked good and worked perfectly. The data is fetched successfully and displayed on the page.
On another page, I created a form, the form uses server actions to create a new post. Below is an example code snippet of the page. Following the code snippet of the page, is the code snippet of the createPost function.
"use client";
import { useState } from "react";
import { createPost } from "@/lib/actions/createPost";
export default function Create() {
const [formData, setFormData] = useState({ title: "", body: "" });
const onSubmit = async () => {
try {
const res = await createPost(formData);
console.log('onsubmit res: ', res)
} catch (error) {
console.log({error})
}
};
return (
<section className="flex flex-col p-24">
<h1>Create Post</h1>
<form action={onSubmit}>
<fieldset>
<label htmlFor="title">Title</label>
<input
type="text"
value={formData.title}
onChange={(e) =>
setFormData((prev) => ({ ...prev, title: e.target.value }))
}
name="title"
className="border"
/>
</fieldset>
<fieldset>
<label htmlFor="body">Body</label>
<input
type="text"
name="body"
value={formData.body}
onChange={(e) =>
setFormData((prev) => ({ ...prev, body: e.target.value }))
}
className="border"
/>
</fieldset>
<button type='submit'>
Submit
</button>
</form>
</section>
);
}
'use server'
import axios from "axios";
type FormData = {
title: string;
body: string;
}
export const createPost = async (formData: FormData) => {
const postData = {
...formData,
userId: 1,
};
try {
const response = await axios.post('https://jsonplaceholder.typicode.com/posts', postData, {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
});
console.log( 'createpost res:', response);
return response.data
} catch (error) {
console.error('Error:', error);
return error
}
};
When I tried creating a new post, a response status of 201, response Ok of true, was returned from the request, indicating that the post has been created successfully. However, on the page where I'm fetching all posts, when I reloaded the page to fetch the latest data, the new post was not included.
Why?
In short, because Next.JS caches fetch requests made on the server.
When a request is made on the server using fetch, Next.JS automatically caches the data so it doesn't need to be re-fetched from the data source on every request. So even when I reloaded the page and the request was made again, the data did not change because the request returned the data from the sever's data cache instead of getting it from the data source.
Solution.
While NextJS caches data from requests made on the server, it also provides ways to revalidate data.
Revalidation simply means to invalidate the data so that the data can be re- fetched from the data source.
There are two ways to revalidate data in Next.JS:
Time-based revalidation.
On-demand revalidation.
To solve the caching problem I was facing, I utilized the time-based revalidation method. Below is an updated version the getPosts function.
const getPosts = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
next: { revalidate: 60 }, //here
});
console.log(response);
if (response.ok) {
return response.json();
}
} catch (err) {
console.log(err);
}
};
In the above code snippet, I added next: { revalidate: 60} to the options object of the fetch method. Adding next: { revalidate: 60} will invalidate the data in the data cache after 60 seconds.
Voila! It seems my problem has been solved! I just have to wait for a 60 seconds and then the updated data would be fetched from the data source. So I waited. After about five minutes, the stale data was still being displayed on the page, I still couldn't see the post I created.
Why?
Because a request is not automatically made when the revalidation time elapses, the route(page or path) that houses the request has to be revisited to trigger the request to be made.
So, I thought, I just need to reload the page, or revisit the route that the request is being made in, so that the request is made again, and since the data has been invalidated, the request will return the fresh data and not the cached data. Well, I reloaded the page, and I still couldn't see the post I created.
Why?
Well, this is the full story from Next.JS Docs:
The first time a fetch request with revalidate is called, the data will be fetched from the external data source and stored in the Data Cache.
Any requests that are called within the specified timeframe (e.g. 60-seconds) will return the cached data.
After the timeframe, the next request will still return the cached (now stale) data.
Next.js will trigger a revalidation of the data in the background.
Once the data is fetched successfully, Next.js will update the Data Cache with the fresh data.
If the background revalidation fails, the previous data will be kept unaltered.
Therefore, what this means is that after I reloaded the page, the request was made again but it still returned the cached data. However, a request to the data source was running in the background, and if that background request is successful, the data cache will be updated with the fresh data, if not, the stale data will remain in the data cache.
Finally, to fetch the fresh data, to confirm if the background revalidation was successful, the request has to be made again. So I reloaded the page again and finally I saw the post I created.