Next.js Data Fetching. Modern Next.js App Router provides multiple data fetching techniques, Server-first by default, Built-in streaming & Suspense, Smart request memoization & caching.
This tutorial covers:
- Server Component data fetching
- Client Component strategies
- Streaming with Suspense and loading.js
- Caching with React.cache Best practices
Server Components (Default & Recommended)
In Next.js App Router, everything is a Server Component by default.
export default async function Page() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await res.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Key behaviors
- You can use async/await directly in components
- Runs only on the server
- No API layer required
- Safe to access DB / secrets
Important details:
- fetch is memoized automatically in a render tree
- Requests are NOT cached by default unless configured
- The page blocks rendering until data resolves (unless streaming is used)
Parallel vs Sequential Fetching
Sequential (slow)
const user = await getUser() const posts = await getPosts()
Parallel (better)
Using javascript promise.all()
const [user, posts] = await Promise.all([ getUser(), getPosts(), ])
Client Components Data Fetching
Client Components (“use client”) cannot directly use async component functions. You have 2 main strategies:
Strategy A — Pass Promise from Server → Client (Recommended)
By using React Suspense and the use API:
Server component:
import Posts from './Posts'
import { Suspense } from 'react'
const getPosts = () => fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json())
export default function Page() {
const postsPromise = getPosts() // don't await
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={postsPromise} />
</Suspense>
)
}
Client component:
'use client'
import { use } from 'react'
export default function Posts({ posts }) {
const data = use(posts)
return (
<ul>
{data.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
)
}
In this code, server starts fetching early and pass data to client. Client component unwraps the “promise” with use. This method is the preferred pattern.
Strategy B — Client-side fetching (traditional)
Using libraries such as react-query or SWR:
'use client'
import useSWR from 'swr'
export default function Component() {
const { data } = useSWR('https://jsonplaceholder.typicode.com/posts', fetcher)
if (!data) return <div>Loading...</div>
return <div>{data.length}</div>
}
Streaming UI with Suspense
Without streaming, slow data blocks the whole page.
With Suspense:
<Suspense fallback={<div>Loading posts...</div>}>
<Posts posts={postsPromise} />
</Suspense>
In that case, page renders immediately and suspended parts loads later and UI progressively fills later. This is called streaming rendering.
What about loading states? There are two loading states, the first is by using Suspense fallback prop, described above:
<Suspense fallback={<LoadingSkeleton />}>
<BlogList />
</Suspense>
The other way is by creating a loading.js file beside the page component, this is for (Route Level Loading):
app/mypage/loading.js:
export default () => {
return (
<div className="max-w-sm rounded overflow-hidden shadow-lg animate-pulse">
<div className="h-48 bg-gray-300"></div>
<div className="px-6 py-4 space-y-3">
<div className="h-6 bg-gray-300 rounded w-3/4"></div>
<div className="h-4 bg-gray-300 rounded"></div>
<div className="h-4 bg-gray-300 rounded w-5/6"></div>
</div>
</div>
)
}
Behavior
- Automatically shown during navigation
- Wraps the entire route
- No need to add Suspense manually
- Think of it as global Suspense fallback per route
Data Fetching Caching
You can achieve request-level caching using React.cache API:
Example: Declare a Cached data function
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
const res = await fetch(`https://api.com/user/${id}`)
return res.json()
})
Then use this cached function in a server component:
export default async function Page() {
const user = await getUser('1')
return <div>{user.name}</div>
}
Here if multiple calls happen to the same cached function, it will return the same data. Works across components, so it won’t work outside components. However there are difference between cache and react memo.
For cache sharing in Client components, you can combine context+cache+suspense:
Provider (Server)
<UserProvider userPromise={getUser(id)}>
{children}
</UserProvider>
Client
'use client'
import { use, useContext } from 'react'
const user = use(userPromise)
This allows:
- Server fetch
- Client consumption
- Shared cache


