Cours JavaScript et React · Module 10: Écosystème et avenir de React

React Server Components (RSC) - l'avenir de React

14 min de lecture
Dans cette leçon6

Le panneau de mission affiche des posts provenant de la base de données et un bouton « J'aime ». Dans une application classique, tout le code des composants, avec les bibliothèques et la logique des requêtes, part dans le navigateur, et tu ne récupères les données qu'après le chargement de la page. Les React Server Components inversent ce partage : une partie des composants s'exécute uniquement sur le serveur, accède directement aux données et n'ajoute pas un seul octet au bundle JavaScript. C'est un changement fondamental dans la façon de penser les applications React.

Que sont les React Server Components ?

Un Server Component se rend sur le serveur, au moment du build ou à chaque requête. Son résultat n'est pas du HTML tout prêt, mais le RSC Payload, une description sérialisée de l'arbre d'interface avec des emplacements pour les composants client. Le cycle d'une requête ressemble à ceci : le client demande une page, les composants serveur récupèrent les données et créent le payload, le client construit l'arbre à partir de celui-ci, et enfin les composants client sont hydratés, c'est-à-dire qu'ils reçoivent leurs événements et leur état. Le HTML peut en plus être produit par le SSR.

Différences entre RSC et SSR

Le SSR classique rend toute l'application en HTML, puis envoie quand même tout son code au navigateur pour l'hydratation. Les RSC permettent de mélanger composants serveur et composants client dans un même arbre :

1// SSR classique (Pages Router) - les données viennent de getServerSideProps
2export async function getServerSideProps() {
3  const posts = await fetchPosts(); // S'exécute sur le serveur à chaque requête
4  return { props: { posts } };
5}
6
7export default function TraditionalSSRPage({ posts }) {
8  const [count, setCount] = useState(0);
9
10  // Ce composant et tous ses imports partent aussi dans le navigateur
11  return (
12    <div>
13      <PostsList posts={posts} />
14      <Counter count={count} setCount={setCount} />
15    </div>
16  );
17}
18
19// React Server Components - un mélange de serveur et de client
20async function ServerComponentPage() {
21  // Ce code s'exécute UNIQUEMENT sur le serveur
22  const posts = await fetchPosts(); // Appel direct à la base de données
23  const user = await getUser();
24
25  return (
26    <div>
27      {/* Server Component - rendu sur le serveur */}
28      <PostsList posts={posts} />
29
30      {/* Client Component - interactif sur le client */}
31      <Counter />
32
33      {/* Server Component avec des données du serveur */}
34      <UserProfile user={user} />
35    </div>
36  );
37}

Dans le SSR classique, les données sont récupérées par une fonction séparée, getServerSideProps, et le composant de page les reçoit dans ses props. Le serveur en génère le HTML, mais le code de TraditionalSSRPage, de PostsList et de toutes leurs bibliothèques part quand même dans le navigateur pour l'hydratation. ServerComponentPage est asynchrone et attend les données elle-même : son code reste sur le serveur, et seul le Counter interactif arrive dans le navigateur.

Les bases des Server Components

Dans l'App Router de Next.js, chaque composant est un composant serveur par défaut. La directive 'use client' tout en haut d'un fichier marque la frontière : ce module et ses imports partent dans le navigateur et peuvent utiliser les hooks, les événements et l'état :

1// ServerComponent.jsx (server component par défaut)
2import { db } from './database';
3
4export default async function PostsList() {
5  // Accès direct à la base de données
6  const posts = await db.posts.findMany({
7    include: { author: true },
8    orderBy: { createdAt: 'desc' }
9  });
10
11  return (
12    <div className="posts-list">
13      {posts.map(post => (
14        <article key={post.id}>
15          <h2>{post.title}</h2>
16          <p>By {post.author.name}</p>
17          <p>{post.excerpt}</p>
18
19          {/* Client Component pour l'interaction */}
20          <LikeButton postId={post.id} initialLikes={post.likes} />
21        </article>
22      ))}
23    </div>
24  );
25}
26
27// LikeButton.jsx (client component)
28'use client'; // Directive qui marque un client component
29
30import { useState } from 'react';
31
32export default function LikeButton({ postId, initialLikes }) {
33  const [likes, setLikes] = useState(initialLikes);
34  const [isLiking, setIsLiking] = useState(false);
35
36  const handleLike = async () => {
37    setIsLiking(true);
38    try {
39      const response = await fetch(`/api/posts/${postId}/like`, {
40        method: 'POST'
41      });
42      const data = await response.json();
43      setLikes(data.likes);
44    } catch (error) {
45      console.error('Failed to like post:', error);
46    } finally {
47      setIsLiking(false);
48    }
49  };
50
51  return (
52    <button
53      onClick={handleLike}
54      disabled={isLiking}
55      className="like-button"
56    >
57      {likes} {isLiking && '...'}
58    </button>
59  );
60}

PostsList interroge directement la base, et LikeButton gère les clics via useState. Le composant serveur rend le composant client comme enfant et lui passe des props sérialisables, comme postId. Dans un projet, ce sont deux fichiers séparés, car la directive ne fonctionne qu'au début d'un fichier.

Récupérer des données dans les Server Components

Un Server Component peut utiliser sans risque des secrets et des requêtes vers la base, car son code n'atteint jamais le navigateur. La fonction cache de React mémorise le résultat pendant une même requête :

1// utils/api.js (server-side only)
2import { cache } from 'react';
3
4// Mise en cache automatique pour la même requête
5export const getPosts = cache(async () => {
6  const response = await fetch('https://api.example.com/posts', {
7    // Les Server Components peuvent utiliser des secrets en toute sécurité
8    headers: {
9      'Authorization': `Bearer ${process.env.API_SECRET}`
10    }
11  });
12  return response.json();
13});
14
15export const getUser = cache(async (id) => {
16  // Requête directe à la base de données
17  return await db.user.findUnique({
18    where: { id },
19    include: { profile: true }
20  });
21});
22
23// components/PostsPage.jsx
24import { getPosts, getUser } from '../utils/api';
25import { Suspense } from 'react';
26
27export default async function PostsPage({ userId }) {
28  // Récupération des données en parallèle
29  const [posts, user] = await Promise.all([
30    getPosts(),
31    getUser(userId)
32  ]);
33
34  return (
35    <main>
36      <h1>Welcome, {user.name}!</h1>
37
38      {/* Server Component imbriqué avec sa propre récupération de données */}
39      <Suspense fallback={<div>Loading posts...</div>}>
40        <PostsList posts={posts} />
41      </Suspense>
42
43      {/* Client Component pour l'interaction */}
44      <CreatePostForm userId={userId} />
45    </main>
46  );
47}

Promise.all récupère les posts et l'utilisateur en parallèle. cache ne fonctionne que dans les Server Components et déduplique les appels avec les mêmes arguments, donc plusieurs composants peuvent appeler getUser(userId) sans requêtes supplémentaires.

Streaming et Suspense

Toutes les parties de la page ne se chargent pas aussi vite. Suspense avec la propriété fallback marque une frontière : le serveur envoie d'abord un squelette de remplacement, puis transmet en streaming les sections terminées :

1// components/DashboardPage.jsx
2import { Suspense } from 'react';
3
4export default function DashboardPage() {
5  return (
6    <div className="dashboard">
7      <h1>Dashboard</h1>
8
9      {/* Contenu qui se charge vite */}
10      <Suspense fallback={<SkeletonCard />}>
11        <UserStats />
12      </Suspense>
13
14      {/* Contenu qui se charge plus lentement */}
15      <Suspense fallback={<SkeletonChart />}>
16        <AnalyticsChart />
17      </Suspense>
18
19      {/* Contenu qui se charge très lentement */}
20      <Suspense fallback={<SkeletonTable />}>
21        <ReportsTable />
22      </Suspense>
23    </div>
24  );
25}
26
27// components/UserStats.jsx (Server Component)
28async function UserStats() {
29  // Requête rapide - cache hit
30  const stats = await getUserStats();
31
32  return (
33    <div className="stats-grid">
34      <StatCard title="Total Users" value={stats.totalUsers} />
35      <StatCard title="Active Today" value={stats.activeToday} />
36      <StatCard title="Revenue" value={`$${stats.revenue}`} />
37    </div>
38  );
39}
40
41// components/AnalyticsChart.jsx (Server Component)
42async function AnalyticsChart() {
43  // Requête de durée moyenne
44  const chartData = await getAnalyticsData();
45
46  return (
47    <div className="chart-container">
48      {/* Client Component pour le graphique interactif */}
49      <InteractiveChart data={chartData} />
50    </div>
51  );
52}
53
54// components/ReportsTable.jsx (Server Component)
55async function ReportsTable() {
56  // Requête longue - agrégations complexes
57  const reports = await getComplexReports();
58
59  return (
60    <div className="reports-table">
61      <table>
62        <thead>
63          <tr>
64            <th>Report</th>
65            <th>Status</th>
66            <th>Date</th>
67          </tr>
68        </thead>
69        <tbody>
70          {reports.map(report => (
71            <tr key={report.id}>
72              <td>{report.name}</td>
73              <td>{report.status}</td>
74              <td>{report.date}</td>
75            </tr>
76          ))}
77        </tbody>
78      </table>
79    </div>
80  );
81}

Chaque section a sa propre frontière, donc le tableau de rapports, lent, ne bloque pas les statistiques. Suspense gère l'état de chargement, et les erreurs sont captées par un Error Boundary, donc ensemble ils couvrent tout le cycle d'une opération asynchrone. C'est le même streaming que celui que tu as découvert dans la leçon sur le Streaming SSR.

Composition des Server et Client Components

Voici les patterns de composition qui fonctionnent, et un qui échoue :

1// patterns/CompositionPatterns.jsx
2
3// BON - un Server Component peut rendre un Client Component
4async function ServerParent() {
5  const data = await fetchData();
6
7  return (
8    <div>
9      <h1>Server Rendered Title</h1>
10      <ClientChild data={data} />
11    </div>
12  );
13}
14
15// BON - un Client Component peut rendre un Server Component via children
16'use client';
17function ClientWrapper({ children }) {
18  const [isOpen, setIsOpen] = useState(false);
19
20  return (
21    <div>
22      <button onClick={() => setIsOpen(!isOpen)}>
23        Toggle Content
24      </button>
25      {isOpen && children}
26    </div>
27  );
28}
29
30// Utilisation :
31function ParentPage() {
32  return (
33    <ClientWrapper>
34      <ServerContent /> {/* Server Component passé en children */}
35    </ClientWrapper>
36  );
37}
38
39// MAUVAIS - un Client Component ne peut pas importer directement un Server Component
40'use client';
41function BadClientComponent() {
42  const [show, setShow] = useState(false);
43
44  return (
45    <div>
46      {show && <ServerContent />} {/* Ça ne marchera pas ! */}
47    </div>
48  );
49}
50
51// BON - Server Component passé en prop depuis le JSX (slot)
52'use client';
53function GoodClientComponent({ content }) {
54  const [show, setShow] = useState(false);
55
56  return (
57    <div>
58      <button onClick={() => setShow(!show)}>Toggle</button>
59      {show && content}
60    </div>
61  );
62}
63
64// Utilisation :
65function ParentComponent() {
66  return (
67    <GoodClientComponent
68      content={<ServerContent />}
69    />
70  );
71}

Le pattern recommandé est un Server Component qui rend un Client Component comme enfant. Quand un composant client doit contenir un composant serveur, passe-le en children ou en prop depuis le JSX. Tu ne peux pas envoyer une fonction, comme une render prop, du serveur vers le client, car elle ne peut pas être sérialisée, et importer un composant serveur dans un fichier avec 'use client' le transforme en composant client.

Patterns avancés

Mutations via les Server Functions

Pour modifier des données, il existe les Server Functions avec la directive 'use server', appelées Server Actions dans les formulaires. Le formulaire les appelle sans que tu aies à écrire ton propre endpoint :

1// actions/postActions.js
2'use server'; // Server Actions
3
4import { revalidatePath } from 'next/cache';
5
6export async function createPost(prevState, formData) {
7  const title = formData.get('title');
8  const content = formData.get('content');
9
10  // Validation côté serveur
11  if (!title || !content) {
12    return { error: 'Title and content are required' };
13  }
14
15  try {
16    const post = await db.post.create({
17      data: { title, content, userId: getCurrentUserId() }
18    });
19
20    // Revalidation du cache de la page des posts
21    revalidatePath('/posts');
22
23    return { success: true, post };
24  } catch (error) {
25    return { error: 'Failed to create post' };
26  }
27}
28
29export async function deletePost(postId) {
30  try {
31    await db.post.delete({
32      where: { id: postId }
33    });
34
35    revalidatePath('/posts');
36    return { success: true };
37  } catch (error) {
38    return { error: 'Failed to delete post' };
39  }
40}
41
42// components/CreatePostForm.jsx
43'use client';
44
45import { createPost } from '../actions/postActions';
46import { useActionState } from 'react';
47import { useFormStatus } from 'react-dom';
48
49function SubmitButton() {
50  const { pending } = useFormStatus();
51
52  return (
53    <button type="submit" disabled={pending}>
54      {pending ? 'Creating...' : 'Create Post'}
55    </button>
56  );
57}
58
59export default function CreatePostForm() {
60  const [state, formAction] = useActionState(createPost, null);
61
62  return (
63    <form action={formAction}>
64      <div>
65        <label htmlFor="title">Title:</label>
66        <input
67          type="text"
68          id="title"
69          name="title"
70          required
71        />
72      </div>
73
74      <div>
75        <label htmlFor="content">Content:</label>
76        <textarea
77          id="content"
78          name="content"
79          required
80        />
81      </div>
82
83      {state?.error && (
84        <div className="error">{state.error}</div>
85      )}
86
87      {state?.success && (
88        <div className="success">Post created successfully!</div>
89      )}
90
91      <SubmitButton />
92    </form>
93  );
94}

useActionState passe à l'action l'état précédent en premier argument et les données du formulaire en second, c'est pourquoi createPost reçoit prevState, formData. useFormStatus désactive le bouton pendant l'envoi, et revalidatePath('/posts') rafraîchit la liste. La validation dans l'action est obligatoire, car une action est un endpoint public.

Stratégies de cache

Le cache de React ne fonctionne que pendant une seule requête. Pour mémoriser le résultat d'une requête coûteuse d'une requête à l'autre, Next.js 16 propose la directive 'use cache', que tu actives avec l'option cacheComponents: true dans next.config.js. cacheLife fixe la durée de vie d'une entrée, et cacheTag lui donne une étiquette pour l'invalidation :

1// lib/posts.js
2import { cacheLife, cacheTag } from 'next/cache';
3
4export async function getCachedPosts() {
5  'use cache';
6  cacheLife('minutes'); // Actualisation en arrière-plan environ toutes les minutes
7  cacheTag('posts');
8
9  return db.post.findMany({
10    include: { author: true },
11    orderBy: { createdAt: 'desc' },
12  });
13}
14
15export async function getCachedUser(userId) {
16  'use cache';
17  cacheLife('hours');
18  cacheTag('users');
19
20  // userId devient une partie de la clé du cache
21  return db.user.findUnique({
22    where: { id: userId },
23    include: { profile: true },
24  });
25}
26
27// components/PostsWithCache.jsx
28import { getCachedPosts } from '../lib/posts';
29
30export default async function PostsWithCache() {
31  const posts = await getCachedPosts();
32
33  return (
34    <div>
35      <h1>Publications en cache</h1>
36      {posts.map(post => (
37        <article key={post.id}>
38          <h2>{post.title}</h2>
39          <p>Auteur : {post.author.name}</p>
40        </article>
41      ))}
42    </div>
43  );
44}
45
46// actions/cacheActions.js
47'use server';
48
49import { revalidateTag, updateTag } from 'next/cache';
50
51export async function refreshPosts() {
52  revalidateTag('posts', 'max'); // Les anciennes données restent, les fraîches arrivent en arrière-plan
53}
54
55export async function publishPost(formData) {
56  await db.post.create({ data: { title: formData.get('title') } });
57  updateTag('posts'); // L'auteur voit tout de suite sa publication
58}

Les arguments de la fonction, comme userId, entrent automatiquement dans la clé du cache, donc chaque utilisateur a sa propre entrée. revalidateTag('posts', 'max') marque les données comme périmées et les actualise en arrière-plan à la requête suivante, tandis que updateTag, disponible seulement dans les Server Actions, les fait expirer immédiatement pour que l'auteur voie sa modification. L'ancien unstable_cache a été remplacé par 'use cache' dans Next.js 16, et revalidateTag à un seul argument est déprécié.

Performance et optimisations

Le plus grand gain des RSC ne se voit pas dans le code : les composants serveur et leurs imports n'arrivent pas dans le bundle client :

1// Les Server Components n'arrivent pas dans le bundle client !
2
3// ServerHeavyComponent.jsx (Server Component)
4import { someHeavyLibrary } from 'heavy-library'; // N'arrivera pas dans le bundle client !
5import { complexCalculation } from './utils/complex';
6
7export default async function ServerHeavyComponent() {
8  const data = await fetchData();
9  const processed = complexCalculation(data); // Se passe sur le serveur
10  const result = someHeavyLibrary.process(processed);
11
12  return (
13    <div>
14      <h2>Processed Data</h2>
15      <pre>{JSON.stringify(result, null, 2)}</pre>
16    </div>
17  );
18}
19
20// ClientLightComponent.jsx (Client Component)
21'use client';
22
23import { useState } from 'react';
24
25export default function ClientLightComponent({ serverData }) {
26  const [selected, setSelected] = useState(null);
27
28  return (
29    <div>
30      {serverData.map(item => (
31        <button
32          key={item.id}
33          onClick={() => setSelected(item)}
34          className={selected?.id === item.id ? 'selected' : ''}
35        >
36          {item.name}
37        </button>
38      ))}
39      {selected && <ItemDetails item={selected} />}
40    </div>
41  );
42}

heavy-library ne tourne que sur le serveur, donc l'utilisateur ne la télécharge pas. Seul le léger ClientLightComponent arrive dans le navigateur.

Error Boundaries pour les Server Components

Une erreur dans un composant serveur remonte jusqu'à la frontière d'erreur la plus proche côté client. Un Error Boundary doit être une classe, donc le fichier commence par 'use client' :

1// components/ServerErrorBoundary.jsx
2'use client';
3
4import { Component } from 'react';
5
6export class ServerErrorBoundary extends Component {
7  constructor(props) {
8    super(props);
9    this.state = { hasError: false, error: null };
10  }
11
12  static getDerivedStateFromError(error) {
13    return { hasError: true, error };
14  }
15
16  componentDidCatch(error, errorInfo) {
17    console.error('Server component error:', error, errorInfo);
18    // Envoi au service de monitoring
19  }
20
21  render() {
22    if (this.state.hasError) {
23      return (
24        <div className="error-boundary">
25          <h2>Something went wrong on the server</h2>
26          <details>
27            <summary>Error details</summary>
28            <pre>{this.state.error?.message}</pre>
29          </details>
30          <button
31            onClick={() => this.setState({ hasError: false })}
32          >
33            Try again
34          </button>
35        </div>
36      );
37    }
38
39    return this.props.children;
40  }
41}
42
43// Utilisation dans le layout
44export default function Layout({ children }) {
45  return (
46    <html>
47      <body>
48        <ServerErrorBoundary>
49          {children}
50        </ServerErrorBoundary>
51      </body>
52    </html>
53  );
54}

getDerivedStateFromError bascule la vue vers un écran de secours, et componentDidCatch signale l'erreur. En production, Next.js masque le contenu des erreurs du serveur, donc tu verras un message générique, et tu trouveras les détails dans les logs. Dans Next.js, tu n'as pas besoin d'écrire cette classe toi-même : un fichier error.js crée une frontière d'erreur pour tout un segment de route, et depuis la version 16.3 la fonction catchError de next/error en construit une à partir d'un composant fonctionnel et lui passe retry(), qui rend aussi à nouveau les composants serveur.

Le premier framework à intégrer entièrement les RSC a été Next.js avec l'App Router. Je te recommande de commencer chaque composant comme composant serveur et d'ajouter 'use client' seulement là où tu as besoin d'état ou d'événements. Les RSC apportent de meilleures performances, un découpage automatique du code et des opérations plus sûres sur le serveur, et dans la leçon sur le Partial Pre-rendering tu les combineras avec une coque statique de la page.

Retiens : les Server Components sont l'équipe de la station au sol - elle prépare les données et envoie un plan tout prêt, et seuls les modules que quelqu'un pilote vraiment montent à bord. Dans l'exemple ci-dessous, le bouton « Montrer ce qui arrive dans le navigateur » indique quel code reste sur le serveur et lequel tourne dans le navigateur.

Code de cette leçon : App.jsx
1import { Suspense, use, useState } from 'react';
2
3// === React Server Components - une simulation dans le navigateur ===
4// Un vrai Server Component est une fonction asynchrone exécutée sur le serveur :
5//   async function PostsList() { const posts = await db.post.findMany(); ... }
6// Le navigateur ne peut pas exécuter ce code, donc on simule le "serveur" avec une
7// promesse lue par use(), et Suspense gère l’attente, comme lors du streaming du
8// RSC Payload.
9
10const database = {
11  stats: { posts: 156, crew: 89, online: 23 },
12  posts: [
13    { id: 1, title: 'Découverte d’une nouvelle galaxie', author: 'Commandant Nova', likes: 42 },
14    { id: 2, title: 'Technologies de propulsion spatiale', author: 'Astro', likes: 28 },
15    { id: 3, title: 'La vie sur Mars - un rapport', author: 'Ra', likes: 56 },
16  ],
17};
18
19const serverCache = new Map();
20
21// "Requête à la base" - la même promesse pour la même table
22function queryServer(table, delay) {
23  if (!serverCache.has(table)) {
24    serverCache.set(table, new Promise(resolve => setTimeout(() => resolve(database[table]), delay)));
25  }
26  return serverCache.get(table);
27}
28
29function Tag({ type, show }) {
30  if (!show) return null;
31  const server = type === 'server';
32  return (
33    <span style={{ ...styles.tag, ...(server ? styles.serverTag : styles.clientTag) }}>
34      {server ? 'Server Component : 0 Ko de code dans le bundle JS' : "Client Component ('use client') : code dans le bundle JS"}
35    </span>
36  );
37}
38
39// SERVER COMPONENT (simulation) : pas d’état ni d’événements, seulement des données et du JSX
40function ServerStats({ showBundle }) {
41  const stats = use(queryServer('stats', 600));
42
43  return (
44    <section style={{ ...styles.box, ...(showBundle ? styles.serverBox : null) }}>
45      <Tag type="server" show={showBundle} />
46      <div style={styles.statsGrid}>
47        <div style={styles.stat}><strong style={styles.statValue}>{stats.posts}</strong>Publications</div>
48        <div style={styles.stat}><strong style={styles.statValue}>{stats.crew}</strong>Membres d’équipage</div>
49        <div style={styles.stat}><strong style={styles.statValue}>{stats.online}</strong>Online</div>
50      </div>
51    </section>
52  );
53}
54
55// SERVER COMPONENT (simulation) : rend un composant client comme enfant
56// et ne lui passe que des props sérialisables (le nombre de J’aime)
57function ServerPostsList({ showBundle }) {
58  const posts = use(queryServer('posts', 1200));
59
60  return (
61    <section style={{ ...styles.box, ...(showBundle ? styles.serverBox : null) }}>
62      <Tag type="server" show={showBundle} />
63      {posts.map(post => (
64        <article key={post.id} style={styles.post}>
65          <h3 style={styles.postTitle}>{post.title}</h3>
66          <div style={styles.postMeta}>
67            <span>Auteur : {post.author}</span>
68            <LikeButton initialLikes={post.likes} showBundle={showBundle} />
69          </div>
70        </article>
71      ))}
72    </section>
73  );
74}
75
76// CLIENT COMPONENT : dans un projet, un fichier séparé qui commence par 'use client'.
77// Seul ce code arrive dans le navigateur et peut utiliser l’état et les événements.
78function LikeButton({ initialLikes, showBundle }) {
79  const [liked, setLiked] = useState(false);
80  const likes = initialLikes + (liked ? 1 : 0);
81
82  return (
83    <span style={showBundle ? styles.clientBox : null}>
84      <Tag type="client" show={showBundle} />
85      <button onClick={() => setLiked(l => !l)} style={{ ...styles.like, color: liked ? '#ff6b6b' : '#8892b0' }}>
86        {liked ? 'Aimé' : 'J’aime'} ({likes})
87      </button>
88    </span>
89  );
90}
91
92export default function App() {
93  const [showBundle, setShowBundle] = useState(false);
94
95  return (
96    <div style={styles.page}>
97      <div style={{ maxWidth: '700px', margin: '0 auto' }}>
98        <h1 style={styles.title}>React Server Components - simulation</h1>
99        <p style={styles.subtitle}>
100          Les composants serveur récupèrent les données, et seul le bouton J’aime interactif tourne dans le navigateur
101        </p>
102        <button onClick={() => setShowBundle(s => !s)} style={styles.toggle}>
103          {showBundle ? 'Masquer la répartition serveur et client' : 'Montrer ce qui arrive dans le navigateur'}
104        </button>
105
106        <Suspense fallback={<p style={styles.fallback}>Le serveur calcule les statistiques...</p>}>
107          <ServerStats showBundle={showBundle} />
108        </Suspense>
109
110        <h2 style={styles.h2}>Publications de la base (Server Component + LikeButton client)</h2>
111        <Suspense fallback={<p style={styles.fallback}>Le serveur récupère les publications de la base...</p>}>
112          <ServerPostsList showBundle={showBundle} />
113        </Suspense>
114      </div>
115    </div>
116  );
117}
118
119const styles = {
120  page: { minHeight: '100vh', background: 'linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%)', padding: '24px', color: '#e0e1dd', fontFamily: 'system-ui, sans-serif' },
121  title: { textAlign: 'center', color: '#00d4ff', margin: '0 0 6px', fontSize: '22px' },
122  subtitle: { textAlign: 'center', color: '#8892b0', margin: '0 0 14px', fontSize: '13px' },
123  toggle: { display: 'block', margin: '0 auto 16px', padding: '6px 14px', background: 'rgba(0,212,255,0.15)', color: '#00d4ff', border: '1px solid rgba(0,212,255,0.4)', borderRadius: '6px', cursor: 'pointer' },
124  fallback: { color: '#8892b0', padding: '16px', border: '1px dashed rgba(0,212,255,0.25)', borderRadius: '8px' },
125  box: { borderRadius: '10px', padding: '10px', marginBottom: '16px', border: '1px solid transparent' },
126  serverBox: { border: '1px dashed #ffa500', background: 'rgba(255,165,0,0.05)' },
127  clientBox: { display: 'inline-flex', flexDirection: 'column', alignItems: 'flex-end', gap: '2px', padding: '4px', border: '1px solid #00ff88', borderRadius: '6px' },
128  tag: { display: 'inline-block', fontSize: '10px', padding: '2px 6px', borderRadius: '4px', marginBottom: '6px' },
129  serverTag: { background: 'rgba(255,165,0,0.15)', color: '#ffa500' },
130  clientTag: { background: 'rgba(0,255,136,0.15)', color: '#00ff88', marginBottom: 0 },
131  statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px' },
132  stat: { padding: '12px', background: 'rgba(0,212,255,0.08)', borderRadius: '8px', textAlign: 'center', fontSize: '12px', color: '#8892b0' },
133  statValue: { display: 'block', fontSize: '24px', color: '#00d4ff' },
134  h2: { color: '#64ffda', fontSize: '16px', margin: '0 0 8px' },
135  post: { padding: '10px 12px', margin: '6px 0', background: 'rgba(0,0,0,0.3)', borderRadius: '8px', border: '1px solid rgba(0,212,255,0.15)' },
136  postTitle: { color: '#00d4ff', margin: '0 0 6px', fontSize: '15px' },
137  postMeta: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '12px', color: '#8892b0', gap: '8px' },
138  like: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '12px' },
139};

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que signifie la directive 'use client' au début d'un fichier de composant ?

  2. 2. Quelle est la différence clé entre les React Server Components et le SSR traditionnel ?

Voici 2 des 5 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Compose la syntaxe d'une frontière Suspense avec un composant de repli :

  • Éditeur de code

    Affiche les données des constellations de l'observatoire avec Suspense et use() de React 19. ___BLANK1___ : getConstellation enregistre dans le cache la promesse de fetchConstellation(name), pour que chaque nom ne soit récupéré qu'une seule fois. ___BLANK2___ : ConstellationCard lit les données avec use() à partir de la promesse renvoyée par getConstellation(name) (ne crée pas de nouvelle promesse pendant le rendu). ___BLANK3___ : le fallback de la frontière Suspense est un élément avec un texte de chargement, par ex. <p>Chargement des données du télescope...</p>. Après un clic sur une autre constellation, tu verras le fallback un instant, et en revenant à une constellation déjà récupérée, les données s'affichent tout de suite.

  • Tri vertical

    Ordonne les étapes du pipeline de SSR en streaming dans React (depuis la version 18) :

  • Éditeur de code

    Termine la classe MissionBoundary, la frontière d'erreurs des panneaux de la passerelle. Le capteur de carburant est en panne et son panneau lève une erreur, tandis que les autres panneaux doivent continuer à fonctionner. ___BLANK1___ : getDerivedStateFromError renvoie le nouvel état avec un champ error qui contient l'erreur capturée ({ error }). ___BLANK2___ : componentDidCatch signale la panne avec reportCrash(error, info.componentStack). ___BLANK3___ : quand l'état contient une erreur, render renvoie la vue de this.props.renderFallback, à laquelle tu passes l'erreur de l'état et la fonction this.reset (la classe reste sans JSX). Un clic sur Redémarrer le capteur répare le capteur et réinitialise la frontière : le panneau du carburant revient avec la valeur 50%.

  • Tri horizontal

    Compose la syntaxe pour envelopper un composant dans Error Boundary avec un fallback :

  • Tri vertical

    Ordonne le cycle de vie d'une requête dans l'architecture des React Server Components :

  • Clique dans l'ordre

    Ordonne l'implémentation d'un Server Component :

  • Éditeur de code

    Complète les boutons de la bibliothèque de composants de la Flotte. ___BLANK1___ : le bouton est désactivé quand il a la prop disabled ou pendant le chargement (loading). ___BLANK2___ : le padding vient des tokens : tokens.spacing pour la taille choisie size (sm, md ou lg). ___BLANK3___ : IconButton n'affiche qu'une icône, passe donc à Button un attribut aria-label avec la valeur de label, pour que le lecteur d'écran annonce le nom du bouton. Les composants n'ont ni état ni effets : ils fonctionnent donc aussi dans les Server Components.

Articles utiles