Comment récupérer des données et les streamer

Cette page vous guidera sur la manière de récupérer des données dans les composants Serveur et Client, et comment streamer les composants qui dépendent de données.

Récupération de données

Composants Serveur

Vous pouvez récupérer des données dans les composants Serveur en utilisant :

  1. L'API fetch
  2. Un ORM ou une base de données

Avec l'API fetch

Pour récupérer des données avec l'API fetch, transformez votre composant en une fonction asynchrone et utilisez await sur l'appel à fetch. Par exemple :

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Bon à savoir :

  • Les réponses de fetch ne sont pas mises en cache par défaut. Cependant, Next.js prérendra la route et le résultat sera mis en cache pour améliorer les performances. Si vous souhaitez opter pour un rendu dynamique, utilisez l'option { cache: 'no-store' }. Consultez la référence de l'API fetch.
  • Pendant le développement, vous pouvez logger les appels à fetch pour une meilleure visibilité et un meilleur débogage. Consultez la référence de l'API logging.

Avec un ORM ou une base de données

Comme les composants Serveur sont rendus côté serveur, vous pouvez interroger une base de données en toute sécurité en utilisant un ORM ou un client de base de données. Transformez votre composant en une fonction asynchrone et utilisez await sur l'appel :

import { db, posts } from '@/lib/db'

export default async function Page() {
  const allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Composants Client

Il existe deux façons de récupérer des données dans les composants Client, en utilisant :

  1. Le hook use de React
  2. Une bibliothèque communautaire comme SWR ou React Query

Streamer des données avec le hook use

Vous pouvez utiliser le hook use de React pour streamer des données du serveur vers le client. Commencez par récupérer des données dans votre composant Serveur, puis passez la promesse à votre composant Client en tant que prop :

import Posts from '@/app/ui/posts
import { Suspense } from 'react'

export default function Page() {
  // Ne pas utiliser await sur la fonction de récupération de données
  const posts = getPosts()

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}

Ensuite, dans votre composant Client, utilisez le hook use pour lire la promesse :

'use client'
import { use } from 'react'

export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)

  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Dans l'exemple ci-dessus, le composant <Posts> est enveloppé dans une limite <Suspense>. Cela signifie que le fallback sera affiché pendant que la promesse est en cours de résolution. Apprenez-en plus sur le streaming.

Bibliothèques communautaires

Vous pouvez utiliser une bibliothèque communautaire comme SWR ou React Query pour récupérer des données dans les composants Client. Ces bibliothèques ont leurs propres sémantiques pour la mise en cache, le streaming et d'autres fonctionnalités. Par exemple, avec SWR :

'use client'
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((r) => r.json())

export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    'https://api.vercel.app/blog',
    fetcher
  )

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Déduplication des requêtes avec React.cache

La déduplication est le processus de prévention des requêtes en double pour la même ressource pendant un rendu. Elle vous permet de récupérer les mêmes données dans différents composants tout en évitant plusieurs requêtes réseau vers votre source de données.

Si vous utilisez fetch, les requêtes peuvent être dédupliquées en ajoutant cache: 'force-cache'. Cela signifie que vous pouvez appeler la même URL avec les mêmes options en toute sécurité, et une seule requête sera effectuée.

Si vous n'utilisez pas fetch, mais plutôt un ORM ou une base de données directement, vous pouvez envelopper votre récupération de données avec la fonction React cache.

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'

export const getPost = cache(async (id: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})

Streaming

Avertissement : Le contenu ci-dessous suppose que l'option de configuration dynamicIO est activée dans votre application. Ce drapeau a été introduit dans Next.js 15 canary.

Lorsque vous utilisez async/await dans les composants Serveur, Next.js optera pour un rendu dynamique. Cela signifie que les données seront récupérées et rendues côté serveur pour chaque requête utilisateur. S'il y a des requêtes de données lentes, toute la route sera bloquée pour le rendu.

Pour améliorer le temps de chargement initial et l'expérience utilisateur, vous pouvez utiliser le streaming pour diviser le HTML de la page en petits morceaux et les envoyer progressivement du serveur vers le client.

Fonctionnement du rendu côté serveur avec streaming

Il existe deux façons d'implémenter le streaming dans votre application :

  1. En enveloppant une page avec un fichier loading.js
  2. En enveloppant un composant avec <Suspense>

Avec loading.js

Vous pouvez créer un fichier loading.js dans le même dossier que votre page pour streamer toute la page pendant que les données sont en cours de récupération. Par exemple, pour streamer app/blog/page.js, ajoutez le fichier dans le dossier app/blog.

Structure du dossier Blog avec fichier loading.js
export default function Loading() {
  // Définissez l'interface utilisateur de chargement ici
  return <div>Loading...</div>
}

Lors de la navigation, l'utilisateur verra immédiatement la mise en page et un état de chargement pendant que la page est en cours de rendu. Le nouveau contenu sera automatiquement remplacé une fois le rendu terminé.

Interface utilisateur de chargement

En arrière-plan, loading.js sera imbriqué dans layout.js et enveloppera automatiquement le fichier page.js et tous les enfants en dessous dans une limite <Suspense>.

Aperçu de loading.js

Cette approche fonctionne bien pour les segments de route (layouts et pages), mais pour un streaming plus granulaire, vous pouvez utiliser <Suspense>.

Avec <Suspense>

<Suspense> vous permet d'être plus précis sur les parties de la page à streamer. Par exemple, vous pouvez immédiatement afficher tout contenu de page qui se trouve en dehors de la limite <Suspense>, et streamer la liste des articles de blog à l'intérieur de la limite.

import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'

export default function BlogPage() {
  return (
    <div>
      {/* Ce contenu sera envoyé au client immédiatement */}
      <header>
        <h1>Bienvenue sur le Blog</h1>
        <p>Lisez les derniers articles ci-dessous.</p>
      </header>
      <main>
        {/* Tout contenu enveloppé dans une limite <Suspense> sera streamé */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

Création d'états de chargement significatifs

Un état de chargement instantané est une interface utilisateur de fallback qui est affichée immédiatement à l'utilisateur après la navigation. Pour la meilleure expérience utilisateur, nous recommandons de concevoir des états de chargement qui sont significatifs et aident les utilisateurs à comprendre que l'application répond. Par exemple, vous pouvez utiliser des squelettes et des spinners, ou une petite partie significative des écrans futurs comme une photo de couverture, un titre, etc.

En développement, vous pouvez prévisualiser et inspecter l'état de chargement de vos composants en utilisant les React Devtools.

Exemples

Récupération séquentielle de données

La récupération séquentielle de données se produit lorsque des composants imbriqués dans un arbre récupèrent chacun leurs propres données et que les requêtes ne sont pas dédupliquées, ce qui entraîne des temps de réponse plus longs.

Récupération séquentielle et parallèle de données

Il peut y avoir des cas où vous voulez ce modèle parce qu'une récupération dépend du résultat de l'autre.

Par exemple, le composant <Playlists> ne commencera à récupérer des données que lorsque le composant <Artist> aura terminé de récupérer des données, car <Playlists> dépend de la prop artistID :

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // Récupérer les informations de l'artiste
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      {/* Afficher une interface utilisateur de fallback pendant que le composant Playlists charge */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* Passer l'ID de l'artiste au composant Playlists */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

async function Playlists({ artistID }: { artistID: string }) {
  // Utiliser l'ID de l'artiste pour récupérer les playlists
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

Pour améliorer l'expérience utilisateur, vous devez utiliser React <Suspense> pour afficher un fallback pendant que les données sont en cours de récupération. Cela permettra le streaming et empêchera que toute la route soit bloquée par les requêtes de données séquentielles.

Récupération de données en parallèle

La récupération de données en parallèle se produit lorsque les requêtes de données dans une route sont initiées de manière anticipée et démarrent en même temps.

Par défaut, les layouts et les pages sont rendus en parallèle. Ainsi, chaque segment commence à récupérer les données dès que possible.

Cependant, dans n'importe quel composant, plusieurs requêtes async/await peuvent toujours être séquentielles si elles sont placées les unes après les autres. Par exemple, getAlbums sera bloqué jusqu'à ce que getArtist soit résolu :

import { getArtist, getAlbums } from '@/app/lib/data'

export default async function Page({ params }) {
  // Ces requêtes seront séquentielles
  const { username } = await params
  const artist = await getArtist(username)
  const albums = await getAlbums(username)
  return <div>{artist.name}</div>
}

Vous pouvez initier les requêtes en parallèle en les définissant en dehors des composants qui utilisent les données, et en les résolvant ensemble, par exemple avec Promise.all :

import Albums from './albums'

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)

  // Initie les deux requêtes en parallèle
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

Bon à savoir : Si une requête échoue lors de l'utilisation de Promise.all, l'ensemble de l'opération échouera. Pour gérer cela, vous pouvez utiliser la méthode Promise.allSettled à la place.

Préchargement des données

Vous pouvez précharger les données en créant une fonction utilitaire que vous appelez de manière anticipée avant les requêtes bloquantes. <Item> s'affiche conditionnellement en fonction de la fonction checkIsAvailable().

Vous pouvez appeler preload() avant checkIsAvailable() pour initier de manière anticipée les dépendances de données de <Item/>. Au moment où <Item/> est rendu, ses données ont déjà été récupérées.

import { getItem } from '@/lib/data'

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // commence le chargement des données de l'item
  preload(id)
  // effectue une autre tâche asynchrone
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}

export const preload = (id: string) => {
  // void évalue l'expression donnée et retourne undefined
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}

De plus, vous pouvez utiliser la fonction cache de React et le package server-only pour créer une fonction utilitaire réutilisable. Cette approche vous permet de mettre en cache la fonction de récupération de données et de vous assurer qu'elle n'est exécutée que sur le serveur.

import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'

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

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