nextjs-data-fetching

安装量: 182
排名: #4715

安装

npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-data-fetching
Next.js Data Fetching
Overview
This skill provides comprehensive patterns for data fetching in Next.js App Router applications. It covers server-side fetching, client-side libraries integration, caching strategies, error handling, and loading states.
When to Use
Use this skill for:
Implementing data fetching in Next.js App Router
Choosing between Server Components and Client Components for data fetching
Setting up SWR or React Query integration
Implementing parallel data fetching patterns
Configuring ISR and revalidation strategies
Creating error boundaries for data fetching
Instructions
Server Component Fetching (Default)
Fetch directly in async Server Components:
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
)
=>
(
<
li
key
=
{
post
.
id
}
>
{
post
.
title
}
</
li
>
)
)
}
</
ul
>
)
;
}
Parallel Data Fetching
Fetch multiple resources in parallel:
async
function
getDashboardData
(
)
{
const
[
user
,
posts
,
analytics
]
=
await
Promise
.
all
(
[
fetch
(
'/api/user'
)
.
then
(
r
=>
r
.
json
(
)
)
,
fetch
(
'/api/posts'
)
.
then
(
r
=>
r
.
json
(
)
)
,
fetch
(
'/api/analytics'
)
.
then
(
r
=>
r
.
json
(
)
)
,
]
)
;
return
{
user
,
posts
,
analytics
}
;
}
export
default
async
function
DashboardPage
(
)
{
const
{
user
,
posts
,
analytics
}
=
await
getDashboardData
(
)
;
// Render dashboard
}
Sequential Data Fetching (When Dependencies Exist)
async
function
getUserPosts
(
userId
:
string
)
{
const
user
=
await
fetch
(
`
/api/users/
${
userId
}
`
)
.
then
(
r
=>
r
.
json
(
)
)
;
const
posts
=
await
fetch
(
`
/api/users/
${
userId
}
/posts
`
)
.
then
(
r
=>
r
.
json
(
)
)
;
return
{
user
,
posts
}
;
}
Caching and Revalidation
Time-based Revalidation (ISR)
async
function
getPosts
(
)
{
const
res
=
await
fetch
(
'https://api.example.com/posts'
,
{
next
:
{
revalidate
:
60
// Revalidate every 60 seconds
}
}
)
;
return
res
.
json
(
)
;
}
On-Demand Revalidation
Use route handlers with
revalidateTag
or
revalidatePath
:
// app/api/revalidate/route.ts
import
{
revalidateTag
}
from
'next/cache'
;
import
{
NextRequest
}
from
'next/server'
;
export
async
function
POST
(
request
:
NextRequest
)
{
const
tag
=
request
.
nextUrl
.
searchParams
.
get
(
'tag'
)
;
if
(
tag
)
{
revalidateTag
(
tag
)
;
return
Response
.
json
(
{
revalidated
:
true
}
)
;
}
return
Response
.
json
(
{
revalidated
:
false
}
,
{
status
:
400
}
)
;
}
Tag cached data for selective revalidation:
async
function
getPosts
(
)
{
const
res
=
await
fetch
(
'https://api.example.com/posts'
,
{
next
:
{
tags
:
[
'posts'
]
,
revalidate
:
3600
}
}
)
;
return
res
.
json
(
)
;
}
Opt-out of Caching
// Dynamic rendering (no caching)
async
function
getRealTimeData
(
)
{
const
res
=
await
fetch
(
'https://api.example.com/data'
,
{
cache
:
'no-store'
}
)
;
return
res
.
json
(
)
;
}
// Or use dynamic export
export
const
dynamic
=
'force-dynamic'
;
Client-Side Data Fetching
SWR Integration
Install:
npm install swr
'use client'
;
import
useSWR
from
'swr'
;
const
fetcher
=
(
url
:
string
)
=>
fetch
(
url
)
.
then
(
r
=>
r
.
json
(
)
)
;
export
function
Posts
(
)
{
const
{
data
,
error
,
isLoading
}
=
useSWR
(
'/api/posts'
,
fetcher
,
{
refreshInterval
:
5000
,
revalidateOnFocus
:
true
,
}
)
;
if
(
isLoading
)
return
<
div
>
Loading...
</
div
>
;
if
(
error
)
return
<
div
>
Failed to load posts
</
div
>
;
return
(
<
ul
>
{
data
.
map
(
(
post
:
any
)
=>
(
<
li
key
=
{
post
.
id
}
>
{
post
.
title
}
</
li
>
)
)
}
</
ul
>
)
;
}
React Query Integration
Install:
npm install @tanstack/react-query
Setup provider:
// app/providers.tsx
'use client'
;
import
{
QueryClient
,
QueryClientProvider
}
from
'@tanstack/react-query'
;
import
{
useState
}
from
'react'
;
export
function
Providers
(
{
children
}
:
{
children
:
React
.
ReactNode
}
)
{
const
[
queryClient
]
=
useState
(
(
)
=>
new
QueryClient
(
{
defaultOptions
:
{
queries
:
{
staleTime
:
60
*
1000
,
refetchOnWindowFocus
:
false
,
}
,
}
,
}
)
)
;
return
(
<
QueryClientProvider
client
=
{
queryClient
}
>
{
children
}
</
QueryClientProvider
>
)
;
}
Use in components:
'use client'
;
import
{
useQuery
}
from
'@tanstack/react-query'
;
export
function
Posts
(
)
{
const
{
data
,
error
,
isLoading
}
=
useQuery
(
{
queryKey
:
[
'posts'
]
,
queryFn
:
async
(
)
=>
{
const
res
=
await
fetch
(
'/api/posts'
)
;
if
(
!
res
.
ok
)
throw
new
Error
(
'Failed to fetch'
)
;
return
res
.
json
(
)
;
}
,
}
)
;
if
(
isLoading
)
return
<
div
>
Loading...
</
div
>
;
if
(
error
)
return
<
div
>
Error:
{
error
.
message
}
</
div
>
;
return
(
<
ul
>
{
data
.
map
(
(
post
:
any
)
=>
(
<
li
key
=
{
post
.
id
}
>
{
post
.
title
}
</
li
>
)
)
}
</
ul
>
)
;
}
See
REACT-QUERY.md
for advanced patterns.
Error Boundaries
Creating Error Boundaries
// app/components/ErrorBoundary.tsx
'use client'
;
import
{
Component
,
ReactNode
}
from
'react'
;
interface
Props
{
children
:
ReactNode
;
fallback
:
ReactNode
;
}
interface
State
{
hasError
:
boolean
;
}
export
class
ErrorBoundary
extends
Component
<
Props
,
State
>
{
constructor
(
props
:
Props
)
{
super
(
props
)
;
this
.
state
=
{
hasError
:
false
}
;
}
static
getDerivedStateFromError
(
)
:
State
{
return
{
hasError
:
true
}
;
}
componentDidCatch
(
error
:
Error
,
errorInfo
:
React
.
ErrorInfo
)
{
console
.
error
(
'Error caught by boundary:'
,
error
,
errorInfo
)
;
}
render
(
)
{
if
(
this
.
state
.
hasError
)
{
return
this
.
props
.
fallback
;
}
return
this
.
props
.
children
;
}
}
Using Error Boundaries with Data Fetching
// app/posts/page.tsx
import
{
ErrorBoundary
}
from
'../components/ErrorBoundary'
;
import
{
Posts
}
from
'./Posts'
;
import
{
PostsError
}
from
'./PostsError'
;
export
default
function
PostsPage
(
)
{
return
(
<
ErrorBoundary
fallback
=
{
<
PostsError
/>
}
>
<
Posts
/>
</
ErrorBoundary
>
)
;
}
Error Boundary with Reset
'use client'
;
import
{
Component
,
ReactNode
}
from
'react'
;
interface
Props
{
children
:
ReactNode
;
fallback
:
(
props
:
{
reset
:
(
)
=>
void
}
)
=>
ReactNode
;
}
interface
State
{
hasError
:
boolean
;
}
export
class
ErrorBoundary
extends
Component
<
Props
,
State
>
{
state
=
{
hasError
:
false
}
;
static
getDerivedStateFromError
(
)
:
State
{
return
{
hasError
:
true
}
;
}
reset
=
(
)
=>
{
this
.
setState
(
{
hasError
:
false
}
)
;
}
;
render
(
)
{
if
(
this
.
state
.
hasError
)
{
return
this
.
props
.
fallback
(
{
reset
:
this
.
reset
}
)
;
}
return
this
.
props
.
children
;
}
}
Server Actions for Mutations
// app/actions/posts.ts
'use server'
;
import
{
revalidateTag
}
from
'next/cache'
;
export
async
function
createPost
(
formData
:
FormData
)
{
const
title
=
formData
.
get
(
'title'
)
as
string
;
const
content
=
formData
.
get
(
'content'
)
as
string
;
const
response
=
await
fetch
(
'https://api.example.com/posts'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
}
,
body
:
JSON
.
stringify
(
{
title
,
content
}
)
,
}
)
;
if
(
!
response
.
ok
)
{
throw
new
Error
(
'Failed to create post'
)
;
}
revalidateTag
(
'posts'
)
;
return
response
.
json
(
)
;
}
// app/posts/CreatePostForm.tsx
'use client'
;
import
{
createPost
}
from
'../actions/posts'
;
export
function
CreatePostForm
(
)
{
return
(
<
form
action
=
{
createPost
}
>
<
input
name
=
"
title
"
placeholder
=
"
Title
"
required
/>
<
textarea
name
=
"
content
"
placeholder
=
"
Content
"
required
/>
<
button
type
=
"
submit
"
>
Create Post
</
button
>
</
form
>
)
;
}
Loading States
Loading.tsx Pattern
// app/posts/loading.tsx
export
default
function
PostsLoading
(
)
{
return
(
<
div
className
=
"
space-y-4
"
>
{
[
...
Array
(
5
)
]
.
map
(
(
_
,
i
)
=>
(
<
div
key
=
{
i
}
className
=
"
h-16 bg-gray-200 animate-pulse rounded
"
/>
)
)
}
</
div
>
)
;
}
Suspense Boundaries
// app/posts/page.tsx
import
{
Suspense
}
from
'react'
;
import
{
PostsList
}
from
'./PostsList'
;
import
{
PostsSkeleton
}
from
'./PostsSkeleton'
;
import
{
PopularPosts
}
from
'./PopularPosts'
;
export
default
function
PostsPage
(
)
{
return
(
<
div
>
<
h1
>
Posts
</
h1
>
<
Suspense
fallback
=
{
<
PostsSkeleton
/>
}
>
<
PostsList
/>
</
Suspense
>
<
Suspense
fallback
=
{
<
div
>
Loading popular...
</
div
>
}
>
<
PopularPosts
/>
</
Suspense
>
</
div
>
)
;
}
Best Practices
Default to Server Components
- Fetch data in Server Components when possible for better performance
Use parallel fetching
- Use
Promise.all()
for independent data requests
Choose appropriate caching
:
Static data: Long revalidation intervals or no revalidation
Dynamic data: Short revalidation or
cache: 'no-store'
User-specific: Use dynamic rendering
Handle errors gracefully
- Wrap client data fetching in error boundaries
Use loading states
- Implement
loading.tsx
or Suspense boundaries
Prefer SWR/React Query for
:
Real-time data
User interactions requiring immediate feedback
Data that needs background updates
Use Server Actions for
:
Form submissions
Mutations that need to revalidate cache
Operations requiring server-side logic
Constraints and Warnings
Critical Constraints
Server Components cannot use hooks like
useState
,
useEffect
, or data fetching libraries (SWR, React Query)
Client Components must include the
'use client'
directive
The
fetch
API in Next.js extends the standard Web API with Next.js-specific caching options
Server Actions require the
'use server'
directive and can only be called from Client Components or form actions
Common Pitfalls
Fetching in loops
Avoid fetching data inside loops in Server Components; use parallel fetching instead
Cache poisoning
Be careful with
cache: 'force-cache'
for user-specific data
Memory leaks
Always clean up subscriptions in Client Components when using real-time data
Hydration mismatches
Ensure server and client render the same initial state when using React Query hydration Decision Matrix Scenario Solution Static content, infrequent updates Server Component + ISR Dynamic content, user-specific Server Component + cache: 'no-store' Real-time updates Client Component + SWR/React Query User interactions Client Component + mutation library Mixed requirements Server for initial, Client for updates Examples Example 1: Basic Server Component with ISR Input: Create a blog page that fetches posts and updates every hour. // app/blog/page.tsx async function getPosts ( ) { const res = await fetch ( 'https://api.example.com/posts' , { next : { revalidate : 3600 } } ) ; return res . json ( ) ; } export default async function BlogPage ( ) { const posts = await getPosts ( ) ; return ( < main

< h1

Blog Posts </ h1

{ posts . map ( post => ( < article key = { post . id }

< h2

{ post . title } </ h2

< p

{ post . excerpt } </ p

</ article

) ) } </ main

) ; } Output: Page statically generated at build time, revalidated every hour. Example 2: Parallel Data Fetching for Dashboard Input: Build a dashboard showing user profile, stats, and recent activity. // app/dashboard/page.tsx async function getDashboardData ( ) { const [ user , stats , activity ] = await Promise . all ( [ fetch ( '/api/user' ) . then ( r => r . json ( ) ) , fetch ( '/api/stats' ) . then ( r => r . json ( ) ) , fetch ( '/api/activity' ) . then ( r => r . json ( ) ) , ] ) ; return { user , stats , activity } ; } export default async function DashboardPage ( ) { const { user , stats , activity } = await getDashboardData ( ) ; return ( < div className = " dashboard "

< UserProfile user = { user } /> < StatsCards stats = { stats } /> < ActivityFeed activity = { activity } /> </ div

) ; } Output: All three requests execute concurrently, reducing total load time. Example 3: Real-time Data with SWR Input: Display live cryptocurrency prices that update every 5 seconds. // app/crypto/PriceTicker.tsx 'use client' ; import useSWR from 'swr' ; const fetcher = ( url : string ) => fetch ( url ) . then ( r => r . json ( ) ) ; export function PriceTicker ( ) { const { data , error } = useSWR ( '/api/crypto/prices' , fetcher , { refreshInterval : 5000 , revalidateOnFocus : true , } ) ; if ( error ) return < div

Failed to load prices </ div

; if ( ! data ) return < div

Loading... </ div

; return ( < div className = " ticker "

< span

BTC: $ { data . bitcoin } </ span

< span

ETH: $ { data . ethereum } </ span

</ div

) ; } Output: Component displays live-updating prices with automatic refresh. Example 4: Form Submission with Server Action Input: Create a contact form that submits data and refreshes the cache. // app/actions/contact.ts 'use server' ; import { revalidateTag } from 'next/cache' ; export async function submitContact ( formData : FormData ) { const data = { name : formData . get ( 'name' ) , email : formData . get ( 'email' ) , message : formData . get ( 'message' ) , } ; await fetch ( 'https://api.example.com/contact' , { method : 'POST' , body : JSON . stringify ( data ) , } ) ; revalidateTag ( 'messages' ) ; } // app/contact/page.tsx import { submitContact } from '../actions/contact' ; export default function ContactPage ( ) { return ( < form action = { submitContact }

< input name = " name " placeholder = " Name " required /> < input name = " email " type = " email " placeholder = " Email " required /> < textarea name = " message " placeholder = " Message " required /> < button type = " submit "

Send </ button

</ form

) ; } Output: Form submits via Server Action, cache is invalidated on success.

返回排行榜