Je travaille avec les React Server Components et les Server Actions dans Next.js depuis 6 mois comme développeur react freelance. Bien que je sois enthousiasmé par les composants serveur et leur capacité à exécuter du code sur le serveur, je ne suis pas (encore) convaincu par l'histoire de la récupération de données dans les composants client (sans actions serveur).

Comme j'ai vu la question (« Puis-je récupérer des données avec des actions serveur ? ») revenir plusieurs fois parmi les développeurs à différentes occasions, je veux partager quelques idées ici.

React Server Components

Dans un composant serveur React, la récupération des données est simple. Comme ces composants vous permettent d'exécuter du code sur le serveur, vous pouvez accéder directement à la couche de données (par exemple, la base de données) à partir de l'interface utilisateur :

import { getPosts } from "@/data";
import { Posts } from "@/posts";

const Page = async () => {
  const posts = await getPosts();

  return (
    <div>
      <h1>React Server Component</h1>

      <Posts posts={posts} />
    </div>
  );
};

export default Page;

La fonction getPosts imite ici une requête de base de données avec un délai artificiel. En fin de compte, getPosts pourrait également implémenter une requête à une API tierce ou à une autre source de données :

import { POSTS } from "./db";
import { Post } from "./types";

export const getPosts = async (): Promise<Post[]> => {
  // artificial delay
  await new Promise((resolve) => setTimeout(resolve, 2000));

  return POSTS;
};

Vous pouvez également étendre le composant serveur précédent avec une limite de suspension pour activer la diffusion en continu et un état de chargement. De cette manière, l'utilisateur peut voir chaque élément de contenu dès qu'il est disponible et reçoit entre-temps un indicateur de chargement pour le contenu restant :

import { getPosts } from "@/data";
import { Posts } from "@/posts";
import { Suspense } from "react";

const MyPosts = async () => {
  const posts = await getPosts();

  return <Posts posts={posts} />;
};

const Page = () => {
  return (
    <div>
      <h1>React Server Component</h1>

      <Suspense fallback={<div>Loading...</div>}>
        <MyPosts />
      </Suspense>
    </div>
  );
};

export default Page;

Mais qu'en est-il de la récupération des données dans les composants clients ? La plupart du temps, quelqu'un vous recommande de récupérer les données dans les composants serveur et de les transmettre aux composants client. Et c'est généralement le meilleur conseil que l'on puisse donner.

Mais il y a des cas où vous voulez récupérer des données dans les composants clients. Par exemple, lorsque vous souhaitez récupérer des données lors d'un clic sur un bouton (par exemple, « Load More ») ou lors d'un événement de défilement (par exemple, défilement infini). Dans ce cas, vous pouvez certainement transmettre le sous-ensemble initial de données du composant serveur au composant client, mais il se peut que vous souhaitiez récupérer plus de données dans le composant client par la suite.

Route Handlers pour le chargement des donnés

La recommandation officielle de Next.js est d'utiliser des Route Handlers pour la récupération de données dans les composants clients, parce que les Route Handlers vous permettent d'implémenter une API dans votre application Next.js. Par exemple, dans un fichier app/api/posts/route.ts vous pourriez implémenter un Route Handler pour récupérer des posts via une requête GET :

import { getPosts } from "@/data";

export async function GET() {
  const posts = await getPosts();

  return Response.json(posts);
}

Vous pouvez ensuite récupérer les données dans votre composant client à l'aide d'une requête fetch. Nous utilisons la bibliothèque de récupération de données la plus populaire, react-query, et nous omettons sa configuration pour simplifier les choses. Ce qui suit montre seulement comment récupérer les données dans un composant client :

"use client";

import { Posts } from "@/posts";
import { useQuery } from "@tanstack/react-query";

const fetchPosts = async () => {
  const response = await fetch("/api/posts");
  return await response.json();
};

const Page = () => {
  const { data, isLoading } = useQuery({
    queryKey: ["posts-route-handler"],
    queryFn: fetchPosts,
  });

  return (
    <div>
      <h1>Route Handler</h1>

      {isLoading
        ? <div>Loading...</div>
        : <Posts posts={data ?? []} />
      }
    </div>
  );
};

export default Page;

Pour les utilisateurs finaux, cette mise en œuvre avec un composant client sera identique à la mise en œuvre du composant serveur, car ils verront un indicateur de chargement jusqu'à ce que les données soient récupérées, puis les messages seront affichés.

Cette approche présente deux inconvénients :

  • Nous ne pouvons pas réutiliser la fonction getPosts du composant serveur dans le composant client. Nous devons implémenter un Route Handler (en tant que proxy) et appeler ce Route Handler avec une requête HTTP de récupération (comme nous le ferions avec n'importe quelle autre API distante).
    En d'autres termes : Dans un scénario où nous voudrions récupérer un ensemble initial de messages dans un composant serveur et ensuite récupérer d'autres messages dans un composant client, nous aurions deux implémentations différentes de récupération de données au lieu de simplement réutiliser la même fonction getPosts.
  • Les données renvoyées par le gestionnaire de route ne sont pas sûres (ici : data : any). Nous devrions les saisir manuellement nous-mêmes ou faire appel à des tiers comme OpenAPI.
    Voyons comment cela peut fonctionner avec les actions serveur.

Voyons comment cela peut fonctionner avec les actions serveur.

Server Actions pour la récupération des données

La documentation officielle de Next.js indique que les actions serveur ne servent qu'à écrire (modifier) et non à lire (interroger) des données. Mais faisons-le quand même pour récupérer des données dans un composant client avec des actions serveur pour le bien de cet argument :

"use client";

import { getPosts } from "@/data";
import { Posts } from "@/posts";
import { useQuery } from "@tanstack/react-query";

const fetchPosts = async () => {
  return await getPosts();
};

const Page = () => {
  const { data, isLoading } = useQuery({
    queryKey: ["posts-server-action"],
    queryFn: fetchPosts,
  });

  return (
    <div>
      <h1>Server Action</h1>

      {isLoading
        ? <div>Loading...</div>
        : <Posts posts={data ?? []} />
      }
    </div>
  );
};

export default Page;

L'expérience utilisateur (UX) sera la même qu'avec le Route Handler. Mais l'expérience du développeur (DX) est améliorée ici :

  • Tout d'abord, nous pouvons réutiliser la fonction getPosts du composant serveur dans le composant client. Nous n'avons pas besoin d'implémenter un gestionnaire de route et nous n'avons pas besoin d'appeler ce gestionnaire de route avec une requête HTTP fetch. Du point de vue de la DX, nous effectuons un appel de procédure à distance (RPC) typé et non une requête HTTP non typée.
  • Deuxièmement, les données renvoyées par getPosts sont typées dès le départ, car nous appelons simplement une fonction typée et non un point de terminaison d'API vaguement typé.

La seule chose que nous devrions changer est de marquer la fonction getPosts précédente comme une action serveur, si elle accède réellement à des ressources côté serveur (par exemple, une base de données) :

"use server";

Et pour être honnête, voici comment j'ai procédé au cours des derniers mois.

Stratégie : J'ai commencé par implémenter des fonctions de requête découplées (c'est-à-dire : /queries/get-posts.ts) de mes composants serveur. Chaque fois que je devais réutiliser ces fonctions de requête dans des composants client, je les marquais simplement comme des actions serveur et je déplaçais le fichier du dossier /queries vers un dossier /actions, c'est-à-dire /actions/get-posts.ts.

Les avantages en bref : De cette façon, je n'ai pas eu à implémenter un Route Handler, j'ai pu réutiliser la fonction de requête dans mon composant client, et j'ai eu une expérience de récupération de données typées en tant que développeur.

Mais je sais que ce n'est pas la recommandation officielle. La récupération de données à l'aide d'actions serveur présente également des inconvénients :

  • Les actions serveur effectuent des requêtes HTTP POST
  • Les actions serveur s'exécutent en séquence

Jusqu'à présent, je n'ai pas eu de problèmes avec ces restrictions, car l'utilisation d'une action serveur pour la récupération de données dans un composant client ne se produit qu'occasionnellement. Toutefois, je suis curieux de voir comment la recommandation officielle évoluera à l'avenir.

Mais je sais que ce n'est pas la fin de l'histoire. Il y a une raison pour laquelle les équipes React et Next ne recommandent pas les Server Actions pour la récupération de données. À l'avenir, il y aura de meilleures façons de récupérer des données dans les composants client, mais elles ne sont pas encore là.

Par exemple, je sais que l'utilisation de React sera le prochain changement pour la récupération de données dans les composants client. Peut-être que cela sera suivi par les composants clients asynchrones. Et qui sait comment nous utiliserons les composants client.