Maxpaths
Fondamentaux·Section 1/19

Qu'est-ce qui change vraiment dans React 19 ?

React 19 marque une évolution majeure avec l'introduction du React Compiler, des Server Components par défaut, et des APIs révolutionnaires comme use() et les Actions. Cette version transforme fondamentalement la façon dont nous développons des applications React performantes.

Les Piliers de React 19

React 19 apporte 4 changements fondamentaux qui redéfinissent le développement React moderne.

React Compiler

Optimisation automatique avec memoization intelligente, éliminant le besoin de useMemo/useCallback dans la plupart des cas

React Server Components

Foundation de React 19, permettant un rendu serveur sans JavaScript côté client pour une performance optimale

Actions & Async Transitions

Gestion automatique des états pending, errors et optimistic updates avec les async transitions

Hook use()

Nouvelle API pour lire des ressources (promises, context) en mode conditionnel dans le render

Nouveautés React 19

Les fonctionnalités ajoutées qui transforment le développement React.

  • use() : Hook pour lire promises et context conditionnellement
  • Actions : Fonctions async dans transitions avec gestion automatique des états
  • useActionState : Remplacement de useFormState pour gérer les actions
  • useOptimistic : Updates optimistes avec rollback automatique
  • Refs as Props : Plus besoin de forwardRef dans 95% des cas
  • Document Metadata : Tags title, meta, link dans les composants
  • Error Handling amélioré : Dédoublonnage et options pour caught/uncaught errors
  • Partial Pre-rendering (19.2) : Pré-render statique + dynamic fill

Breaking Changes & Migration

Points d'attention lors de la migration depuis React 18.

  • forwardRef : Déprécié, utiliser refs directement en props
  • useFormState : Renommé en useActionState
  • React.render : ReactDOM.render déprécié, utiliser createRoot
  • Concurrent rendering : Activé par défaut (pas de flag)
components/button.tsxtsx
1// React 18 vs React 19 - Comparaison
2
3// ❌ React 18 - Verbeux avec forwardRef et useMemo
4import { forwardRef, useMemo, useCallback } from 'react';
5
6const Button = forwardRef<HTMLButtonElement, ButtonProps>(
7 ({ onClick, children }, ref) => {
8 const handleClick = useCallback(() => {
9 onClick();
10 }, [onClick]);
11
12 const computedValue = useMemo(() => {
13 return expensiveCalculation();
14 }, []);
15
16 return (
17 <button ref={ref} onClick={handleClick}>
18 {children}
19 </button>
20 );
21 }
22);
23
24// ✅ React 19 - Simplifié avec Compiler et refs as props
25function Button({ ref, onClick, children }: ButtonProps) {
26 // Le React Compiler gère automatiquement la memoization
27 const handleClick = () => onClick();
28 const computedValue = expensiveCalculation();
29
30 return (
31 <button ref={ref} onClick={handleClick}>
32 {children}
33 </button>
34 );
35}
migration-guide.tstsx
1// Migration Step-by-Step
2
3// 1. Mettre à jour les dépendances
4{
5 "dependencies": {
6 "react": "^19.0.0",
7 "react-dom": "^19.0.0"
8 }
9}
10
11// 2. Remplacer ReactDOM.render par createRoot (si pas déjà fait)
12// ❌ Ancien
13import ReactDOM from 'react-dom';
14ReactDOM.render(<App />, document.getElementById('root'));
15
16// ✅ Nouveau
17import { createRoot } from 'react-dom/client';
18const root = createRoot(document.getElementById('root')!);
19root.render(<App />);
20
21// 3. Supprimer les forwardRef inutiles
22// ❌ Avant
23const Input = forwardRef<HTMLInputElement, InputProps>(
24 (props, ref) => <input ref={ref} {...props} />
25);
26
27// ✅ Après
28function Input({ ref, ...props }: InputProps) {
29 return <input ref={ref} {...props} />;
30}
31
32// 4. Renommer useFormState en useActionState
33// ❌ Avant
34import { useFormState } from 'react-dom';
35
36// ✅ Après
37import { useActionState } from 'react';
38
39// 5. Activer le React Compiler (optionnel mais recommandé)
40// babel.config.js
41module.exports = {
42 plugins: [
43 ['babel-plugin-react-compiler', {
44 target: '19'
45 }]
46 ]
47};

Philosophie React 19

React 19 adopte une philosophie "Convention over Configuration" : le Compiler optimise automatiquement, les Server Components deviennent la norme, et les APIs sont simplifiées pour réduire le boilerplate. L'objectif est de permettre aux développeurs de se concentrer sur la logique métier plutôt que sur les optimisations manuelles.

Fondamentaux·Section 2/19

Comment use() simplifie le data fetching ?

Le hook use() est l'une des innovations majeures de React 19. Il permet de lire des ressources (promises, context) directement dans le render, avec la possibilité d'utilisation conditionnelle - ce qui était impossible avec les hooks classiques.

use() : Un Hook Révolutionnaire

Contrairement aux autres hooks, use() peut être appelé conditionnellement et dans des boucles, ouvrant de nouvelles possibilités architecturales.

  • Suspension automatique : Suspense jusqu'à la résolution de la promise
  • Utilisation conditionnelle : Peut être appelé après un early return
  • Intégration Suspense : Fonctionne nativement avec les boundaries Suspense
  • Type-safe : Inférence TypeScript automatique du type de retour
app/profile/page.tsxtsx
1// Exemple 1 : Lire une Promise avec use()
2import { use, Suspense } from 'react';
3
4// Promise stable (créée en dehors du composant ou avec useMemo)
5const userPromise = fetchUser(userId);
6
7function UserProfile() {
8 // use() suspend le rendu jusqu'à la résolution
9 const user = use(userPromise);
10
11 return (
12 <div>
13 <h1>{user.name}</h1>
14 <p>{user.email}</p>
15 </div>
16 );
17}
18
19// Wrapper avec Suspense obligatoire
20export function UserProfilePage() {
21 return (
22 <Suspense fallback={<div>Chargement...</div>}>
23 <UserProfile />
24 </Suspense>
25 );
26}
components/button.tsxtsx
1// Exemple 2 : Utilisation Conditionnelle (IMPOSSIBLE avec useContext)
2import { use } from 'react';
3import { ThemeContext } from './theme-context';
4
5function Button({ variant }: ButtonProps) {
6 // ✅ use() PEUT être appelé après un early return
7 if (variant === 'unstyled') {
8 return <button>Click me</button>;
9 }
10
11 // Lecture du context seulement si nécessaire
12 const theme = use(ThemeContext);
13
14 return (
15 <button style={{ backgroundColor: theme.primaryColor }}>
16 Styled Button
17 </button>
18 );
19}
20
21// ❌ Impossible avec useContext (violera les Rules of Hooks)
22function ButtonWrong({ variant }: ButtonProps) {
23 if (variant === 'unstyled') {
24 return <button>Click me</button>;
25 }
26
27 // ❌ ERREUR: useContext appelé conditionnellement
28 const theme = useContext(ThemeContext);
29
30 return <button>...</button>;
31}

Patterns Avancés avec use()

Techniques pour maximiser l'efficacité du hook use() dans des scénarios complexes.

1. Promise Stable avec useMemo

La promise doit être stable (même référence) entre les renders. Utiliser useMemo ou créer la promise en dehors du composant.

2. Streaming de Données

Combiner use() avec des promises qui se résolvent progressivement pour un streaming de données fluide.

3. Waterfall vs Parallel

Attention aux waterfalls : créer toutes les promises avant d'appeler use() pour un chargement parallèle.

app/dashboard/page.tsxtsx
1// Pattern : Promise Stable avec useMemo
2import { use, useMemo, Suspense } from 'react';
3
4function UserDashboard({ userId }: { userId: string }) {
5 // ✅ Promise stable grâce à useMemo
6 const userPromise = useMemo(() => {
7 return fetchUser(userId);
8 }, [userId]); // Recréée seulement si userId change
9
10 const user = use(userPromise);
11
12 return <div>Welcome, {user.name}!</div>;
13}
14
15// ❌ Anti-pattern : Promise recréée à chaque render
16function UserDashboardWrong({ userId }: { userId: string }) {
17 // ❌ Nouvelle promise à chaque render = boucle infinie
18 const userPromise = fetchUser(userId);
19 const user = use(userPromise);
20
21 return <div>Welcome, {user.name}!</div>;
22}
app/profile/page.tsxtsx
1// Pattern : Parallel Loading (éviter les waterfalls)
2import { use, useMemo } from 'react';
3
4function UserWithPosts({ userId }: { userId: string }) {
5 // ✅ Créer TOUTES les promises AVANT de les consommer
6 const promises = useMemo(() => {
7 const userPromise = fetchUser(userId);
8 const postsPromise = fetchPosts(userId);
9 return { userPromise, postsPromise };
10 }, [userId]);
11
12 // Consommer en parallèle
13 const user = use(promises.userPromise);
14 const posts = use(promises.postsPromise);
15
16 return (
17 <div>
18 <h1>{user.name}</h1>
19 <ul>
20 {posts.map(post => <li key={post.id}>{post.title}</li>)}
21 </ul>
22 </div>
23 );
24}
25
26// ❌ Anti-pattern : Waterfall (séquentiel)
27function UserWithPostsWrong({ userId }: { userId: string }) {
28 const userPromise = useMemo(() => fetchUser(userId), [userId]);
29 const user = use(userPromise); // Attend user...
30
31 // ❌ posts ne commence à charger QUE quand user est résolu
32 const postsPromise = useMemo(() => fetchPosts(userId), [userId]);
33 const posts = use(postsPromise);
34
35 return <div>...</div>;
36}
Lire une Promise
Suspendre le rendu jusqu'à la résolution de la promise
Avantages
  • Syntaxe simple et intuitive
  • Intégration native avec Suspense
  • Gestion automatique du loading
Inconvenients
  • Nécessite un boundary Suspense
  • Promise doit être stable (pas recréée à chaque render)
Cas d'usage
  • Data fetching dans des composants
  • Chargement de ressources async
  • Lazy loading de données
Lire un Context
Accéder au context de manière conditionnelle
Avantages
  • Permet les early returns
  • Simplification du code
  • Pas besoin de wrapper HOC
Inconvenients
  • Syntaxe différente de useContext
  • Courbe d'apprentissage
Cas d'usage
  • Accès conditionnel au context
  • Branches conditionnelles
  • Simplification de logique complexe
app/data/page.tsxtsx
1// Pattern : Error Handling avec use() et Error Boundary
2import { use, Suspense } from 'react';
3import { ErrorBoundary } from 'react-error-boundary';
4
5function DataComponent({ dataPromise }: { dataPromise: Promise<Data> }) {
6 const data = use(dataPromise); // Si reject, throw l'erreur
7
8 return <div>{data.content}</div>;
9}
10
11export function DataPage() {
12 const dataPromise = useMemo(() => fetchData(), []);
13
14 return (
15 <ErrorBoundary
16 fallback={<div>Erreur lors du chargement</div>}
17 onError={(error) => console.error(error)}
18 >
19 <Suspense fallback={<div>Chargement...</div>}>
20 <DataComponent dataPromise={dataPromise} />
21 </Suspense>
22 </ErrorBoundary>
23 );
24}

use() vs useEffect pour Data Fetching

La nouvelle approche recommandée pour charger des données dans React 19.

useEffect (React 18)

  • • Race conditions possibles
  • • Gestion manuelle du loading
  • • Logique complexe d'état
  • • Waterfalls fréquents

use() + Suspense (React 19)

  • • Pas de race conditions
  • • Loading automatique via Suspense
  • • Code déclaratif et simple
  • • Parallel loading natif

Recommandations Seniors

  • Toujours wrapper avec Suspense : use() nécessite un boundary Suspense parent
  • Promises stables : Utiliser useMemo ou créer hors du composant
  • Préférer use() à useEffect pour le data fetching (moins de bugs, meilleure perf)
  • Error Boundaries : Toujours gérer les rejections de promises
  • TanStack Query : Pour des besoins avancés (caching, revalidation, mutations)
Modes de Rendu·Section 3/19

Pourquoi le React Compiler va remplacer useMemo ?

Le React Compiler est une transformation build-time qui analyse votre code et insère automatiquement les optimisations nécessaires. Il élimine le besoin deuseMemo, useCallback etmemo() dans 95% des cas.

Comment Fonctionne le React Compiler

Un compilateur qui transforme votre code React en code optimisé avec memoization automatique.

  • Analyse statique : Détecte automatiquement les valeurs réactives (props, state)
  • Fine-grained memoization : Optimise au niveau des expressions, pas seulement des composants
  • Inférence de dépendances : Calcule automatiquement les dépendances (plus besoin de les spécifier)
  • Build-time : Zéro impact runtime, optimisations appliquées au build
Configurationtypescript
1// Installation et Configuration
2// 1. Installer le plugin Babel
3npm install --save-dev babel-plugin-react-compiler
4
5// 2. Configuration Babel
6// babel.config.js
7module.exports = {
8 plugins: [
9 ['babel-plugin-react-compiler', {
10 target: '19', // Version de React
11 runtimeModule: 'react/compiler-runtime'
12 }]
13 ]
14};
15
16// 3. Pour Vite
17// vite.config.ts
18import { defineConfig } from 'vite';
19import react from '@vitejs/plugin-react';
20
21export default defineConfig({
22 plugins: [
23 react({
24 babel: {
25 plugins: [['babel-plugin-react-compiler', { target: '19' }]]
26 }
27 })
28 ]
29});
components/expensive-component-old.tsxtsx
1// Avant : React 18 avec useMemo/useCallback manuel
2import { useMemo, useCallback, memo } from 'react';
3
4function ExpensiveComponent({ data, onUpdate }: Props) {
5 // ❌ Verbeux : callback memoization manuelle
6 const handleClick = useCallback(() => {
7 onUpdate(data.id);
8 }, [onUpdate, data.id]);
9
10 // ❌ Verbeux : calcul memoizé manuellement
11 const processedData = useMemo(() => {
12 return data.items.map(item => ({
13 ...item,
14 computed: expensiveCalculation(item)
15 }));
16 }, [data.items]);
17
18 // ❌ Verbeux : sous-composant memoizé
19 const ListItem = memo(({ item }: { item: Item }) => (
20 <div>{item.name}</div>
21 ));
22
23 return (
24 <div onClick={handleClick}>
25 {processedData.map(item => (
26 <ListItem key={item.id} item={item} />
27 ))}
28 </div>
29 );
30}
31
32export default memo(ExpensiveComponent);
components/expensive-component-new.tsxtsx
1// Après : React 19 avec Compiler (automatique)
2function ExpensiveComponent({ data, onUpdate }: Props) {
3 // ✅ Le Compiler gère la memoization automatiquement
4 const handleClick = () => {
5 onUpdate(data.id);
6 };
7
8 // ✅ Memoization automatique du calcul
9 const processedData = data.items.map(item => ({
10 ...item,
11 computed: expensiveCalculation(item)
12 }));
13
14 // ✅ Pas besoin de memo() pour les sous-composants
15 function ListItem({ item }: { item: Item }) {
16 return <div>{item.name}</div>;
17 }
18
19 return (
20 <div onClick={handleClick}>
21 {processedData.map(item => (
22 <ListItem key={item.id} item={item} />
23 ))}
24 </div>
25 );
26}
27
28// ✅ Pas besoin de memo() wrapper
29export default ExpensiveComponent;
React 18 Manuel
useMemo/useCallback explicites
Avantages
  • Contrôle total
  • Prévisible
Inconvenients
  • Verbeux
  • Oublis fréquents
  • Dépendances incorrectes
Cas d'usage
  • Legacy code
  • Cas très spécifiques
React Compiler (19)
Optimisation automatique build-time
Avantages
  • Zéro boilerplate
  • Pas d'oublis
  • Fine-grained memoization
Inconvenients
  • Moins de contrôle
  • Debugging plus complexe
Cas d'usage
  • Par défaut
  • Nouvelle codebase
  • Migration progressive

Quand Garder useMemo/useCallback ?

Le Compiler ne remplace pas 100% des cas. Voici quand continuer à utiliser les hooks manuels.

Compiler Gère (95% des cas)

  • • Callbacks simples passés en props
  • • Calculs dérivés de state/props
  • • Rendu conditionnel
  • • Listes et maps

Garder useMemo/useCallback (5% des cas)

  • • Calculs extrêmement coûteux (regex complexes, parsing massif)
  • • Référence d'identité critique (WeakMap keys, refs)
  • • Logique avec side-effects intentionnels
  • • Performance profiling indique un besoin explicite
components/data-processor.tsxtsx
1// Cas où useMemo reste pertinent
2import { useMemo } from 'react';
3
4function DataProcessor({ rawData }: Props) {
5 // ✅ Calcul VRAIMENT coûteux : garder useMemo
6 const parsedData = useMemo(() => {
7 // Parsing de 10MB de JSON + transformations lourdes
8 const parsed = JSON.parse(rawData);
9 return complexTransformation(parsed); // 100ms+
10 }, [rawData]);
11
12 // ✅ WeakMap nécessite référence stable
13 const cache = useMemo(() => new WeakMap(), []);
14
15 // ❌ Calcul simple : laisser le Compiler gérer
16 const count = data.length; // Pas besoin de useMemo ici
17
18 return <div>{parsedData.summary}</div>;
19}

Migration Progressive

Stratégie pour adopter le Compiler sur une codebase existante.

1
Activer en opt-in : Commencer par les nouveaux composants uniquement
2
Monitoring : Utiliser React DevTools Profiler pour comparer avant/après
3
Supprimer progressivement : Retirer useMemo/useCallback une fois le Compiler confirmé actif
4
Opt-out si besoin : Utiliser "use no memo" directive pour désactiver sur un composant spécifique
components/legacy-component.tsxtsx
1// Opt-out du Compiler pour un composant spécifique
2'use no memo'; // Directive spéciale
3
4function LegacyComponent({ data }: Props) {
5 // Ce composant ne sera PAS optimisé par le Compiler
6 // Utile pour des composants avec logique complexe non-standard
7 const result = complexLegacyLogic(data);
8
9 return <div>{result}</div>;
10}

Recommandations Seniors

  • Activer le Compiler dès le début sur les nouveaux projets React 19
  • Supprimer les useMemo/useCallback inutiles une fois le Compiler actif
  • Profiler avant d'optimiser manuellement : le Compiler est souvent suffisant
  • Utiliser React DevTools Profiler pour valider les optimisations
  • Garder un œil sur la taille du bundle : le Compiler ajoute du runtime minimal
Modes de Rendu·Section 4/19

Quand utiliser les Server Components ?

Les React Server Components (RSC) sont la foundation de React 19. Ils permettent un rendu serveur sans envoyer de JavaScript au client, réduisant drastiquement la taille du bundle et améliorant les performances initiales.

Server Components : Zero-Bundle React

Les Server Components ne sont jamais envoyés au client - seulement leur output HTML.

  • Zéro JavaScript client : Le composant n'est jamais téléchargé au navigateur
  • Accès direct aux ressources : Database, filesystem, secrets sans exposition
  • Streaming automatique : Progressive rendering avec Suspense
  • Async par défaut : Composants peuvent être async functions
app/users/page.tsx (Server Component)tsx
1// Server Component (dans un framework supportant les RSC)
2import { prisma } from '@/lib/db';
3
4// ✅ Async component - SEULEMENT possible en Server Component
5export default async function UserList() {
6 // ✅ Accès direct à la DB - Pas besoin d'API route
7 const users = await prisma.user.findMany({
8 select: { id: true, name: true, email: true }
9 });
10
11 // ✅ Ce code ne sera JAMAIS envoyé au client
12 const API_SECRET = process.env.SECRET_KEY; // Sécurisé !
13
14 return (
15 <div>
16 <h1>Users ({users.length})</h1>
17 <ul>
18 {users.map(user => (
19 <li key={user.id}>{user.name} - {user.email}</li>
20 ))}
21 </ul>
22 </div>
23 );
24}
25
26// Caractéristiques :
27// - Pas de 'use client'
28// - Peut être async
29// - Accès direct DB/filesystem
30// - Pas de useState, useEffect, onClick, etc.
31// - Zéro JS envoyé au client
components/counter.tsx (Client Component)tsx
1// Client Component - Pour l'interactivité
2'use client'; // ⚠️ Directive obligatoire
3
4import { useState } from 'react';
5
6export function Counter() {
7 // ✅ Hooks autorisés dans Client Components
8 const [count, setCount] = useState(0);
9
10 return (
11 <div>
12 <p>Count: {count}</p>
13 {/* ✅ Event handlers autorisés */}
14 <button onClick={() => setCount(count + 1)}>
15 Increment
16 </button>
17 </div>
18 );
19}
20
21// Caractéristiques :
22// - Directive 'use client' en haut du fichier
23// - Tous les hooks React disponibles
24// - Event handlers (onClick, onChange, etc.)
25// - Browser APIs (window, document, localStorage)
26// - Ce composant + ses dépendances = envoyés au client
Server Components
Rendu 100% serveur, zéro JS client
Avantages
  • Bundle size réduit
  • Accès direct DB/API
  • SEO optimal
  • Secrets sécurisés
Inconvenients
  • Pas d'interactivité
  • Pas de hooks useState/useEffect
  • Pas de browser APIs
Cas d'usage
  • Layouts
  • Fetch de données
  • Contenu statique
  • Marketing pages
Client Components
Rendu client avec hydration
Avantages
  • Interactivité
  • Hooks complets
  • Browser APIs
  • Real-time updates
Inconvenients
  • JavaScript au client
  • Bundle size++
  • Hydration overhead
Cas d'usage
  • Forms
  • Modals
  • Interactivity
  • Client state

Composition Server + Client

La vraie puissance : combiner Server et Client Components intelligemment.

Pattern 1 : Server Component avec Client enfants

Un Server Component peut importer et rendre des Client Components. Le Server fait le fetch, le Client gère l'interactivité.

Pattern 2 : Client Component avec Server children via props

Un Client Component peut recevoir des Server Components en children/props, permettant l'interactivité autour de contenu serveur.

app/post/[id]/page.tsxtsx
1// Pattern : Server Component parent avec Client Component enfant
2import { prisma } from '@/lib/db';
3import { LikeButton } from '@/components/like-button'; // Client Component
4
5// ✅ Server Component (async)
6export default async function PostPage({ params }: { params: { id: string } }) {
7 // Fetch côté serveur
8 const post = await prisma.post.findUnique({
9 where: { id: params.id },
10 include: { author: true, likes: true }
11 });
12
13 return (
14 <article>
15 <h1>{post.title}</h1>
16 <p>Par {post.author.name}</p>
17 <div>{post.content}</div>
18
19 {/* ✅ Client Component pour l'interactivité */}
20 <LikeButton postId={post.id} initialLikes={post.likes.length} />
21 </article>
22 );
23}
24
25// components/like-button.tsx (Client)
26'use client';
27import { useState } from 'react';
28
29export function LikeButton({ postId, initialLikes }: Props) {
30 const [likes, setLikes] = useState(initialLikes);
31
32 return (
33 <button onClick={() => setLikes(likes + 1)}>
34 ❤️ {likes} likes
35 </button>
36 );
37}
Composition avancéetsx
1// Pattern Avancé : Client wrapper avec Server children
2// components/tabs.tsx (Client Component)
3'use client';
4import { useState, ReactNode } from 'react';
5
6export function Tabs({ children }: { children: ReactNode }) {
7 const [activeTab, setActiveTab] = useState(0);
8
9 return (
10 <div>
11 {/* Interactivité gérée ici */}
12 <div className="tabs">
13 <button onClick={() => setActiveTab(0)}>Tab 1</button>
14 <button onClick={() => setActiveTab(1)}>Tab 2</button>
15 </div>
16
17 {/* ✅ children peut être un Server Component ! */}
18 <div className="tab-content">{children}</div>
19 </div>
20 );
21}
22
23// app/dashboard/page.tsx (Server Component)
24import { Tabs } from '@/components/tabs';
25import { prisma } from '@/lib/db';
26
27export default async function Dashboard() {
28 const data = await prisma.metrics.findMany();
29
30 return (
31 <Tabs>
32 {/* ✅ Contenu Server Component dans Client wrapper */}
33 <div>
34 {data.map(metric => (
35 <div key={metric.id}>{metric.value}</div>
36 ))}
37 </div>
38 </Tabs>
39 );
40}

Règles des Server Components

Ce que vous POUVEZ et NE POUVEZ PAS faire dans un Server Component.

✅ Autorisé

  • • async/await
  • • Direct DB access
  • • File system (fs)
  • • process.env secrets
  • • Imports de librairies serveur
  • • Rendering de Client Components

❌ Interdit

  • • useState, useEffect, useContext
  • • Event handlers (onClick, onChange)
  • • Browser APIs (window, document)
  • • createContext (côté serveur)
  • • Lifecycle hooks
Anti-patterns et correctionstsx
1// Anti-patterns courants
2
3// ❌ ERREUR : useState dans Server Component
4export default async function Page() {
5 const [count, setCount] = useState(0); // ❌ ERREUR !
6 return <div>{count}</div>;
7}
8
9// ❌ ERREUR : onClick dans Server Component
10export default async function Page() {
11 return (
12 <button onClick={() => console.log('click')}> // ❌ ERREUR !
13 Click me
14 </button>
15 );
16}
17
18// ❌ ERREUR : Importer Server Component dans Client Component
19'use client';
20import { ServerData } from './server-data'; // ❌ Server Component !
21
22export function ClientWrapper() {
23 return <ServerData />; // ❌ Ne fonctionnera pas
24}
25
26// ✅ CORRECT : Passer en children
27export function ClientWrapper({ children }: { children: ReactNode }) {
28 return <div className="wrapper">{children}</div>;
29}
30
31// app/page.tsx
32import { ClientWrapper } from './client-wrapper';
33import { ServerData } from './server-data';
34
35export default function Page() {
36 return (
37 <ClientWrapper>
38 <ServerData /> {/* ✅ CORRECT */}
39 </ClientWrapper>
40 );
41}

Best Practices Seniors

  • Server Components par défaut : N'ajouter 'use client' que quand nécessaire
  • Data fetching au plus proche : Fetch dans le composant qui utilise les données (colocation)
  • Pas d'over-client : Ne pas mettre 'use client' sur un parent si seul l'enfant en a besoin
  • Composition intelligente : Utiliser children/props pour injecter Server dans Client
  • Streaming avec Suspense : Wrapper les Server Components lents avec Suspense
Modes de Rendu·Section 5/19

Comment gérer les actions async sans loading state ?

React 19 introduit les Actions - des fonctions async dans des transitions qui gèrent automatiquement les états pending, errors et optimistic updates. C'est une révolution pour les forms et mutations.

Actions & useTransition

Les Actions simplifient drastiquement la gestion des états asynchrones dans React.

  • Pending automatique : isPending géré par React pendant l'exécution
  • Error handling intégré : try/catch automatique avec Error Boundaries
  • Optimistic updates : UI update immédiat avec rollback auto si échec
  • Non-blocking UI : L'UI reste responsive pendant l'action
React 18 - Manueltsx
1// React 18 : Gestion manuelle du loading/error
2import { useState } from 'react';
3
4function FormOld() {
5 const [isPending, setIsPending] = useState(false);
6 const [error, setError] = useState<string | null>(null);
7
8 async function handleSubmit(e: React.FormEvent) {
9 e.preventDefault();
10 setIsPending(true);
11 setError(null);
12
13 try {
14 await submitForm(data);
15 } catch (err) {
16 setError(err.message);
17 } finally {
18 setIsPending(false);
19 }
20 }
21
22 return (
23 <form onSubmit={handleSubmit}>
24 {error && <div className="error">{error}</div>}
25 <button disabled={isPending}>
26 {isPending ? 'Envoi...' : 'Envoyer'}
27 </button>
28 </form>
29 );
30}
React 19 - Actionstsx
1// React 19 : Actions avec useTransition
2import { useTransition } from 'react';
3
4function FormNew() {
5 const [isPending, startTransition] = useTransition();
6
7 async function handleSubmit(e: React.FormEvent) {
8 e.preventDefault();
9
10 // ✅ startTransition accepte maintenant des fonctions async !
11 startTransition(async () => {
12 await submitForm(data);
13 // isPending = true pendant l'exécution
14 // Si une erreur est throw, Error Boundary la catch
15 });
16 }
17
18 return (
19 <form onSubmit={handleSubmit}>
20 <button disabled={isPending}>
21 {isPending ? 'Envoi...' : 'Envoyer'}
22 </button>
23 </form>
24 );
25}
components/login-form.tsxtsx
1// Pattern : Form Action avec validation
2import { useTransition } from 'react';
3import { z } from 'zod';
4
5const schema = z.object({
6 email: z.string().email(),
7 password: z.string().min(8)
8});
9
10export function LoginForm() {
11 const [isPending, startTransition] = useTransition();
12
13 async function handleLogin(formData: FormData) {
14 startTransition(async () => {
15 // Validation
16 const data = schema.parse({
17 email: formData.get('email'),
18 password: formData.get('password')
19 });
20
21 // Appel API
22 const response = await fetch('/api/login', {
23 method: 'POST',
24 body: JSON.stringify(data)
25 });
26
27 if (!response.ok) {
28 throw new Error('Login failed');
29 }
30
31 // Redirect ou update UI
32 window.location.href = '/dashboard';
33 });
34 }
35
36 return (
37 <form action={handleLogin}>
38 <input name="email" type="email" required />
39 <input name="password" type="password" required />
40 <button disabled={isPending}>
41 {isPending ? 'Connexion...' : 'Se connecter'}
42 </button>
43 </form>
44 );
45}
components/search.tsxtsx
1// Pattern : useTransition pour navigation non-bloquante
2import { useTransition, useState } from 'react';
3
4function SearchResults() {
5 const [query, setQuery] = useState('');
6 const [results, setResults] = useState([]);
7 const [isPending, startTransition] = useTransition();
8
9 function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
10 const value = e.target.value;
11 setQuery(value); // ✅ Update immédiat (UI responsive)
12
13 // ✅ Recherche en transition (non-bloquant)
14 startTransition(async () => {
15 const data = await searchAPI(value);
16 setResults(data);
17 });
18 }
19
20 return (
21 <div>
22 <input
23 value={query}
24 onChange={handleSearch}
25 placeholder="Rechercher..."
26 />
27
28 {/* Visual feedback pendant la recherche */}
29 {isPending && <span className="spinner"></span>}
30
31 <ul style={{ opacity: isPending ? 0.6 : 1 }}>
32 {results.map(item => (
33 <li key={item.id}>{item.title}</li>
34 ))}
35 </ul>
36 </div>
37 );
38}

Concurrent Rendering & Interruptibilité

React 19 active le concurrent rendering par défaut - les transitions peuvent être interrompues.

Avec le concurrent rendering, React peut interrompre une transition en cours si une mise à jour plus urgente arrive (ex: un clic utilisateur).

Exemple : Recherche interruptible

L'utilisateur tape "react" puis immédiatement "vue". React annule automatiquement la recherche "react" en cours et lance "vue" - pas besoin de debounce manuel !

components/dashboard.tsxtsx
1// Pattern avancé : Multiple transitions avec priorité
2import { useTransition } from 'react';
3
4function DataDashboard() {
5 const [urgentPending, startUrgentTransition] = useTransition();
6 const [backgroundPending, startBackgroundTransition] = useTransition();
7
8 function refreshData() {
9 // ✅ Transition urgente (haute priorité)
10 startUrgentTransition(async () => {
11 await fetchCriticalData();
12 });
13
14 // ✅ Transition background (basse priorité)
15 startBackgroundTransition(async () => {
16 await fetchAnalytics();
17 });
18
19 // Si l'utilisateur clique pendant ce temps,
20 // React peut interrompre backgroundTransition mais pas urgentTransition
21 }
22
23 return (
24 <div>
25 <button onClick={refreshData}>
26 Rafraîchir {urgentPending && '⏳'}
27 </button>
28 {backgroundPending && <div className="bg-loading">Chargement analytics...</div>}
29 </div>
30 );
31}

Recommandations Seniors

  • Utiliser useTransition pour toutes les mutations : Forms, updates, navigation
  • Combiner avec useOptimistic pour une UX instantanée (voir section suivante)
  • Error Boundaries obligatoires : Les erreurs dans les transitions sont catchées par Error Boundary
  • Éviter les transitions imbriquées : Complexité inutile, préférer une seule transition
Modes de Rendu·Section 6/19

Comment optimiser l'UX pendant les requêtes serveur ?

useActionState (anciennement useFormState) et useOptimisticsont les nouveaux hooks React 19 pour gérer les actions avec état et updates optimistes.

useActionState : Forms Simplifiés

Gérer l'état d'une action (form submission) avec pending, errors et résultat.

useActionState remplace useFormState et simplifie la gestion des forms avec validation serveur, états pending automatiques et intégration native avec les Server Actions.

components/profile-form.tsxtsx
1// useActionState pour form submission
2import { useActionState } from 'react';
3
4async function updateProfile(prevState: State, formData: FormData) {
5 'use server'; // Server Action
6
7 const name = formData.get('name') as string;
8
9 try {
10 await db.user.update({ data: { name } });
11 return { success: true, message: 'Profil mis à jour' };
12 } catch (error) {
13 return { success: false, error: 'Échec de la mise à jour' };
14 }
15}
16
17export function ProfileForm() {
18 const [state, action, isPending] = useActionState(updateProfile, {
19 success: false,
20 message: '',
21 error: ''
22 });
23
24 return (
25 <form action={action}>
26 {state.error && <div className="error">{state.error}</div>}
27 {state.success && <div className="success">{state.message}</div>}
28
29 <input name="name" required />
30 <button disabled={isPending}>
31 {isPending ? 'Envoi...' : 'Mettre à jour'}
32 </button>
33 </form>
34 );
35}

useOptimistic : UX Instantanée

Afficher immédiatement le résultat attendu avant la confirmation serveur.

useOptimistic permet d'update l'UI instantanément (optimistic update) puis de revenir en arrière automatiquement si l'action échoue.

components/like-button.tsxtsx
1// useOptimistic pour like instantané
2import { useOptimistic, useTransition } from 'react';
3
4export function LikeButton({ postId, initialLikes }: Props) {
5 const [likes, setLikes] = useState(initialLikes);
6 const [optimisticLikes, addOptimisticLike] = useOptimistic(
7 likes,
8 (current, amount: number) => current + amount
9 );
10 const [isPending, startTransition] = useTransition();
11
12 async function handleLike() {
13 // ✅ Update optimiste immédiate
14 addOptimisticLike(1);
15
16 // Appel serveur en arrière-plan
17 startTransition(async () => {
18 const newLikes = await likePost(postId);
19 setLikes(newLikes); // Update réel
20 // Si l'appel échoue, optimisticLikes revient automatiquement à 'likes'
21 });
22 }
23
24 return (
25 <button onClick={handleLike} disabled={isPending}>
26 ❤️ {optimisticLikes} {isPending && '⏳'}
27 </button>
28 );
29}
Modes de Rendu·Section 7/19

Comment accélérer le temps de chargement perçu ?

Le Streaming et le Partial Pre-rendering (React 19.2) permettent d'envoyer progressivement le HTML au client pour un Time-to-First-Byte (TTFB) optimal.

app/page.tsxtsx
1// Streaming avec Suspense
2import { Suspense } from 'react';
3
4export default function Page() {
5 return (
6 <div>
7 <h1>Ma Page</h1>
8
9 {/* Shell envoyé immédiatement */}
10 <Suspense fallback={<div>Chargement...</div>}>
11 <SlowComponent /> {/* Streamed progressivement */}
12 </Suspense>
13 </div>
14 );
15}
16
17async function SlowComponent() {
18 const data = await fetch('https://api.slow.com/data');
19 return <div>{data}</div>;
20}

Partial Pre-rendering (React 19.2)

Pré-render des parties statiques, streaming des parties dynamiques.

Partial Pre-rendering combine le meilleur du SSG (static) et du SSR (dynamic) : le shell statique est servi depuis le CDN, puis les parties dynamiques sont streamées.

Optimisations·Section 8/19

Comment réduire drastiquement la taille de votre bundle ?

L'optimisation du bundle est critique pour les performances. React 19 avec Server Components réduit drastiquement la taille du JavaScript côté client.

Imports optimiséstsx
1// Tree-shaking et imports sélectifs
2// ❌ Import tout
3import * as _ from 'lodash';
4
5// ✅ Import sélectif
6import { debounce } from 'lodash-es';
7
8// ✅ Encore mieux : alternatives lightweight
9import debounce from 'just-debounce-it'; // 200 bytes vs 70KB
Code splittingtsx
1// Code Splitting avec React.lazy()
2import React, { Suspense } from 'react';
3
4const HeavyChart = React.lazy(() => import('@/components/heavy-chart'));
5
6export function Dashboard() {
7 return (
8 <div>
9 <h1>Dashboard</h1>
10 <Suspense fallback={<p>Chargement du graphique...</p>}>
11 <HeavyChart /> {/* Chargé seulement quand nécessaire */}
12 </Suspense>
13 </div>
14 );
15}

Bundle Analysis

Analyser votre bundle pour identifier les gros modules.

Utiliser webpack-bundle-analyzer, source-map-explorer ou bundle-stats pour visualiser la taille de chaque module et identifier les opportunités d'optimisation.

Optimisations·Section 9/19

Quels hooks pour diagnostiquer les problèmes de perf ?

Avec le React Compiler, les hooks de performance (useMemo, useCallback, memo) deviennent optionnels dans 95% des cas. Focus sur les cas où ils restent pertinents.

useMemo
Memoize le résultat d'un calcul
Avantages
  • Évite recalculs coûteux
  • Contrôle explicite
Inconvenients
  • Boilerplate
  • Facile à mal utiliser
Cas d'usage
  • Calculs très coûteux (>50ms)
  • Cas non gérés par le Compiler
React Compiler
Memoization automatique
Avantages
  • Zéro boilerplate
  • Optimisations fines
  • Pas d'oublis
Inconvenients
  • Debugging complexe
  • Moins de contrôle
Cas d'usage
  • Par défaut dans React 19
Performance hookstsx
1// Quand GARDER useMemo (React 19)
2import { useMemo } from 'react';
3
4function DataProcessor({ largeDataset }: Props) {
5 // ✅ Calcul extrêmement coûteux : garder useMemo
6 const processed = useMemo(() => {
7 return largeDataset.map(item => {
8 // Traitement lourd : parsing, regex complexes, etc.
9 return heavyComputation(item); // >50ms
10 });
11 }, [largeDataset]);
12
13 // ❌ Calcul simple : laisser le Compiler gérer
14 const count = data.length; // Pas besoin de useMemo
15
16 return <div>{processed}</div>;
17}

React DevTools Profiler

Mesurer les performances réelles avant d'optimiser.

Toujours profiler avant d'ajouter useMemo/useCallback. Le Compiler gère déjà la plupart des optimisations - n'optimisez que si le Profiler indique un problème.

Optimisations·Section 10/19

Comment éviter les fuites mémoire dans React ?

Les fuites mémoire en React sont souvent causées par des event listeners non nettoyés, des timers actifs ou des subscriptions oubliées. React 19 n'y change rien : le cleanup reste essentiel.

Cleanup patternstsx
1// Pattern : Cleanup proper avec useEffect
2import { useEffect } from 'react';
3
4function LiveData() {
5 useEffect(() => {
6 // Setup : subscription
7 const subscription = dataSource.subscribe(handleData);
8
9 // ✅ Cleanup function : OBLIGATOIRE
10 return () => {
11 subscription.unsubscribe();
12 };
13 }, []);
14
15 return <div>Live Data</div>;
16}
17
18// ❌ Fuite mémoire : pas de cleanup
19function LiveDataBad() {
20 useEffect(() => {
21 const subscription = dataSource.subscribe(handleData);
22 // ❌ PAS de return = fuite mémoire
23 }, []);
24
25 return <div>Live Data</div>;
26}

Sources Courantes de Fuites Mémoire

Les erreurs les plus fréquentes qui causent des fuites.

  • • Event listeners non supprimés (addEventListener sans removeEventListener)
  • • Timers actifs (setTimeout/setInterval sans clear)
  • • Subscriptions non fermées (WebSocket, EventSource, RxJS)
  • • Références circulaires dans les closures
  • • State updates sur composants unmounted
Safe state updatestsx
1// Pattern : Safe state update (éviter warning unmount)
2import { useEffect, useRef } from 'react';
3
4function DataFetcher() {
5 const [data, setData] = useState(null);
6 const isMountedRef = useRef(true);
7
8 useEffect(() => {
9 isMountedRef.current = true;
10
11 async function fetchData() {
12 const result = await fetch('/api/data');
13
14 // ✅ Update seulement si le composant est toujours monté
15 if (isMountedRef.current) {
16 setData(result);
17 }
18 }
19
20 fetchData();
21
22 return () => {
23 isMountedRef.current = false; // Cleanup
24 };
25 }, []);
26
27 return <div>{data}</div>;
28}
Optimisations·Section 11/19

Quel pattern choisir pour fetcher vos données ?

React 19 change fondamentalement le data fetching : use() + Suspense remplace useEffect pour éviter les race conditions et simplifier le code.

Data fetching patternstsx
1// ❌ React 18 : useEffect (race conditions possibles)
2function UserProfile({ userId }: Props) {
3 const [user, setUser] = useState(null);
4 const [loading, setLoading] = useState(true);
5
6 useEffect(() => {
7 let cancelled = false;
8
9 async function loadUser() {
10 setLoading(true);
11 const data = await fetchUser(userId);
12
13 if (!cancelled) {
14 setUser(data);
15 setLoading(false);
16 }
17 }
18
19 loadUser();
20
21 return () => { cancelled = true; }; // Cleanup pour race condition
22 }, [userId]);
23
24 if (loading) return <div>Loading...</div>;
25 return <div>{user.name}</div>;
26}
27
28// ✅ React 19 : use() + Suspense (pas de race condition)
29import { use, Suspense } from 'react';
30
31function UserProfile({ userId }: Props) {
32 const userPromise = useMemo(() => fetchUser(userId), [userId]);
33 const user = use(userPromise); // Suspend automatiquement
34
35 return <div>{user.name}</div>; // Simple !
36}
37
38export function UserPage({ userId }: Props) {
39 return (
40 <Suspense fallback={<div>Loading...</div>}>
41 <UserProfile userId={userId} />
42 </Suspense>
43 );
44}

TanStack Query : La Solution Seniors

Pour des besoins avancés (caching, revalidation, mutations), TanStack Query reste indispensable.

TanStack Query (React Query) gère automatiquement le caching, la revalidation, les mutations optimistes, et bien plus. C'est la bibliothèque de référence pour le data fetching en production.

TanStack Query patterntsx
1// TanStack Query v5 avec React 19
2import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
3
4function UserProfile() {
5 const queryClient = useQueryClient();
6
7 // ✅ Fetch avec cache automatique
8 const { data: user, isLoading } = useQuery({
9 queryKey: ['user', userId],
10 queryFn: () => fetchUser(userId),
11 staleTime: 5 * 60 * 1000 // Cache 5 min
12 });
13
14 // ✅ Mutation avec invalidation auto
15 const updateMutation = useMutation({
16 mutationFn: updateUser,
17 onSuccess: () => {
18 // Invalide et refetch automatiquement
19 queryClient.invalidateQueries({ queryKey: ['user', userId] });
20 }
21 });
22
23 if (isLoading) return <div>Loading...</div>;
24
25 return (
26 <div>
27 <h1>{user.name}</h1>
28 <button onClick={() => updateMutation.mutate({ name: 'New Name' })}>
29 Update
30 </button>
31 </div>
32 );
33}
Optimisations·Section 12/19

Comment simplifier votre gestion d'état ?

Un pattern courant chez les developpeurs React : multiplier les appels a useState pour chaque variable d'etat. Cette approche semble intuitive, mais elle introduit des etats impossibles, des updates non-atomiques, et des bugs subtils. Consolider l'etat dans un objet unique ou utiliser useReducer resout ces problemes.

Pourquoi les useState eparpilles posent probleme

Des states separes pour des donnees liees creent une surface de bugs significative.

  • Etats impossibles : loading=true et submitted=true ne devraient jamais coexister, mais rien ne l'empeche
  • Updates non-atomiques : entre deux setState successifs, un render intermediaire peut survenir avec un etat incoherent
  • Maintenance complexe : ajouter un champ implique un nouveau useState, un nouveau setter, et des interactions a gerer
  • Tests fragiles : tester les combinaisons d'etats independants multiplie les cas de test
components/user-form.tsx (anti-pattern)tsx
1// Anti-pattern : useState eparpilles
2function UserForm() {
3 const [name, setName] = useState('');
4 const [email, setEmail] = useState('');
5 const [loading, setLoading] = useState(false);
6 const [error, setError] = useState<string | null>(null);
7 const [submitted, setSubmitted] = useState(false);
8 const [validating, setValidating] = useState(false);
9
10 async function handleSubmit() {
11 setValidating(false);
12 setLoading(true);
13 setError(null);
14 setSubmitted(false);
15 // Probleme : entre ces appels, un render peut afficher
16 // un etat incoherent (loading=true + submitted=true)
17
18 try {
19 await submitForm({ name, email });
20 setLoading(false);
21 setSubmitted(true);
22 // Bug potentiel : si un autre effet lit 'loading'
23 // entre ces deux lignes, il voit un etat impossible
24 } catch (err) {
25 setLoading(false);
26 setError((err as Error).message);
27 // Rien n'empeche submitted=true ET error d'etre defini
28 // si un render precedent a deja setSubmitted(true)
29 }
30 }
31
32 return (
33 <form onSubmit={handleSubmit}>
34 {loading && <Spinner />}
35 {error && <div className="error">{error}</div>}
36 {submitted && <div className="success">Envoye !</div>}
37 {/* 6 variables d'etat a coordonner manuellement */}
38 </form>
39 );
40}
components/user-form.tsx (consolide)tsx
1// Solution : etat consolide avec type discrimine
2interface FormState {
3 name: string;
4 email: string;
5 status: 'idle' | 'validating' | 'submitting' | 'success' | 'error';
6 error: string | null;
7}
8
9function UserForm() {
10 const [form, setForm] = useState<FormState>({
11 name: '',
12 email: '',
13 status: 'idle',
14 error: null,
15 });
16
17 async function handleSubmit() {
18 // Une seule update atomique : pas d'etat intermediaire incoherent
19 setForm(prev => ({ ...prev, status: 'submitting', error: null }));
20
21 try {
22 await submitForm({ name: form.name, email: form.email });
23 setForm(prev => ({ ...prev, status: 'success' }));
24 } catch (err) {
25 setForm(prev => ({
26 ...prev,
27 status: 'error',
28 error: (err as Error).message,
29 }));
30 }
31 }
32
33 return (
34 <form onSubmit={handleSubmit}>
35 {form.status === 'submitting' && <Spinner />}
36 {form.status === 'error' && <div className="error">{form.error}</div>}
37 {form.status === 'success' && <div className="success">Envoye !</div>}
38 {/* Le status discrimine rend les etats impossibles... impossibles */}
39 </form>
40 );
41}

Quand passer a useReducer

Pour les transitions d'etat complexes avec plusieurs actions, useReducer rend la logique explicite et testable.

  • useState consolide : suffisant quand les transitions sont simples (2-4 champs lies, logique lineaire)
  • useReducer : preferable quand les transitions sont nombreuses, ont des regles metier, ou doivent etre testees unitairement
  • Avantage cle : le reducer est une fonction pure, testable sans React, avec des transitions explicites
hooks/use-form-reducer.tstsx
1// useReducer : transitions explicites et testables
2type FormAction =
3 | { type: 'SET_FIELD'; field: keyof FormFields; value: string }
4 | { type: 'SUBMIT_START' }
5 | { type: 'SUBMIT_SUCCESS' }
6 | { type: 'SUBMIT_ERROR'; error: string }
7 | { type: 'RESET' };
8
9interface FormFields {
10 name: string;
11 email: string;
12}
13
14interface FormState extends FormFields {
15 status: 'idle' | 'submitting' | 'success' | 'error';
16 error: string | null;
17}
18
19function formReducer(state: FormState, action: FormAction): FormState {
20 switch (action.type) {
21 case 'SET_FIELD':
22 return { ...state, [action.field]: action.value, status: 'idle' };
23 case 'SUBMIT_START':
24 return { ...state, status: 'submitting', error: null };
25 case 'SUBMIT_SUCCESS':
26 return { ...state, status: 'success' };
27 case 'SUBMIT_ERROR':
28 return { ...state, status: 'error', error: action.error };
29 case 'RESET':
30 return { name: '', email: '', status: 'idle', error: null };
31 }
32}
33
34// Chaque transition est explicite et documentee par le type
35function UserForm() {
36 const [state, dispatch] = useReducer(formReducer, {
37 name: '', email: '', status: 'idle', error: null,
38 });
39
40 async function handleSubmit() {
41 dispatch({ type: 'SUBMIT_START' });
42 try {
43 await submitForm({ name: state.name, email: state.email });
44 dispatch({ type: 'SUBMIT_SUCCESS' });
45 } catch (err) {
46 dispatch({ type: 'SUBMIT_ERROR', error: (err as Error).message });
47 }
48 }
49
50 return (
51 <form onSubmit={handleSubmit}>
52 <input
53 value={state.name}
54 onChange={e => dispatch({ type: 'SET_FIELD', field: 'name', value: e.target.value })}
55 />
56 {/* Chaque action est typee et auto-documentee */}
57 </form>
58 );
59}

Recommandations

Choisir la bonne approche selon la complexite de l'etat.

  • 2-3 etats lies (ex: data + loading + error) : consolider dans un seul useState avec un objet type
  • Transitions complexes avec plusieurs actions possibles : useReducer avec discriminated union
  • Etats independants (ex: isOpen d'un modal + query de recherche) : garder des useState separes
  • React 19 : le React Compiler optimise le batching, mais les etats impossibles restent un probleme de conception, pas de performance
Bonnes Pratiques·Section 13/19

Comment structurer une app React qui scale ?

Une architecture scalable est essentielle pour les projets React modernes. React 19 avec Server Components pousse vers une architecture feature-based plutôt que layer-based.

Structure feature-basedtext
1// Architecture Feature-Based (recommandée)
2app/
3├── (features)/
4│ ├── auth/
5│ │ ├── components/
6│ │ │ ├── login-form.tsx
7│ │ │ └── signup-form.tsx
8│ │ ├── hooks/
9│ │ │ └── use-auth.ts
10│ │ ├── actions/
11│ │ │ └── auth-actions.ts
12│ │ └── types/
13│ │ └── auth.types.ts
14│ ├── products/
15│ │ ├── components/
16│ │ ├── hooks/
17│ │ └── actions/
18│ └── checkout/
19│ └── ...
20├── shared/
21│ ├── components/
22│ ├── hooks/
23│ └── utils/
24└── lib/
25 └── db.ts

Principes d'Architecture Seniors

Les règles essentielles pour une codebase maintenable.

  • Colocation : Garder le code lié ensemble (feature folders)
  • Separation of Concerns : Server Components (data) vs Client Components (UI)
  • Composition over Inheritance : Privilégier la composition de composants
  • Single Responsibility : Un composant = une responsabilité
  • Domain-Driven Design : Organiser par domaine métier, pas par type technique
Bonnes Pratiques·Section 14/19

Comment gérer les erreurs gracieusement ?

React 19 améliore les Error Boundaries avec un meilleur handling des erreurs async et moins de duplication.

components/error-boundary.tsxtsx
1// Error Boundary moderne (React 19)
2'use client';
3import { Component, ReactNode } from 'react';
4
5interface Props {
6 children: ReactNode;
7 fallback: ReactNode;
8}
9
10interface State {
11 hasError: boolean;
12 error?: Error;
13}
14
15export class ErrorBoundary extends Component<Props, State> {
16 constructor(props: Props) {
17 super(props);
18 this.state = { hasError: false };
19 }
20
21 static getDerivedStateFromError(error: Error): State {
22 return { hasError: true, error };
23 }
24
25 componentDidCatch(error: Error, info: React.ErrorInfo) {
26 // Log vers service de monitoring (Sentry, etc.)
27 console.error('Error caught:', error, info);
28 }
29
30 render() {
31 if (this.state.hasError) {
32 return this.props.fallback;
33 }
34
35 return this.props.children;
36 }
37}

Error Handling avec Actions

Les Actions throws sont automatiquement catchées par Error Boundaries.

Quand une Action (dans useTransition) throw une erreur, React la propage vers l'Error Boundary le plus proche. Pas besoin de try/catch manuel dans le composant.

Bonnes Pratiques·Section 15/19

Quels patterns TypeScript pour React 19 ?

React 19 avec TypeScript 5+ permet des patterns avancés pour une type-safety maximale.

Generic patternstsx
1// Generic Component Pattern
2import { ReactNode } from 'react';
3
4interface ListProps<T> {
5 items: T[];
6 renderItem: (item: T) => ReactNode;
7 keyExtractor: (item: T) => string;
8}
9
10function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
11 return (
12 <ul>
13 {items.map(item => (
14 <li key={keyExtractor(item)}>
15 {renderItem(item)}
16 </li>
17 ))}
18 </ul>
19 );
20}
21
22// Usage avec inférence automatique
23function UserList() {
24 const users: User[] = [...];
25
26 return (
27 <List
28 items={users} // T = User inféré automatiquement
29 renderItem={user => <div>{user.name}</div>}
30 keyExtractor={user => user.id}
31 />
32 );
33}

Patterns TypeScript Seniors

Les techniques avancées pour un code type-safe.

  • Generics : Composants réutilisables avec type safety
  • Discriminated Unions : État avec types exclusifs
  • as const : Narrowing automatique des types
  • Type Guards : Validation runtime + types
  • Zod : Validation avec inférence TypeScript
Bonnes Pratiques·Section 16/19

Comment tester efficacement vos Server Components ?

Une stratégie de tests complète combine unit tests (Vitest/Jest),integration tests (React Testing Library) et E2E tests (Playwright).

hooks/use-counter.test.tstsx
1// Unit Test : Hook personnalisé
2import { renderHook, act } from '@testing-library/react';
3import { useCounter } from './use-counter';
4
5describe('useCounter', () => {
6 it('should increment counter', () => {
7 const { result } = renderHook(() => useCounter());
8
9 expect(result.current.count).toBe(0);
10
11 act(() => {
12 result.current.increment();
13 });
14
15 expect(result.current.count).toBe(1);
16 });
17});
components/login-form.test.tsxtsx
1// Integration Test : Composant
2import { render, screen, fireEvent } from '@testing-library/react';
3import { LoginForm } from './login-form';
4
5describe('LoginForm', () => {
6 it('should submit form with credentials', async () => {
7 const onSubmit = vi.fn();
8 render(<LoginForm onSubmit={onSubmit} />);
9
10 // User interaction
11 fireEvent.change(screen.getByLabelText('Email'), {
12 target: { value: 'test@example.com' }
13 });
14 fireEvent.change(screen.getByLabelText('Password'), {
15 target: { value: 'password123' }
16 });
17 fireEvent.click(screen.getByRole('button', { name: 'Login' }));
18
19 // Assertions
20 await waitFor(() => {
21 expect(onSubmit).toHaveBeenCalledWith({
22 email: 'test@example.com',
23 password: 'password123'
24 });
25 });
26 });
27});

E2E Testing avec Playwright

Tests end-to-end pour valider le flow complet utilisateur.

Playwright permet de tester les flows critiques dans un navigateur réel. Idéal pour valider les parcours utilisateur complets (signup, checkout, etc.).

Bonnes Pratiques·Section 17/19

Comment garantir l'accessibilité de votre app ?

L'accessibilité (a11y) n'est pas optionnelle. WCAG 2.2 Level AA est le standard minimum pour les applications modernes.

Accessibilité Essentielle

Les règles fondamentales pour rendre votre app accessible.

  • Contraste : Minimum 4.5:1 pour le texte normal, 3:1 pour le large
  • Navigation clavier : Tab, Enter, Espace doivent fonctionner partout
  • ARIA labels : Décrire les éléments non-textuels
  • Focus visible : Toujours afficher un focus outline
  • Alt text : Descriptions pour toutes les images
Accessibility patternstsx
1// Composant accessible
2function AccessibleButton({ onClick, children, ariaLabel }: Props) {
3 return (
4 <button
5 onClick={onClick}
6 aria-label={ariaLabel}
7 className="focus:ring-2 focus:ring-primary focus:outline-none"
8 >
9 {children}
10 </button>
11 );
12}
13
14// Dialog accessible avec focus trap
15import { Dialog } from '@headlessui/react';
16
17function Modal({ isOpen, onClose }: Props) {
18 return (
19 <Dialog open={isOpen} onClose={onClose}>
20 <div className="fixed inset-0 bg-black/30" aria-hidden="true" />
21
22 <Dialog.Panel>
23 <Dialog.Title>Mon Dialog</Dialog.Title>
24 <Dialog.Description>
25 Description accessible du dialog
26 </Dialog.Description>
27
28 <button onClick={onClose}>Fermer</button>
29 </Dialog.Panel>
30 </Dialog>
31 );
32}
Avance·Section 18/19

Quels patterns pour des hooks réutilisables ?

Les custom hooks sont l'outil principal de réutilisation en React. Bien conçus, ils encapsulent la logique complexe et améliorent la testabilité.

hooks/use-fetch.tstsx
1// Pattern : Custom Hook avec TypeScript
2import { useState, useEffect } from 'react';
3
4interface UseFetchResult<T> {
5 data: T | null;
6 loading: boolean;
7 error: Error | null;
8 refetch: () => void;
9}
10
11function useFetch<T>(url: string): UseFetchResult<T> {
12 const [data, setData] = useState<T | null>(null);
13 const [loading, setLoading] = useState(true);
14 const [error, setError] = useState<Error | null>(null);
15 const [refetchIndex, setRefetchIndex] = useState(0);
16
17 useEffect(() => {
18 let cancelled = false;
19
20 async function fetchData() {
21 setLoading(true);
22 setError(null);
23
24 try {
25 const response = await fetch(url);
26 const json = await response.json();
27
28 if (!cancelled) {
29 setData(json);
30 }
31 } catch (err) {
32 if (!cancelled) {
33 setError(err as Error);
34 }
35 } finally {
36 if (!cancelled) {
37 setLoading(false);
38 }
39 }
40 }
41
42 fetchData();
43
44 return () => {
45 cancelled = true;
46 };
47 }, [url, refetchIndex]);
48
49 const refetch = () => setRefetchIndex(i => i + 1);
50
51 return { data, loading, error, refetch };
52}
53
54// Usage avec inférence de type
55function UserProfile() {
56 const { data: user, loading, error } = useFetch<User>('/api/user');
57
58 if (loading) return <div>Loading...</div>;
59 if (error) return <div>Error: {error.message}</div>;
60
61 return <div>{user?.name}</div>;
62}

Best Practices pour Custom Hooks

Les règles d'or pour créer des hooks réutilisables et maintenables.

  • Nommage : Toujours préfixer avec "use" (useMyHook)
  • Single Responsibility : Un hook = une responsabilité claire
  • TypeScript Generics : Pour la réutilisabilité avec type safety
  • Cleanup : Toujours nettoyer les effets (return dans useEffect)
  • Composition : Combiner des hooks simples plutôt qu'un hook complexe
Composition de hookstsx
1// Pattern : Hook composé (composition de hooks)
2import { useState, useEffect } from 'react';
3
4function useLocalStorage<T>(key: string, initialValue: T) {
5 const [value, setValue] = useState<T>(() => {
6 const stored = localStorage.getItem(key);
7 return stored ? JSON.parse(stored) : initialValue;
8 });
9
10 useEffect(() => {
11 localStorage.setItem(key, JSON.stringify(value));
12 }, [key, value]);
13
14 return [value, setValue] as const;
15}
16
17function useDebounce<T>(value: T, delay: number): T {
18 const [debouncedValue, setDebouncedValue] = useState(value);
19
20 useEffect(() => {
21 const timer = setTimeout(() => setDebouncedValue(value), delay);
22 return () => clearTimeout(timer);
23 }, [value, delay]);
24
25 return debouncedValue;
26}
27
28// Composition : Combiner plusieurs hooks
29function useSearchWithHistory(initialQuery = '') {
30 const [query, setQuery] = useLocalStorage('searchQuery', initialQuery);
31 const debouncedQuery = useDebounce(query, 300);
32 const { data, loading } = useFetch<Result[]>(`/api/search?q=${debouncedQuery}`);
33
34 return { query, setQuery, results: data, loading };
35}
Avance·Section 19/19

Comment gérer les refs et metadata dans React 19 ?

React 19 simplifie les refs avec refs as props (plus besoin de forwardRef) et permet de gérer les metadata documents (title, meta) directement dans les composants.

Refs as propstsx
1// React 18 : forwardRef obligatoire
2import { forwardRef } from 'react';
3
4const Input = forwardRef<HTMLInputElement, InputProps>(
5 (props, ref) => {
6 return <input ref={ref} {...props} />;
7 }
8);
9
10// React 19 : ref directement en prop
11function Input({ ref, ...props }: InputProps) {
12 return <input ref={ref} {...props} />;
13}
14
15// Usage identique
16function Form() {
17 const inputRef = useRef<HTMLInputElement>(null);
18
19 return <Input ref={inputRef} placeholder="Email" />;
20}

Document Metadata dans React 19

Gérer title, meta et link directement dans vos composants.

React 19 permet d'inclure les tags <title>, <meta> et <link> directement dans vos composants. React les hoistera automatiquement dans le <head> du document.

Document metadatatsx
1// Document Metadata directement dans le composant
2export function BlogPost({ post }: Props) {
3 return (
4 <article>
5 {/* ✅ React 19 : Metadata dans le composant */}
6 <title>{post.title} - Mon Blog</title>
7 <meta name="description" content={post.excerpt} />
8 <meta property="og:title" content={post.title} />
9 <meta property="og:image" content={post.coverImage} />
10 <link rel="canonical" href={`https://blog.com/${post.slug}`} />
11
12 {/* Contenu de la page */}
13 <h1>{post.title}</h1>
14 <div>{post.content}</div>
15 </article>
16 );
17}
18
19// React hoiste automatiquement les tags dans <head>
20// Résultat HTML :
21// <head>
22// <title>Mon Article - Mon Blog</title>
23// <meta name="description" content="..." />
24// ...
25// </head>
26// <body>
27// <article>
28// <h1>Mon Article</h1>
29// ...
30// </article>
31// </body>
app/products/[id]/page.tsxtsx
1// Pattern : Metadata dynamique avec Server Component
2export default async function ProductPage({ params }: Props) {
3 const product = await fetchProduct(params.id);
4
5 return (
6 <div>
7 {/* Metadata SEO dynamique */}
8 <title>{product.name} - Shop</title>
9 <meta name="description" content={product.description} />
10 <meta property="og:title" content={product.name} />
11 <meta property="og:image" content={product.image} />
12 <meta property="product:price:amount" content={product.price} />
13 <link rel="canonical" href={`https://shop.com/products/${product.slug}`} />
14
15 <h1>{product.name}</h1>
16 <p>{product.description}</p>
17 <span>{product.price}</span>
18 </div>
19 );
20}

Avantages des Metadata React 19

Pourquoi gérer les metadata directement dans les composants.

  • Colocation : Metadata proche du contenu concerné
  • Type-safety : TypeScript valide les props des meta tags
  • Composition : Metadata héritée et surchargeable
  • SSR natif : Fonctionne automatiquement avec Server Components

Felicitations !

Vous avez termine ce guide.