mirror of
https://github.com/greedychipmunk/agent-skills.git
synced 2026-09-14 20:46:42 +08:00
8.0 KiB
8.0 KiB
Next.js Data Fetching Guide
Overview
Next.js App Router provides powerful data fetching capabilities with automatic caching and revalidation.
Server Component Data Fetching
Basic Fetch
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts")
if (!res.ok) {
throw new Error("Failed to fetch posts")
}
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Direct Database Access
// app/users/page.tsx
import { db } from "@/lib/db"
export default async function UsersPage() {
const users = await db.user.findMany()
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
Caching Strategies
Static Data (Default)
// Cached indefinitely until revalidated
async function getData() {
const res = await fetch("https://api.example.com/data")
return res.json()
}
Dynamic Data (No Cache)
// Never cached, always fresh
async function getData() {
const res = await fetch("https://api.example.com/data", {
cache: "no-store",
})
return res.json()
}
Time-based Revalidation
// Revalidate every hour
async function getData() {
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 3600 },
})
return res.json()
}
Route Segment Config
Force Dynamic Rendering
// app/dashboard/page.tsx
export const dynamic = "force-dynamic"
// Options: 'auto' | 'force-dynamic' | 'error' | 'force-static'
Revalidation at Route Level
// app/blog/page.tsx
export const revalidate = 3600 // Revalidate every hour
// Use 0 for no caching, false to cache indefinitely
Dynamic Params
// app/posts/[id]/page.tsx
export const dynamicParams = true // Allow params not in generateStaticParams
// false = return 404 for unknown params
On-Demand Revalidation
Revalidate by Path
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache"
import { NextRequest } from "next/server"
export async function POST(request: NextRequest) {
const path = request.nextUrl.searchParams.get("path")
if (path) {
revalidatePath(path)
return Response.json({ revalidated: true, now: Date.now() })
}
return Response.json({
revalidated: false,
message: "Missing path param",
})
}
Revalidate by Tag
// Fetch with tag
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
})
return res.json()
}
// Revalidate the tag
import { revalidateTag } from "next/cache"
export async function POST() {
revalidateTag("posts")
return Response.json({ revalidated: true })
}
Parallel Data Fetching
Using Promise.all
// app/dashboard/page.tsx
async function getUser(userId: string) {
const res = await fetch(`/api/users/${userId}`)
return res.json()
}
async function getPosts(userId: string) {
const res = await fetch(`/api/users/${userId}/posts`)
return res.json()
}
export default async function Dashboard({
params,
}: {
params: { userId: string }
}) {
// Fetch in parallel
const [user, posts] = await Promise.all([
getUser(params.userId),
getPosts(params.userId),
])
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
Using Suspense for Streaming
// app/dashboard/page.tsx
import { Suspense } from "react"
async function UserInfo({ userId }: { userId: string }) {
const user = await getUser(userId)
return <h1>{user.name}</h1>
}
async function UserPosts({ userId }: { userId: string }) {
const posts = await getPosts(userId)
return (
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default function Dashboard({
params,
}: {
params: { userId: string }
}) {
return (
<div>
<Suspense fallback={<div>Loading user...</div>}>
<UserInfo userId={params.userId} />
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<UserPosts userId={params.userId} />
</Suspense>
</div>
)
}
Sequential Data Fetching
When data depends on previous results:
// app/artist/[id]/page.tsx
async function getArtist(id: string) {
const res = await fetch(`/api/artists/${id}`)
return res.json()
}
async function getAlbums(artistId: string) {
const res = await fetch(`/api/artists/${artistId}/albums`)
return res.json()
}
export default async function ArtistPage({
params,
}: {
params: { id: string }
}) {
// Sequential: albums depend on artist
const artist = await getArtist(params.id)
const albums = await getAlbums(artist.id)
return (
<div>
<h1>{artist.name}</h1>
<ul>
{albums.map((album: Album) => (
<li key={album.id}>{album.title}</li>
))}
</ul>
</div>
)
}
Preloading Data
// lib/data.ts
import { cache } from "react"
export const getUser = cache(async (id: string) => {
const res = await fetch(`/api/users/${id}`)
return res.json()
})
export const preloadUser = (id: string) => {
void getUser(id)
}
// app/user/[id]/page.tsx
import { getUser, preloadUser } from "@/lib/data"
export default async function UserPage({
params,
}: {
params: { id: string }
}) {
// Start loading early
preloadUser(params.id)
// ... other work
const user = await getUser(params.id)
return <div>{user.name}</div>
}
Static Generation
generateStaticParams
// app/posts/[id]/page.tsx
export async function generateStaticParams() {
const posts = await fetch("https://api.example.com/posts").then((res) =>
res.json()
)
return posts.map((post: Post) => ({
id: post.id.toString(),
}))
}
export default async function PostPage({
params,
}: {
params: { id: string }
}) {
const post = await getPost(params.id)
return <article>{post.content}</article>
}
Generating Multiple Params
// app/[lang]/[slug]/page.tsx
export async function generateStaticParams() {
const products = await getProducts()
return products.flatMap((product) =>
["en", "es", "fr"].map((lang) => ({
lang,
slug: product.slug,
}))
)
}
Request Memoization
React automatically memoizes fetch requests with the same URL and options:
// This fetch is called in multiple components
async function getItem(id: string) {
// Only one request is made even if called multiple times
const res = await fetch(`/api/items/${id}`)
return res.json()
}
// Component A
async function ComponentA({ id }: { id: string }) {
const item = await getItem(id) // Request 1
return <div>{item.name}</div>
}
// Component B
async function ComponentB({ id }: { id: string }) {
const item = await getItem(id) // Deduplicated, uses cached result
return <div>{item.description}</div>
}
Error Handling
Try-Catch Pattern
async function getData() {
try {
const res = await fetch("https://api.example.com/data")
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
return res.json()
} catch (error) {
console.error("Fetch error:", error)
throw error // Re-throw to trigger error boundary
}
}
With Error Boundary
// app/posts/error.tsx
"use client"
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Failed to load posts</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Retry</button>
</div>
)
}
Last Updated: January 2026