Course
Data Fetching & Caching & Redirect
Next.js Mastery: From Fundamentals to Full-Stack
Unlock the power of Next.js with this comprehensive course! Starting with the basics, you’ll learn essential skills such as routing, data fetching, and styling. Progress through practical projects, including building your own React Notes app, to gain hands-on experience. Dive into the source code to understand the inner workings and core principles of Next.js. Perfect for developers with a basic knowledge of React and Node.js, this course ensures you’ll be ready to create high-performance full-stack applications efficiently. Join us and master Next.js, backed by industry experts and a supportive learning community.
Introduction
How do you fetch data in Next.js?
Next.js recommends using the native fetch method because it extends the native fetch with caching and revalidation mechanisms. This allows automatic reuse of request data to improve performance. However, if you are not familiar with it, you might encounter some "mysterious" situations...
Let's see how to use it specifically.
Fetching Data on the Server Side
1. Basic Usage
Next.js extends the native fetch Web API to configure caching and revalidating behavior for each server-side request. You can use fetch with async/await syntax in server components, route handlers, and Server Actions.
For example:
// app/page.jsasync function getData() { const res = await fetch('https://jsonplaceholder.typicode.com/todos') if (!res.ok) { // Handled by the nearest error.js throw new Error('Failed to fetch data') } return res.json()}
export default async function Page() { const data = await getData() return <main>{JSON.stringify(data)}</main>}
2. Default Caching
By default, Next.js automatically caches the return value of server-side fetch requests (using Data Cache behind the scenes).
// The cache option of fetch is used to control the caching behavior of the request// The default is 'force-cache', you can omit it while writingfetch('https://...', { cache: 'force-cache' })
However, these situations won't automatically cache by default:
- When used in Server Actions
- When defined in route handlers with non-GET methods
In simple terms, fetch returns results that are automatically cached when used in server components and route handlers with only GET methods.
2.1. logging Configuration
Let's illustrate this with examples. But before writing the code, let's modify the
next.config.mjs
configuration:const nextConfig = { logging: { fetches: { fullUrl: true } }};
export default nextConfig;
Currently, logging has only this one configuration, used to display fetch request and cache logs in development mode:
GET /api/cache 200 in 385ms | GET https://dog.ceo/api/breeds/image/random 200 in 20ms (caches HIT)
The log indicates:
- The API returned in 20ms with status code 200.
- The request hit the cache (HIT).
These logs help us check the caching situation (though some log results are not very accurate and need improvement).
2.2. Server Components
First, let's use it in server components. Modify
app/page.js
as follows:async function getData() { // Each call to the API returns a random cat image data const res = await fetch('https://api.thecatapi.com/v1/images/search') if (!res.ok) { throw new Error('Failed to fetch data') } return res.json()}
export default async function Page() { const data = await getData() return <img src={data[0].url} width="300" />}
Run
npm run dev
to start development mode:
In development mode, you can use a hard refresh (Command + Shift + R) in the browser to clear the cache, causing the data to change (cache: SKIP). With a regular refresh, since the cache is hit (cache: HIT), the data remains the same.
Run
npm run build && npm run start
to start the production version:
Because the fetch request's return result is cached, the image data remains the same regardless of whether it's a hard refresh or not.
2.3. GET Request in Route Handlers
Next, let's use it in route handlers. Create app/api/cache/route.js with the following code:
export async function GET() { const res = await fetch('https://dog.ceo/api/breeds/image/random') const data = await res.json() return Response.json({ data, now: Date.now() })}
Run
npm run dev
to start development mode:
In development mode, a hard refresh in the browser skips the cache, while a regular refresh hits the cache. You can see that the first hard refresh request took
128ms
, but subsequent regular refreshes return cached data in about 8ms
.
Run
npm run build && npm run start
to start the production version:
Because the fetch request's return result is cached, the API data remains the same regardless of whether it's a hard refresh or not.
3. Revalidation
In Next.js, clearing the data cache and fetching the latest data is called revalidation. Next.js provides two ways to revalidate:
- Time-based revalidation: Revalidates data after a certain time when new requests are made, suitable for data that doesn't change frequently and doesn't require freshness.
- On-demand revalidation: Manually revalidates data based on events. This can be done using tag-based or path-based revalidation, suitable for scenarios that need to display the latest data quickly.
Time-based Revalidation
To use time-based revalidation, you need to set the next.revalidate option (in seconds) when using fetch:
fetch('https://...', { next: { revalidate: 3600 } })
Or configure it through route segment options, which revalidates all fetch requests in that route segment.
// layout.jsx | page.jsx | route.jsexport const revalidate = 3600
Note: In a statically rendered route, if you have multiple requests, each with different revalidation times, the shortest time will be used for all requests. For dynamically rendered routes, each fetch request will revalidate independently.
On-demand Revalidation
On-demand revalidation is done in route handlers or Server Actions using path-based (revalidatePath) or tag-based (revalidateTag) methods.
revalidatePath
Create app/api/revalidatePath/route.js with the following code:
import { revalidatePath } from 'next/cache'
export async function GET(request) { const path = request.nextUrl.searchParams.get('path')
if (path) { revalidatePath(path) return Response.json({ revalidated: true, now: Date.now() }) }
return Response.json({ revalidated: false, now: Date.now(), message: 'Missing path to revalidate', })}
Accessing
/api/revalidatePath?path=/
updates the fetch request data on the /
route:
Accessing
/api/revalidatePath?path=/api/cache
updates the fetch request data on the /api/cache
route:
Note: These GIFs demonstrate the behavior in development mode. The revalidatePath method updates the corresponding path's fetch cache results. However, in the production version, revalidatePath only affects pages, not route handlers.
This is because
/api/cache
is statically rendered. To test revalidatePath in production, you need to switch /api/cache
to dynamic rendering, which might involve using cookies or other functions that trigger Next.js's automatic logic to skip fetch caching.To test revalidatePath for route handlers in production, use this configuration:
// Route dynamic renderingexport const revalidate = 0// Force cache fetchexport const fetchCache = 'force-cache'export async function GET() { const res = await fetch('https://dog.ceo/api/breeds/image/random') const data = await res.json() return Response.json({ data, now: Date.now() })}
This code can be revalidated by revalidatePath in production, similar to the screenshots from development mode.
revalidateTag
Next.js has a route tagging system that allows revalidating multiple fetch requests across routes. The process involves:
- Tagging requests with one or more tags when using fetch.
- Calling
revalidateTag
to revalidate all requests with the specified tag.
For example:
javascriptCopy code// app/page.jsexport default async function Page() { const res = await fetch('https://...', { next: { tags: ['collection'] } }); const data = await res.json(); // ...}
To revalidate all requests tagged with
collection
, call revalidateTag
in a Server Action:javascriptCopy code// app/actions.js'use server';
import { revalidateTag } from 'next/cache';
export default async function action() { revalidateTag('collection');}
Let's write a complete example. Modify
app/page.js
:Copy codeasync function getData() { const res = await fetch('https://api.thecatapi.com/v1/images/search', { next: { tags: ['collection'] } }); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json();}
export default async function Page() { const data = await getData(); return <img src={data[0].url} width="300" />;}
Modify
app/api/cache/route.js
:export const revalidate = 0;export const fetchCache = 'force-cache';
export async function GET() { const res = await fetch('https://dog.ceo/api/breeds/image/random', { next: { tags: ['collection'] } }); const data = await res.json(); return Response.json({ data, now: Date.now() });}
Create
app/api/revalidateTag/route.js
:import { revalidateTag } from 'next/cache';
export async function GET(request) { const tag = request.nextUrl.searchParams.get('tag'); revalidateTag(tag); return Response.json({ revalidated: true, now: Date.now() });}
Accessing
/api/revalidateTag?tag=collection
will revalidate both the /
page and the /api/cache
endpoint:
Error Handling and Revalidation
If an error occurs during revalidation, the cache continues to provide the last successfully generated data. Next.js will attempt to revalidate the data on the next request.
4. Opting out of Data Caching
fetch requests will opt out of data caching when:
- The fetch request adds the cache: 'no-store' option
- The fetch request adds the revalidate: 0 option
- The fetch request is in a route handler and uses the POST method
- The fetch request is used after using headers or cookies methods
- The route segment option const dynamic = 'force-dynamic' is configured
- The route segment option fetchCache is configured, which by default skips caching
- The fetch request uses Authorization or Cookie request headers, and there's an uncached request above it in the component tree
When using it specifically, if you don't want to cache a single request:
// layout.js | page.jsfetch('https://...', { cache: 'no-store' })
To not cache multiple requests, you can use route segment configuration options:
// layout.js | page.jsexport const dynamic = 'force-dynamic'
Next.js recommends configuring the caching behavior of each request individually, which allows for more fine-grained control over caching behavior.