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)
1// React 18 vs React 19 - Comparaison2
3// ❌ React 18 - Verbeux avec forwardRef et useMemo4import { 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 props25function Button({ ref, onClick, children }: ButtonProps) {26 // Le React Compiler gère automatiquement la memoization27 const handleClick = () => onClick();28 const computedValue = expensiveCalculation();29
30 return (31 <button ref={ref} onClick={handleClick}>32 {children}33 </button>34 );35}1// Migration Step-by-Step2
3// 1. Mettre à jour les dépendances4{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// ❌ Ancien13import ReactDOM from 'react-dom';14ReactDOM.render(<App />, document.getElementById('root'));15
16// ✅ Nouveau17import { createRoot } from 'react-dom/client';18const root = createRoot(document.getElementById('root')!);19root.render(<App />);20
21// 3. Supprimer les forwardRef inutiles22// ❌ Avant23const Input = forwardRef<HTMLInputElement, InputProps>(24 (props, ref) => <input ref={ref} {...props} />25);26
27// ✅ Après28function Input({ ref, ...props }: InputProps) {29 return <input ref={ref} {...props} />;30}31
32// 4. Renommer useFormState en useActionState33// ❌ Avant34import { useFormState } from 'react-dom';35
36// ✅ Après37import { useActionState } from 'react';38
39// 5. Activer le React Compiler (optionnel mais recommandé)40// babel.config.js41module.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.
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
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ésolution9 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 obligatoire20export function UserProfilePage() {21 return (22 <Suspense fallback={<div>Chargement...</div>}>23 <UserProfile />24 </Suspense>25 );26}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 return7 if (variant === 'unstyled') {8 return <button>Click me</button>;9 }10
11 // Lecture du context seulement si nécessaire12 const theme = use(ThemeContext);13
14 return (15 <button style={{ backgroundColor: theme.primaryColor }}>16 Styled Button17 </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é conditionnellement28 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.
1// Pattern : Promise Stable avec useMemo2import { use, useMemo, Suspense } from 'react';3
4function UserDashboard({ userId }: { userId: string }) {5 // ✅ Promise stable grâce à useMemo6 const userPromise = useMemo(() => {7 return fetchUser(userId);8 }, [userId]); // Recréée seulement si userId change9
10 const user = use(userPromise);11
12 return <div>Welcome, {user.name}!</div>;13}14
15// ❌ Anti-pattern : Promise recréée à chaque render16function UserDashboardWrong({ userId }: { userId: string }) {17 // ❌ Nouvelle promise à chaque render = boucle infinie18 const userPromise = fetchUser(userId);19 const user = use(userPromise);20
21 return <div>Welcome, {user.name}!</div>;22}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 consommer6 const promises = useMemo(() => {7 const userPromise = fetchUser(userId);8 const postsPromise = fetchPosts(userId);9 return { userPromise, postsPromise };10 }, [userId]);11
12 // Consommer en parallèle13 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ésolu32 const postsPromise = useMemo(() => fetchPosts(userId), [userId]);33 const posts = use(postsPromise);34
35 return <div>...</div>;36}- Syntaxe simple et intuitive
- Intégration native avec Suspense
- Gestion automatique du loading
- Nécessite un boundary Suspense
- Promise doit être stable (pas recréée à chaque render)
- •Data fetching dans des composants
- •Chargement de ressources async
- •Lazy loading de données
- Permet les early returns
- Simplification du code
- Pas besoin de wrapper HOC
- Syntaxe différente de useContext
- Courbe d'apprentissage
- •Accès conditionnel au context
- •Branches conditionnelles
- •Simplification de logique complexe
Lire une Promise | Lire un Context |
|---|---|
Suspendre le rendu jusqu'à la résolution de la promise | Accéder au context de manière conditionnelle |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
1// Pattern : Error Handling avec use() et Error Boundary2import { 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'erreur7
8 return <div>{data.content}</div>;9}10
11export function DataPage() {12 const dataPromise = useMemo(() => fetchData(), []);13
14 return (15 <ErrorBoundary16 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)
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
1// Installation et Configuration2// 1. Installer le plugin Babel3npm install --save-dev babel-plugin-react-compiler4
5// 2. Configuration Babel6// babel.config.js7module.exports = {8 plugins: [9 ['babel-plugin-react-compiler', {10 target: '19', // Version de React11 runtimeModule: 'react/compiler-runtime'12 }]13 ]14};15
16// 3. Pour Vite17// vite.config.ts18import { 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});1// Avant : React 18 avec useMemo/useCallback manuel2import { useMemo, useCallback, memo } from 'react';3
4function ExpensiveComponent({ data, onUpdate }: Props) {5 // ❌ Verbeux : callback memoization manuelle6 const handleClick = useCallback(() => {7 onUpdate(data.id);8 }, [onUpdate, data.id]);9
10 // ❌ Verbeux : calcul memoizé manuellement11 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);1// Après : React 19 avec Compiler (automatique)2function ExpensiveComponent({ data, onUpdate }: Props) {3 // ✅ Le Compiler gère la memoization automatiquement4 const handleClick = () => {5 onUpdate(data.id);6 };7
8 // ✅ Memoization automatique du calcul9 const processedData = data.items.map(item => ({10 ...item,11 computed: expensiveCalculation(item)12 }));13
14 // ✅ Pas besoin de memo() pour les sous-composants15 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() wrapper29export default ExpensiveComponent;- Contrôle total
- Prévisible
- Verbeux
- Oublis fréquents
- Dépendances incorrectes
- •Legacy code
- •Cas très spécifiques
- Zéro boilerplate
- Pas d'oublis
- Fine-grained memoization
- Moins de contrôle
- Debugging plus complexe
- •Par défaut
- •Nouvelle codebase
- •Migration progressive
React 18 Manuel | React Compiler (19) |
|---|---|
useMemo/useCallback explicites | Optimisation automatique build-time |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
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
1// Cas où useMemo reste pertinent2import { useMemo } from 'react';3
4function DataProcessor({ rawData }: Props) {5 // ✅ Calcul VRAIMENT coûteux : garder useMemo6 const parsedData = useMemo(() => {7 // Parsing de 10MB de JSON + transformations lourdes8 const parsed = JSON.parse(rawData);9 return complexTransformation(parsed); // 100ms+10 }, [rawData]);11
12 // ✅ WeakMap nécessite référence stable13 const cache = useMemo(() => new WeakMap(), []);14
15 // ❌ Calcul simple : laisser le Compiler gérer16 const count = data.length; // Pas besoin de useMemo ici17
18 return <div>{parsedData.summary}</div>;19}Migration Progressive
Stratégie pour adopter le Compiler sur une codebase existante.
"use no memo" directive pour désactiver sur un composant spécifique1// Opt-out du Compiler pour un composant spécifique2'use no memo'; // Directive spéciale3
4function LegacyComponent({ data }: Props) {5 // Ce composant ne sera PAS optimisé par le Compiler6 // Utile pour des composants avec logique complexe non-standard7 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
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
1// Server Component (dans un framework supportant les RSC)2import { prisma } from '@/lib/db';3
4// ✅ Async component - SEULEMENT possible en Server Component5export default async function UserList() {6 // ✅ Accès direct à la DB - Pas besoin d'API route7 const users = await prisma.user.findMany({8 select: { id: true, name: true, email: true }9 });10
11 // ✅ Ce code ne sera JAMAIS envoyé au client12 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 async29// - Accès direct DB/filesystem30// - Pas de useState, useEffect, onClick, etc.31// - Zéro JS envoyé au client1// Client Component - Pour l'interactivité2'use client'; // ⚠️ Directive obligatoire3
4import { useState } from 'react';5
6export function Counter() {7 // ✅ Hooks autorisés dans Client Components8 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 Increment16 </button>17 </div>18 );19}20
21// Caractéristiques :22// - Directive 'use client' en haut du fichier23// - Tous les hooks React disponibles24// - Event handlers (onClick, onChange, etc.)25// - Browser APIs (window, document, localStorage)26// - Ce composant + ses dépendances = envoyés au client- Bundle size réduit
- Accès direct DB/API
- SEO optimal
- Secrets sécurisés
- Pas d'interactivité
- Pas de hooks useState/useEffect
- Pas de browser APIs
- •Layouts
- •Fetch de données
- •Contenu statique
- •Marketing pages
- Interactivité
- Hooks complets
- Browser APIs
- Real-time updates
- JavaScript au client
- Bundle size++
- Hydration overhead
- •Forms
- •Modals
- •Interactivity
- •Client state
Server Components | Client Components |
|---|---|
Rendu 100% serveur, zéro JS client | Rendu client avec hydration |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
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.
1// Pattern : Server Component parent avec Client Component enfant2import { prisma } from '@/lib/db';3import { LikeButton } from '@/components/like-button'; // Client Component4
5// ✅ Server Component (async)6export default async function PostPage({ params }: { params: { id: string } }) {7 // Fetch côté serveur8 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} likes35 </button>36 );37}1// Pattern Avancé : Client wrapper avec Server children2// 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
1// Anti-patterns courants2
3// ❌ ERREUR : useState dans Server Component4export default async function Page() {5 const [count, setCount] = useState(0); // ❌ ERREUR !6 return <div>{count}</div>;7}8
9// ❌ ERREUR : onClick dans Server Component10export default async function Page() {11 return (12 <button onClick={() => console.log('click')}> // ❌ ERREUR !13 Click me14 </button>15 );16}17
18// ❌ ERREUR : Importer Server Component dans Client Component19'use client';20import { ServerData } from './server-data'; // ❌ Server Component !21
22export function ClientWrapper() {23 return <ServerData />; // ❌ Ne fonctionnera pas24}25
26// ✅ CORRECT : Passer en children27export function ClientWrapper({ children }: { children: ReactNode }) {28 return <div className="wrapper">{children}</div>;29}30
31// app/page.tsx32import { 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
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
1// React 18 : Gestion manuelle du loading/error2import { 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}1// React 19 : Actions avec useTransition2import { 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écution14 // Si une erreur est throw, Error Boundary la catch15 });16 }17
18 return (19 <form onSubmit={handleSubmit}>20 <button disabled={isPending}>21 {isPending ? 'Envoi...' : 'Envoyer'}22 </button>23 </form>24 );25}1// Pattern : Form Action avec validation2import { 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 // Validation16 const data = schema.parse({17 email: formData.get('email'),18 password: formData.get('password')19 });20
21 // Appel API22 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 UI32 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}1// Pattern : useTransition pour navigation non-bloquante2import { 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 <input23 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 !
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 urgentTransition21 }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
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.
1// useActionState pour form submission2import { useActionState } from 'react';3
4async function updateProfile(prevState: State, formData: FormData) {5 'use server'; // Server Action6
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.
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 + amount9 );10 const [isPending, startTransition] = useTransition();11
12 async function handleLike() {13 // ✅ Update optimiste immédiate14 addOptimisticLike(1);15
16 // Appel serveur en arrière-plan17 startTransition(async () => {18 const newLikes = await likePost(postId);19 setLikes(newLikes); // Update réel20 // 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}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.
1// Streaming avec Suspense2import { 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.
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.
1// Tree-shaking et imports sélectifs2// ❌ Import tout3import * as _ from 'lodash';4
5// ✅ Import sélectif6import { debounce } from 'lodash-es';7
8// ✅ Encore mieux : alternatives lightweight9import debounce from 'just-debounce-it'; // 200 bytes vs 70KB1// 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.
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.
- Évite recalculs coûteux
- Contrôle explicite
- Boilerplate
- Facile à mal utiliser
- •Calculs très coûteux (>50ms)
- •Cas non gérés par le Compiler
- Zéro boilerplate
- Optimisations fines
- Pas d'oublis
- Debugging complexe
- Moins de contrôle
- •Par défaut dans React 19
useMemo | React Compiler |
|---|---|
Memoize le résultat d'un calcul | Memoization automatique |
Avantages
| Avantages
|
Inconvenients
| Inconvenients
|
Cas d'usage
| Cas d'usage
|
1// Quand GARDER useMemo (React 19)2import { useMemo } from 'react';3
4function DataProcessor({ largeDataset }: Props) {5 // ✅ Calcul extrêmement coûteux : garder useMemo6 const processed = useMemo(() => {7 return largeDataset.map(item => {8 // Traitement lourd : parsing, regex complexes, etc.9 return heavyComputation(item); // >50ms10 });11 }, [largeDataset]);12
13 // ❌ Calcul simple : laisser le Compiler gérer14 const count = data.length; // Pas besoin de useMemo15
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.
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.
1// Pattern : Cleanup proper avec useEffect2import { useEffect } from 'react';3
4function LiveData() {5 useEffect(() => {6 // Setup : subscription7 const subscription = dataSource.subscribe(handleData);8
9 // ✅ Cleanup function : OBLIGATOIRE10 return () => {11 subscription.unsubscribe();12 };13 }, []);14
15 return <div>Live Data</div>;16}17
18// ❌ Fuite mémoire : pas de cleanup19function LiveDataBad() {20 useEffect(() => {21 const subscription = dataSource.subscribe(handleData);22 // ❌ PAS de return = fuite mémoire23 }, []);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
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; // Cleanup24 };25 }, []);26
27 return <div>{data}</div>;28}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.
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 condition22 }, [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 automatiquement34
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.
1// TanStack Query v5 avec React 192import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';3
4function UserProfile() {5 const queryClient = useQueryClient();6
7 // ✅ Fetch avec cache automatique8 const { data: user, isLoading } = useQuery({9 queryKey: ['user', userId],10 queryFn: () => fetchUser(userId),11 staleTime: 5 * 60 * 1000 // Cache 5 min12 });13
14 // ✅ Mutation avec invalidation auto15 const updateMutation = useMutation({16 mutationFn: updateUser,17 onSuccess: () => {18 // Invalide et refetch automatiquement19 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 Update30 </button>31 </div>32 );33}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
1// Anti-pattern : useState eparpilles2function 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 afficher16 // 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 impossible24 } catch (err) {25 setLoading(false);26 setError((err as Error).message);27 // Rien n'empeche submitted=true ET error d'etre defini28 // 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}1// Solution : etat consolide avec type discrimine2interface 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 incoherent19 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
1// useReducer : transitions explicites et testables2type 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 type35function 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 <input53 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
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.
1// Architecture Feature-Based (recommandée)2app/3├── (features)/4│ ├── auth/5│ │ ├── components/6│ │ │ ├── login-form.tsx7│ │ │ └── signup-form.tsx8│ │ ├── hooks/9│ │ │ └── use-auth.ts10│ │ ├── actions/11│ │ │ └── auth-actions.ts12│ │ └── types/13│ │ └── auth.types.ts14│ ├── products/15│ │ ├── components/16│ │ ├── hooks/17│ │ └── actions/18│ └── checkout/19│ └── ...20├── shared/21│ ├── components/22│ ├── hooks/23│ └── utils/24└── lib/25 └── db.tsPrincipes 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
Comment gérer les erreurs gracieusement ?
React 19 améliore les Error Boundaries avec un meilleur handling des erreurs async et moins de duplication.
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.
Quels patterns TypeScript pour React 19 ?
React 19 avec TypeScript 5+ permet des patterns avancés pour une type-safety maximale.
1// Generic Component Pattern2import { 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 automatique23function UserList() {24 const users: User[] = [...];25
26 return (27 <List28 items={users} // T = User inféré automatiquement29 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
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).
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});1// Integration Test : Composant2import { 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 interaction11 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 // Assertions20 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.).
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
1// Composant accessible2function AccessibleButton({ onClick, children, ariaLabel }: Props) {3 return (4 <button5 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 trap15import { 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 dialog26 </Dialog.Description>27
28 <button onClick={onClose}>Fermer</button>29 </Dialog.Panel>30 </Dialog>31 );32}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é.
1// Pattern : Custom Hook avec TypeScript2import { 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 type55function 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
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 hooks29function 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}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.
1// React 18 : forwardRef obligatoire2import { 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 prop11function Input({ ref, ...props }: InputProps) {12 return <input ref={ref} {...props} />;13}14
15// Usage identique16function 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.
1// Document Metadata directement dans le composant2export 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>1// Pattern : Metadata dynamique avec Server Component2export 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.