Le relais client-serveur avec SWR

|

Afficher des données fraîches le plus rapidement possible est crucial si vous souhaitez créer des sites web agréables. Pour un entrepreneur, cela fait aussi la différence entre générer des revenus ou non.

Regardez ce Call To Action. Il affiche le nombre de clients ayant précommandé le dernier produit tendance de votre site web e-commerce. Si vous avez travaillé dans le domaine du marketing web, vous savez à quel point ces CTA peuvent être percutants.

Capture d'écran du formulaire de précommande

Cependant, les composants affichant des données dynamiques, comme ce décompte de précommandes, ne sont pas faciles à implémenter dans Next.js.

Dans Next, il existe deux endroits où vous pouvez récupérer des données : le serveur et le navigateur de l'utilisateur, également appelé le client. Choisir le bon endroit pour récupérer les données peut être difficile. Mais que diriez-vous si je vous disais que vous n'avez pas toujours à choisir ?

Prend la pilule rouge ET la pilule bleue

Cet article décrit comment mettre en œuvre la technique du "relais client-serveur", un pattern architectural pour Next.js et assimilés qui vous permet de tirer parti des avantages du rendu côté serveur (SEO, réduction du TTFB) et du rendu côté client (interactivité, mises à jour en temps réel), en même temps.

Ce n'est pas seulement pour l'e-commerce, le relais client-serveur est également un pattern utile pour les systèmes de vote de blogs, les interfaces de chat, les tableaux de bord d'administration et d'autres cas d'utilisation similaires.

Prérequis

Cet article suppose que vous ayez une familiarité minimale avec Next.js et React, ou un framework comparable offrant des capacités de rendu côté serveur et côté client : Nuxt, Svelte, Qwik, Angular, etc.

D'abord, le client

Commençons par la récupération de données côté client. Si vous êtes un développeur React, vous allez me dire :

C'est super facile ! Je sais comment récupérer des données dans React côté client. J'ai juste besoin de fetch, useState et d'un effet. Je vais passer à la section suivante sur le rendu côté serveur." - Vous

Nous sommes tous passés par là. Mais êtes-vous sûr d'être au courant de la nouvelle documentation React, qui a récemment été réécrite à partir de zéro ?

Voici deux points majeurs à retenir de cette nouvelle documentation :

  • Vous devriez probablement utiliser une librairie (SWR, react-query...). La récupération de données est particulièrement fastidieuse à configurer car vous devez prendre en compte de nombreux petits détails : état de chargement, annulation, mise en cache, invalidation du cache (vous savez, l'une des deux choses difficiles en informatique d'après Phil Karlton), impact sur les performances de React...
  • Si vous n'utilisez pas de bibliothèque, vous devez mettre en place une logique de nettoyage appropriée. Le hook "useData" présenté dans la documentation montre une implémentation correcte, écrite par l'équipe principale de React.

Dans cet article, nous choisirons la première voie et nous appuierons sur SWR, une bibliothèque de récupération de données de Vercel.

Voici comment vous pouvez obtenir des données avec SWR. Cela se fait en deux étapes : tout d'abord, nous définissons une fonction "fetcher" générique qui déclenche la requête HTTP.

/**
 * Le fetcher gère la requête HTTP
 * 
 * SWR s'occupe du reste : 
 * mise en cache, chargement et erreur, polling...
 */
async function jsonFetch(url: string) {
  const res = await fetch(url, {
    headers: {
      Accept: "application/json"
    }
  })
  const data = await res.json()
  return data
}

Ensuite, nous utilisons ce fetcher au sein du hook useSWR :

export function PreorderCta() {  
    const { data /*, error, isLoading*/ } = useSWR<{ count: number }>(
        '/api/preorder/count',
        jsonFetch)
    return <p>{data.count} users preordered the product</p>
}

L'utilisation est assez intuitive, vous recevez vos données ainsi qu'un indicateur de chargement et éventuellement des erreurs.

Pour l'instant, cela semble assez simple. Utilisons une fonctionnalité plus avancée de SWR pour comprendre immédiatement l'avantage d'avoir choisi une bibliothèque plutôt que d'implémenter notre propre logique.

Cette fonctionnalité avancée est le polling ("sondage"). Dans SWR, vous pouvez utiliser le paramètre refreshInterval pour indiquer au hook d'envoyer une nouvelle requête toutes les quelques secondes.

Ainsi, si un autre client précommande le produit, tous les autres utilisateurs obtiendront automatiquement une valeur actualisée sans avoir à recharger l'ensemble de la page.

// app/preorder/client-components.tsx
"use client"
export function PreorderCta() {  
    const { data /*, error, isLoading*/ } = useSWR<{ count: number }>(
        '/api/preorder/count',
        jsonFetch,
        {
            // 🎉 le nombre de précommandes est rechargé toutes les 2 secondes
            refreshInterval: 2000        
        })
    return <p>{data.count} users preordered the product</p>
}

Vous pouvez observer le polling en action en ouvrant l'onglet réseau de votre navigateur préféré :

Ensuite, le serveur

Le problème avec la récupération de données côté client est qu'elle est réalisée en cascade. Lorsque l'utilisateur accède à la page, son navigateur télécharge le HTML de la page, qui à son tour fait référence à un certain code JavaScript, qui est interprété, puis déclenche finalement la requête vers votre API pour obtenir les décompte des précommandes.

Quelques décennies plus tard, votre application recevra la réponse et remplacera le loader par le nombre de précommandes réel.

Mouais.

Le but du rendu côté serveur est de servir une version HTML du site avec les données déjà affichées.

L'utilisateur devra toujours attendre quelques centaines de millisecondes pour que la page devienne interactive avec JavaScript, mais au moins il pourra déjà voir certaines données intéressantes plutôt qu'un tas de spinners de chargement.

Dans Next.js avec le App Router, la récupération de données côté serveur est la méthode par défaut pour obtenir des données. La méthode la plus simple consiste à rendre la page de précommande un composant React Server asynchrone, et à récupérer les données à partir de là.

Cela peut sembler effrayant dit comme ça, mais regardez le code, il est en fait assez court :

// app/preorder/page.tsx
import { cache } from "react"
// On suppose que la fonction "countPreorders"
// récupère le nombre de précommandes
// depuis une base de données ou une API
// On peut réutiliser le code du point d'entrée
// "api/preorder/count"
import { countPreorders } from "your-db"

// cache permet la déduplication
// si on affiche le décompte plusieurs fois par page
const rscCountPreorders = cache(countPreorders)

export default async function PreorderPage() {
    const preordersCount = await rscCountPreorders()
    return <PreorderCta initialCount={preordersCount} />
}

Notre composant PreorderCta reçoit maintenant directement le nombre de précommandes de son parent, qui est un composant React Server. Cette valeur peut être affichée avant même que le code JavaScript pour cette page ne soit téléchargé.

Cover photo from Mariam Antadze on Pexels

Enfin, le relais

Nous avons deux décomptes de précommande, l'un récupéré à partir du serveur et l'autre récupéré côté client. Chacun a ses avantages et ses inconvénients.

La valeur côté serveur est immédiatement disponible, mais ne peut être mise à jour qu'en actualisant l'ensemble de la page.

La valeur côté client peut être mise à jour automatiquement toutes les 2 secondes, mais elle n'est disponible qu'après que l'application React soit devenue interactive, un processus appelé hydratation.

$On peut choisir de rendre la page de précommande dynamique, ce qui signifie que le décompte est récupéré à chaque fois que l'utilisateur actualise la page. Cependant, l'approche la plus efficace consiste à en faire une page statique et à utiliser la revalidation pour la mettre à jour chaque fois qu'un client précommande réellement le produit. Vous pouvez également utiliser le Prérendu Partiel (PPR) pour limiter le rendu dynamique au composant CTA plutôt qu'à l'ensemble de la page.

L'idée d'un Relais Client-Serveur est d'utiliser la valeur côté serveur comme valeur initiale affichée, puis de la remplacer par la valeur côté client lorsqu'elle est prête. Le schéma ci-dessous illustre ce modèle dans Next.js :

Client-server relaying: the client gets initial data from a static, server render, and then the value is updated client-side using JavaScript

Heureusement, SWR dispose d'une option très intuitive qui nous permet de réconcilier les mondes serveur et client et de configurer un relais client-serveur : fallback.

// app/preorder/client-components.tsx
"use client"
export function PreorderCta({initialCount}) {  
    const { data /*, error, isLoading*/ } = useSWR<{ count: number }>(
        '/api/preorder/count',
        jsonFetch,
        {
            // 🎉 on actualise le décompte toutes les 2 secondes avec le polling
            refreshInterval: 2000,
            // 🎉 on utilise la valeur SSR pour afficher
            // immédiatement une première valeur
            fallback: { count: initialCount }       
        })
    return <p>{data.count} users preordered the product</p>
}

💡 La documentation de SWR appelle ce modèle "pré-rendu avec données par défaut". J'ai intitulé mon article "relais client-serveur" car c'est plus court, courir est bon pour la santé, et je l'appelais déjà ainsi aux débuts du SSR avec React.

Conclusion : Next.js, c'est l'hybridation avant tout

Le relais client-serveur est un modèle qui hybride la récupération et le rendu des données côté client et côté serveur pour des performances optimales. Mais ce n'est pas le seul modèle hybride dans Next.js. Par exemple, le Prérendu Partiel (PPR) est une fonctionnalité expérimentale qui vous permet de mélanger le rendu statique et dynamique côté serveur.

Next.js propose de nombreuses approches différentes pour atteindre le même but précisément pour cette raison : la combinaison de différentes techniques vous permet de concevoir des architectures super efficaces, qui bénéficient des avantages de chaque technique sans subir leurs limitations.

💡 Cet article est extrait de mon cours Next.js "Blazing Fast Next.js with React Server Components" publié sur Newline.co. Si vous avez aimé le "relais client-serveur", rejoignez le cours pour découvrir des modèles Next.js avancés similaires pour des performances optimales. Le cours montre également comment gérer correctement les mutations avec les Server Actions et la "revalidation", lorsqu'un client soumet le formulaire pour précommander votre produit.

Course cover

🇬🇧 Speak English? This article is a translation of my blog post "How to setup Client-Server Relaying in Next.js"

Resources

https://swr.vercel.app/docs/revalidation

https://swr.vercel.app/docs/with-nextjs#pre-rendering-with-default-data

https://react.dev/learn/you-might-not-need-an-effect

https://react.dev/reference/react/useEffect#fetching-data-with-effects