Composants Serveur
Les Composants Serveur (React Server Components, ou RSC — NdT) sont un nouveau type de Composant qui font un rendu anticipé, avant le bundling, dans un environnement distinct de votre appli client et d’un serveur SSR.
Cet environnement séparé est le « serveur » des Composants Serveur. Les Composants Serveur peuvent n’être exécutés qu’une seule fois au moment du build sur votre serveur de CI, ou peuvent l’être à chaque requête au sein d’un serveur web.
- Composants Serveur… sans serveur
- Composants Serveur… avec un serveur
- Ajouter de l’interactivité aux Composants Serveur
- Composants asynchrones et Composants Serveur
Composants Serveur… sans serveur
Les Composants Serveur peuvent être exécutés au moment du build pour lire des données du système de fichiers ou charger du contenu statique, de sorte qu’un serveur web n’est alors pas nécessaire. Vous pourriez par exemple vouloir lire des données statiques issues d’un système de gestion de contenu (CMS).
Sans les Composants Serveur, on aurait classiquement recours à un chargement des données statiques depuis le client, au sein d’un Effet :
// bundle.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)
function Page({page}) {
const [content, setContent] = useState('');
// NOTE : charge *après* le rendu initial de la page.
useEffect(() => {
fetch(`/api/content/${page}`).then((data) => {
setContent(data.content);
});
}, [page]);
return <div>{sanitizeHtml(marked(content))}</div>;
}
// api.js
app.get(`/api/content/:page`, async (req, res) => {
const page = req.params.page;
const content = await file.readFile(`${page}.md`);
res.send({content});
});
Cette approche implique que les utilisateurs aient besoin de télécharger et parser 75 Ko (compressés) complémentaires de bibliothèques, pour ensuite attendre qu’une seconde requête de chargement aboutisse après le chargement initial de la page, tout ça pour simplement afficher du contenu statique qui ne changera plus une fois la page affichée.
Avec les Composants Serveur, vous pouvez faire le rendu de ces composants une seule fois, lors du build :
import marked from 'marked'; // Non inclus dans le bundle
import sanitizeHtml from 'sanitize-html'; // Non inclus dans le bundle
async function Page({page}) {
// NOTE : charge *pendant* le rendu, durant le build de l'appli.
const content = await file.readFile(`${page}.md`);
return <div>{sanitizeHtml(marked(content))}</div>;
}
Le résultat peut alors être rendu côté serveur (SSR) en HTML et téléversé sur un CDN. Lorsque l’appli charge, le client ne verra pas le composant Page
d’origine, ni les lourdes bibliothèques nécessaires au rendu du Markdown. Le client ne verra que le résultat final :
<div><!-- HTML issu du Markdown --></div>
Ça signifie que le contenu est visible dès le chargement initial de la page, et que le bundle n’inclut pas les lourdes bibliothèques nécessaires à la production du contenu statique.
Composants Serveur… avec un serveur
Les Composants Serveur peuvent aussi être exécutés dans un serveur web lors du traitement d’une requête pour une page, ce qui vous permet d’accéder à votre couche de données sans avoir besoin de construire une API. Le rendu est fait avant le bundling de l’appli, et peut passer des données et du JSX à des Composants Client.
Sans Composants Serveur, on a généralement recours à un Effet pour charger des données côté client :
// bundle.js
function Note({id}) {
const [note, setNote] = useState('');
// NOTE : charge *après* le rendu initial de la page.
useEffect(() => {
fetch(`/api/notes/${id}`).then(data => {
setNote(data.note);
});
}, [id]);
return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}
function Author({id}) {
const [author, setAuthor] = useState('');
// NOTE : charge *après* le rendu de `Note`.
// Ça entraîne une cascade client-serveur coûteuse.
useEffect(() => {
fetch(`/api/authors/${id}`).then(data => {
setAuthor(data.author);
});
}, [id]);
return <span>Par : {author.name}</span>;
}
// api
import db from './database';
app.get(`/api/notes/:id`, async (req, res) => {
const note = await db.notes.get(id);
res.send({note});
});
app.get(`/api/authors/:id`, async (req, res) => {
const author = await db.authors.get(id);
res.send({author});
});
Avec les Composants Serveur, vous pouvez lire les données et les afficher au sein du composant :
import db from './database';
async function Note({id}) {
// NOTE : charge *pendant* le rendu.
const note = await db.notes.get(id);
return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}
async function Author({id}) {
// NOTE : charge *après* `Note`, mais c'est rapide si les données sont co-localisées.
const author = await db.authors.get(id);
return <span>Par : {author.name}</span>;
}
Le bundler produit ensuite un bundle à partir des données, des Composants Serveur dont le rendu est donc déjà fait, et des Composants Client dynamiques. Ce bundle peut, optionnellement, faire l’objet d’un rendu côté client (SSR) pour produire le HTML initial de la page. Lorsque la page est chargée, le navigateur ne voit pas les composants Note
et Author
d’origine ; seul le résultat du rendu est envoyé au client :
<div>
<span>Par : L'équipe React</span>
<p>React 19 est...</p>
</div>
Les Composants Serveur peuvent devenir dynamiques en les rechargeant depuis le serveur, au sein duquel ils sont libres d’accéder aux données pour refaire leur rendu. Cette nouvelle architecture d’application combine le modèle mental simple « requête / réponse » des applis multi-page (MPA, Multi-Page Apps), centrées sur le serveur, avec l’interactivité fluide des applis mono-page (SPA, Single-Page Apps), centrées elles sur le client. Vous obtenez ainsi le meilleur des deux mondes.
Ajouter de l’interactivité aux Composants Serveur
Les Composants Serveur ne sont pas envoyés au navigateur, ils ne peuvent donc pas utiliser des API interactives telles que useState
. Pour ajouter de l’interactivité aux Composants Serveur, vous pouvez les composer avec des Composants Client en utilisant la directive "use client"
.
Dans l’exemple qui suit, le Composant Serveur Notes
importe le Composant Client Expandable
, lequel utilise un état pour basculer son statut expanded
:
// Composant Serveur
import Expandable from './Expandable';
async function Notes() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<Expandable key={note.id}>
<p>{note}</p>
</Expandable>
))}
</div>
)
}
// Composant Client
"use client"
export default function Expandable({children}) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button
onClick={() => setExpanded(!expanded)}
>
Basculer
</button>
{expanded && children}
</div>
)
}
Ça fonctionne en faisant d’abord le rendu de Notes
en tant que Composant Serveur, puis en indiquant au bundler de créer un bundle avec le Composant Client Expandable
. Dans le navigateur, ces Composants Client verront le résultat du rendu des Composants Serveur au travers de leurs props :
<head>
<!-- le bundle pour les Composants Client -->
<script src="bundle.js" />
</head>
<body>
<div>
<Expandable key={1}>
<p>Voici la première note</p>
</Expandable>
<Expandable key={2}>
<p>Voici la deuxième note</p>
</Expandable>
<!--...-->
</div>
</body>
Composants asynchrones et Composants Serveur
Les Composants Serveur apportent une nouvelle façon d’écrire les composants en tirant parti de async
/await
. Lorsque vous utilisez await
au sein d’un composant asynchrone, React le suspend et attend l’accomplissement de la promesse pour reprendre son rendu. Ça peut traverser la frontière client/serveur grâce à la prise en charge du streaming par Suspense.
Vous pouvez même créer une promesse sur le serveur, et l’attendre côté client :
// Composant Serveur
import db from './database';
async function Page({id}) {
// Suspendra le Composant Serveur
const note = await db.notes.get(id);
// NOTE : on n’attend pas, on démarre juste. On attendra côté client.
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note}
<Suspense fallback={<p>Chargement des commentaires...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
// Composant Client
"use client";
import {use} from 'react';
function Comments({commentsPromise}) {
// NOTE : on reprend la promesse issue du serveur.
// Le composant suspendra le temps que les données deviennent disponibles.
const comments = use(commentsPromise);
return comments.map(commment => <p>{comment}</p>);
}
Le contenu note
constitue une donnée important pour le rendu de la page, de sorte qu’on l’attend côté serveur. Mais les commentaires sont sous le fold (la limite basse de la fenêtre initiale de visualisation) et sont donc moins prioritaire, de sorte qu’on se contente de démarrer leur chargement côté serveur, pour n’en attendre l’aboutissement que côté client grâce à l’API use
. Ça suspendra côté client, sans bloquer le rendu initial du contenu note
.
Dans la mesure où les composants asynchrones ne sont pas pris en charge côté client, on attend leur promesse avec use
.