- 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.
nextjs-data-fetching
安装
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-data-fetching