Retour au blog

React 19 Server Components : Guide Pratique Pour les Développeurs

Salut HaWkers, React 19 a apporté les Server Components comme fonctionnalité stable, changeant fondamentalement notre façon de penser le rendu dans React. Si vous n'avez pas encore bien compris comment ils fonctionnent ou quand les utiliser, ce guide pratique va tout clarifier.

Vous êtes-vous déjà demandé pourquoi vos bundles JavaScript sont si gros ou pourquoi la performance initiale de votre application est lente ? Les Server Components pourraient être la réponse.

Que Sont les Server Components

Les Server Components sont des composants React qui s'exécutent exclusivement sur le serveur. Contrairement au SSR traditionnel, ils n'envoient jamais de JavaScript au client.

Différence Fondamentale

Comparatif des approches :

Aspect Client Components Server Components
Où s'exécute Navigateur Serveur
JavaScript côté client Oui Non
Accès à la base de données Via API Direct
État et événements useState, onClick Non disponible
Performance initiale Plus lent Plus rapide

💡 Contexte : Les Server Components ne remplacent pas les Client Components. Ils travaillent ensemble, chacun dans ce qu'il fait de mieux.

Comment Ils Fonctionnent en Pratique

Voyons des exemples concrets de Server et Client Components :

Server Component Basique

// app/posts/page.tsx (Server Component par défaut)
import { db } from '@/lib/database';

// Ce composant ne va JAMAIS au navigateur
async function PostsPage() {
  // Accès direct à la base de données
  const posts = await db.posts.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });

  return (
    <div className="posts-container">
      <h1>Derniers Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <time>{new Date(post.createdAt).toLocaleDateString()}</time>
        </article>
      ))}
    </div>
  );
}

export default PostsPage;

Client Component

// components/LikeButton.tsx
'use client'; // Marqueur obligatoire

import { useState, useTransition } from 'react';
import { likePost } from '@/actions/posts';

interface LikeButtonProps {
  postId: string;
  initialLikes: number;
}

export function LikeButton({ postId, initialLikes }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes);
  const [isPending, startTransition] = useTransition();

  const handleLike = () => {
    startTransition(async () => {
      const newLikes = await likePost(postId);
      setLikes(newLikes);
    });
  };

  return (
    <button
      onClick={handleLike}
      disabled={isPending}
      className="like-button"
    >
      {isPending ? '...' : `❤️ ${likes}`}
    </button>
  );
}

Combiner Server et Client

// app/posts/[id]/page.tsx
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';
import { CommentSection } from '@/components/CommentSection';

async function PostPage({ params }: { params: { id: string } }) {
  const post = await db.posts.findUnique({
    where: { id: params.id },
    include: { author: true, comments: true },
  });

  if (!post) {
    return <div>Post non trouvé</div>;
  }

  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <span>Par {post.author.name}</span>
      </header>

      <div className="content">
        {post.content}
      </div>

      {/* Client Component avec interactivité */}
      <LikeButton postId={post.id} initialLikes={post.likes} />

      {/* Autre Client Component */}
      <CommentSection
        postId={post.id}
        initialComments={post.comments}
      />
    </article>
  );
}

export default PostPage;

Patterns de Composition

La clé pour utiliser efficacement les Server Components est de comprendre les patterns de composition :

1. Passer des Server Components comme Children

// components/ClientWrapper.tsx
'use client';

import { useState } from 'react';

interface ClientWrapperProps {
  children: React.ReactNode;
}

export function ClientWrapper({ children }: ClientWrapperProps) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div className={isExpanded ? 'expanded' : 'collapsed'}>
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Réduire' : 'Développer'}
      </button>
      {isExpanded && children}
    </div>
  );
}

// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper';
import { ExpensiveServerComponent } from '@/components/ExpensiveServerComponent';

export default function Page() {
  return (
    <ClientWrapper>
      {/* Ce Server Component est passé comme children */}
      <ExpensiveServerComponent />
    </ClientWrapper>
  );
}

2. Pattern Slots

// components/Layout.tsx
'use client';

import { useState } from 'react';

interface LayoutProps {
  header: React.ReactNode;
  sidebar: React.ReactNode;
  content: React.ReactNode;
}

export function Layout({ header, sidebar, content }: LayoutProps) {
  const [sidebarOpen, setSidebarOpen] = useState(true);

  return (
    <div className="layout">
      <header>{header}</header>
      <button onClick={() => setSidebarOpen(!sidebarOpen)}>
        Toggle Sidebar
      </button>
      {sidebarOpen && <aside>{sidebar}</aside>}
      <main>{content}</main>
    </div>
  );
}

// app/dashboard/page.tsx
import { Layout } from '@/components/Layout';
import { UserStats } from '@/components/UserStats';
import { Navigation } from '@/components/Navigation';
import { DashboardContent } from '@/components/DashboardContent';

export default function DashboardPage() {
  return (
    <Layout
      header={<Navigation />}
      sidebar={<UserStats />}
      content={<DashboardContent />}
    />
  );
}

Server Actions

React 19 a introduit les Server Actions pour les mutations de façon intégrée :

Créer des Server Actions

// actions/posts.ts
'use server';

import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';

const PostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
});

export async function createPost(formData: FormData) {
  const rawData = {
    title: formData.get('title'),
    content: formData.get('content'),
  };

  const validatedData = PostSchema.safeParse(rawData);

  if (!validatedData.success) {
    return { error: validatedData.error.flatten().fieldErrors };
  }

  try {
    const post = await db.posts.create({
      data: validatedData.data,
    });

    revalidatePath('/posts');
    return { success: true, postId: post.id };
  } catch (error) {
    return { error: 'Échec de la création du post' };
  }
}

export async function likePost(postId: string) {
  const post = await db.posts.update({
    where: { id: postId },
    data: { likes: { increment: 1 } },
  });

  revalidatePath(`/posts/${postId}`);
  return post.likes;
}

export async function deletePost(postId: string) {
  await db.posts.delete({ where: { id: postId } });
  revalidatePath('/posts');
}

Utiliser les Server Actions dans les Forms

// components/CreatePostForm.tsx
'use client';

import { useActionState } from 'react';
import { createPost } from '@/actions/posts';

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">Titre</label>
        <input
          id="title"
          name="title"
          type="text"
          required
          disabled={isPending}
        />
        {state?.error?.title && (
          <span className="error">{state.error.title}</span>
        )}
      </div>

      <div>
        <label htmlFor="content">Contenu</label>
        <textarea
          id="content"
          name="content"
          rows={10}
          required
          disabled={isPending}
        />
        {state?.error?.content && (
          <span className="error">{state.error.content}</span>
        )}
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? 'Création...' : 'Créer Post'}
      </button>

      {state?.success && (
        <div className="success">Post créé avec succès !</div>
      )}
    </form>
  );
}

Optimisation et Performance

Les Server Components apportent des bénéfices significatifs de performance :

1. Streaming avec Suspense

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { SlowDataComponent } from '@/components/SlowDataComponent';
import { FastDataComponent } from '@/components/FastDataComponent';

export default function DashboardPage() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* Composant rapide se rend immédiatement */}
      <FastDataComponent />

      {/* Composant lent charge en streaming */}
      <Suspense fallback={<div>Chargement des données...</div>}>
        <SlowDataComponent />
      </Suspense>

      {/* Plusieurs Suspense pour streaming parallèle */}
      <div className="widgets">
        <Suspense fallback={<WidgetSkeleton />}>
          <AnalyticsWidget />
        </Suspense>

        <Suspense fallback={<WidgetSkeleton />}>
          <RevenueWidget />
        </Suspense>

        <Suspense fallback={<WidgetSkeleton />}>
          <UsersWidget />
        </Suspense>
      </div>
    </div>
  );
}

2. Preloading de Données

// lib/preload.ts
import { cache } from 'react';

export const getUser = cache(async (userId: string) => {
  const user = await db.users.findUnique({ where: { id: userId } });
  return user;
});

export const preloadUser = (userId: string) => {
  void getUser(userId);
};

// components/UserProfile.tsx
import { getUser, preloadUser } from '@/lib/preload';

// Dans un composant parent
function ParentComponent({ userId }: { userId: string }) {
  // Démarre le fetch à l'avance
  preloadUser(userId);

  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}

// Le composant enfant utilise les données pré-chargées
async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId); // Déjà en cache !

  return (
    <div className="profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}

Quand Utiliser Chaque Type

Guide pratique pour décider entre Server et Client Components :

Utilisez les Server Components Pour

Cas idéaux :

  • Fetch de données depuis la base
  • Accès aux APIs privées
  • Composants sans interactivité
  • Contenu statique ou semi-statique
  • Réduction de la taille du bundle
// Exemples de Server Components
// - Listes de données
// - Headers et footers
// - Navigation statique
// - Contenu de pages
// - Composants de layout

async function ProductList() {
  const products = await db.products.findMany();

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <h3>{product.name}</h3>
          <p>{product.price}</p>
        </li>
      ))}
    </ul>
  );
}

Utilisez les Client Components Pour

Cas idéaux :

  • Interactivité (onClick, onChange)
  • État local (useState)
  • Effets (useEffect)
  • APIs du navigateur (localStorage, geolocation)
  • Bibliothèques client-only
// Exemples de Client Components
// - Formulaires
// - Modals et dialogs
// - Carousels et sliders
// - Tooltips et dropdowns
// - Composants avec animation

'use client';

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const search = async () => {
      if (query.length > 2) {
        const data = await fetch(`/api/search?q=${query}`);
        setResults(await data.json());
      }
    };
    const timer = setTimeout(search, 300);
    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Rechercher..."
      />
      <ul>
        {results.map(r => <li key={r.id}>{r.title}</li>)}
      </ul>
    </div>
  );
}

Erreurs Courantes et Solutions

Évitez ces problèmes fréquents :

1. Importer un Client Component sans 'use client'

// ❌ Mauvais : utiliser des hooks sans 'use client'
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // Erreur !
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// ✅ Correct : ajouter 'use client'
'use client';

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

2. Passer des Fonctions Non Sérialisables

// ❌ Mauvais : passer une fonction du Server au Client
async function ServerComponent() {
  const handleClick = () => console.log('clicked'); // Non sérialisable !

  return <ClientButton onClick={handleClick} />; // Erreur !
}

// ✅ Correct : utiliser une Server Action
async function ServerComponent() {
  return <ClientButton postId="123" />;
}

// Le Client Component appelle l'action
'use client';
import { likePost } from '@/actions/posts';

function ClientButton({ postId }: { postId: string }) {
  return <button onClick={() => likePost(postId)}>Like</button>;
}

3. Fetch dans un Client Component Inutilement

// ❌ Mauvais : fetching côté client quand ça pourrait être côté serveur
'use client';

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers);
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// ✅ Correct : Server Component avec accès direct
async function UserList() {
  const users = await db.users.findMany();

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Migrer Vers les Server Components

Si vous avez une application React existante :

Stratégie de Migration

Étapes recommandées :

  1. Identifiez les composants sans interactivité
  2. Déplacez le fetch de données vers les Server Components
  3. Ajoutez 'use client' où nécessaire
  4. Refactorisez vers des patterns de composition
  5. Implémentez des Server Actions pour les mutations

Conclusion

React 19 Server Components représentent un changement fondamental dans la façon de construire des applications React. En exécutant les composants sur le serveur et en envoyant seulement du HTML au client, nous obtenons des applications plus rapides, des bundles plus petits, et un accès direct aux ressources serveur.

La clé est de comprendre que Server et Client Components travaillent ensemble. Utilisez les Server Components pour fetcher des données et rendre du contenu statique, et les Client Components pour l'interactivité. Avec cette division claire, vos applications seront plus performantes et plus faciles à maintenir.

Si vous voulez approfondir React et le développement frontend moderne, je recommande de jeter un œil à un autre article : Svelte 5 et Runes : Pourquoi le Framework Gagne du Terrain où vous découvrirez des alternatives intéressantes à React.

C'est parti ! 🦅

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires