Creating Meaningful Loading States in Next.js – Streaming, Preloading, and Parallel Data Fetching

Next.js enables developers to design meaningful loading states using Suspense boundaries, parallel data fetching, and smart preloading. This article explores how to show instant fallback UI, avoid blocking renders, and use techniques like Promise.all and preload to improve user experience and responsiveness.

loading stateparallel data fetchingSuspensepreload

~3 min read • Updated Oct 25, 2025

Introduction


An instant loading state is fallback UI shown immediately after navigation. For a better user experience, use meaningful placeholders like skeletons, spinners, or partial previews (e.g., cover photo, title) to signal responsiveness.


Sequential Data Fetching


In sequential fetching, nested components each fetch their own data independently, which can delay rendering. This pattern is useful when one component depends on the result of another.


For example, <Playlists> waits for <Artist> to finish before fetching:


// app/artist/[username]/page.tsx
export default async function Page({ params }) {
  const { username } = await params
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

async function Playlists({ artistID }) {
  const playlists = await getArtistPlaylists(artistID)
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

Parallel Data Fetching


Parallel fetching starts multiple requests simultaneously. In Next.js, layouts and pages are rendered in parallel by default, but inside a component, sequential await calls can still block rendering.


Use Promise.all to fetch data concurrently:


// app/artist/[username]/page.tsx
export default async function Page({ params }) {
  const { username } = await params

  const artistData = getArtist(username)
  const albumsData = getAlbums(username)

  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  )
}

Note: If one request fails in Promise.all, the entire operation fails. Use Promise.allSettled for safer handling.


Preloading Data


Preload data by calling a utility function before blocking requests. This ensures data is ready when the component renders:


// app/item/[id]/page.tsx
export default async function Page({ params }) {
  const { id } = await params
  preload(id)
  const isAvailable = await checkIsAvailable()
  return isAvailable ? <Item id={id} /> : null
}

export const preload = (id) => {
  void getItem(id)
}

export async function Item({ id }) {
  const result = await getItem(id)
  // ...
}

Caching Fetch Functions


Use React’s cache and the server-only package to cache server-side fetch functions:


// utils/get-item.ts
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

export const preload = (id) => {
  void getItem(id)
}

export const getItem = cache(async (id) => {
  // ...
})

Conclusion


By designing meaningful loading states, using parallel data fetching, and preloading smartly, you can deliver fast, responsive experiences in Next.js. Suspense boundaries and server-side caching help optimize rendering and reduce wait times for users.


Written & researched by Dr. Shahin Siami