Frontend Development

Next.js Data Fetching (App Router) — Complete Guide

Next.js Data Fetching (App Router) — Complete Guide

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

 

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted