Modèles de récupération de données

Il existe quelques modèles recommandés et bonnes pratiques pour récupérer des données dans React et Next.js. Cette page présente certains des modèles les plus courants et comment les utiliser.

Récupération de données côté serveur

Dans la mesure du possible, nous recommandons de récupérer les données côté serveur. Cela vous permet de :

  • Avoir un accès direct aux ressources de données backend (par exemple, les bases de données).
  • Maintenir votre application plus sécurisée en évitant que des informations sensibles, comme les jetons d'accès et les clés API, ne soient exposées au client.
  • Récupérer les données et les rendre dans le même environnement. Cela réduit à la fois les allers-retours entre le client et le serveur, ainsi que le travail sur le thread principal côté client.
  • Effectuer plusieurs récupérations de données en un seul aller-retour au lieu de multiples requêtes individuelles côté client.
  • Réduire les cascades client-serveur.
  • Selon votre région, la récupération de données peut également se produire plus près de votre source de données, réduisant la latence et améliorant les performances.

Vous pouvez récupérer des données côté serveur en utilisant les Composants Serveur, les Gestionnaires de Route et les Actions Serveur.

Récupération de données là où elles sont nécessaires

Si vous avez besoin d'utiliser les mêmes données (par exemple, l'utilisateur actuel) dans plusieurs composants d'une arborescence, vous n'avez pas besoin de récupérer les données globalement, ni de transmettre des props entre les composants. À la place, vous pouvez utiliser fetch ou cache de React dans le composant qui a besoin des données sans vous soucier des implications sur les performances liées à la réalisation de multiples requêtes pour les mêmes données.

Cela est possible car les requêtes fetch sont automatiquement mémorisées. Apprenez-en plus sur la mémorisation des requêtes

Bon à savoir : Cela s'applique également aux layouts, car il n'est pas possible de transmettre des données entre un layout parent et ses enfants.

Streaming

Le streaming et Suspense sont des fonctionnalités de React qui vous permettent de rendre progressivement et de diffuser par incréments des unités rendues de l'interface utilisateur vers le client.

Avec les Composants Serveur et les layouts imbriqués, vous pouvez rendre instantanément les parties de la page qui ne nécessitent pas spécifiquement de données, et afficher un état de chargement pour les parties de la page qui récupèrent des données. Cela signifie que l'utilisateur n'a pas besoin d'attendre que toute la page soit chargée avant de pouvoir commencer à interagir avec elle.

Rendu côté serveur avec streaming

Pour en savoir plus sur le streaming et Suspense, consultez les pages Interface de chargement et Streaming et Suspense.

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

Lors de la récupération de données dans des composants React, vous devez être conscient de deux modèles de récupération de données : Parallèle et Séquentiel.

Récupération de données séquentielle et parallèle
  • Avec la récupération de données séquentielle, les requêtes dans une route dépendent les unes des autres et créent donc des cascades. Il peut y avoir des cas où vous voulez ce modèle car une récupération dépend du résultat de l'autre, ou parce que vous voulez qu'une condition soit satisfaite avant la prochaine récupération pour économiser des ressources. Cependant, ce comportement peut aussi être involontaire et entraîner des temps de chargement plus longs.
  • Avec la récupération de données parallèle, les requêtes dans une route sont initiées avec empressement et chargeront les données en même temps. Cela réduit les cascades client-serveur et le temps total nécessaire pour charger les données.

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

Si vous avez des composants imbriqués et que chaque composant récupère ses propres données, alors la récupération de données se fera séquentiellement si ces requêtes de données sont différentes (cela ne s'applique pas aux requêtes pour les mêmes données car elles sont automatiquement mémorisées).

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

// ...

async function Playlists({ artistID }: { artistID: string }) {
  // Attente des playlists
  const playlists = await getArtistPlaylists(artistID)

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

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Attente de l'artiste
  const artist = await getArtist(username)

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

async function Playlists({ artistID }) {
  // Attente des playlists
  const playlists = await getArtistPlaylists(artistID)

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

export default async function Page({ params: { username } }) {
  // Attente de l'artiste
  const artist = await getArtist(username)

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

Dans des cas comme celui-ci, vous pouvez utiliser loading.js (pour les segments de route) ou React <Suspense> (pour les composants imbriqués) pour afficher un état de chargement instantané pendant que React diffuse le résultat.

Cela empêchera que toute la route soit bloquée par la récupération de données, et l'utilisateur pourra interagir avec les parties de la page qui ne sont pas bloquées.

Requêtes de données bloquantes :

Une approche alternative pour éviter les cascades consiste à récupérer les données globalement, à la racine de votre application, mais cela bloquera le rendu pour tous les segments de route en dessous jusqu'à ce que les données aient fini de charger. Cela peut être décrit comme une récupération de données "tout ou rien". Soit vous avez toutes les données pour votre page ou application, soit aucune.

Toute requête fetch avec await bloquera le rendu et la récupération de données pour toute l'arborescence en dessous, à moins qu'elle ne soit enveloppée dans une limite <Suspense> ou que loading.js soit utilisé. Une autre alternative consiste à utiliser la récupération de données parallèle ou le modèle de préchargement.

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

Pour récupérer des données en parallèle, vous pouvez initier les requêtes avec empressement en les définissant en dehors des composants qui utilisent les données, puis en les appelant depuis l'intérieur du composant. Cela gagne du temps en initiant les deux requêtes en parallèle, cependant, l'utilisateur ne verra pas le résultat rendu avant que les deux promesses ne soient résolues.

Dans l'exemple ci-dessous, les fonctions getArtist et getArtistAlbums sont définies en dehors du composant Page, puis appelées à l'intérieur du composant, et nous attendons que les deux promesses se résolvent :

import Albums from './albums'

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

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

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Initiation des deux requêtes en parallèle
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Attente de la résolution des promesses
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

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

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

export default async function Page({ params: { username } }) {
  // Initiation des deux requêtes en parallèle
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Attente de la résolution des promesses
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

Pour améliorer l'expérience utilisateur, vous pouvez ajouter une Limite Suspense pour diviser le travail de rendu et afficher une partie du résultat dès que possible.

Préchargement de données

Une autre façon d'éviter les cascades est d'utiliser le modèle de préchargement. Vous pouvez éventuellement créer une fonction preload pour optimiser davantage la récupération de données parallèle. Avec cette approche, vous n'avez pas besoin de transmettre des promesses comme props. La fonction preload peut aussi avoir n'importe quel nom car c'est un modèle, pas une API.

import { getItem } from '@/utils/get-item'

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 default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
import { getItem } from '@/utils/get-item'

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

export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // début du chargement des données de l'item
  preload(id)
  // exécution d'une autre tâche asynchrone
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({ params: { id } }) {
  // début du chargement des données de l'item
  preload(id)
  // exécution d'une autre tâche asynchrone
  const isAvailable = await checkIsAvailable()

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

Utilisation de cache de React, server-only et le modèle de préchargement

Vous pouvez combiner la fonction cache, le modèle preload et le package server-only pour créer un utilitaire de récupération de données qui peut être utilisé dans toute votre application.

import { cache } from 'react'
import 'server-only'

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

export const getItem = cache(async (id: string) => {
  // ...
})
import { cache } from 'react'
import 'server-only'

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

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

Avec cette approche, vous pouvez récupérer des données avec empressement, mettre en cache les réponses et garantir que cette récupération de données ne se produit que côté serveur.

Les exports de utils/get-item peuvent être utilisés par les Layouts, Pages ou autres composants pour leur donner le contrôle du moment où les données d'un item sont récupérées.

Bon à savoir :

  • Nous recommandons d'utiliser le package server-only pour s'assurer que les fonctions de récupération de données côté serveur ne sont jamais utilisées côté client.