7.2 — React
🎯 Objectif : écrire des composants React en confiance, choisir entre Server Components et Client Components, et connaître l’écosystème (Zustand, TanStack Query, React Hook Form, Zod, Vitest, Playwright).
À l'issue de cet axe, tu sauras :
- Maîtriser les hooks essentiels et savoir écrire un hook custom
- Distinguer Server Components et Client Components
- Utiliser Suspense pour gérer le chargement
- Manier les hooks de React 19 : useActionState, useOptimistic, useFormStatus
- Choisir entre useState, useReducer, Zustand, Redux Toolkit, Jotai
- Faire du data fetching avec TanStack Query
- Construire un formulaire typé avec React Hook Form + Zod
- Tester un composant avec Vitest + Testing Library
Débutant
Pourquoi React ?
Section intitulée « Pourquoi React ? »| Pour | Contre |
|---|---|
| Écosystème énorme, talents disponibles partout | Modèle mental complexe (re-render, hooks rules) |
| Server Components depuis 2023 — un saut générationnel | Souvent plus lourd que Svelte/Solid à perf égale |
| Maintien actif (Meta + open source) | Beaucoup de churn (RSC, hooks, server actions…) |
En 2026, React reste le défaut industriel. Pas le « meilleur » dans l’absolu, mais le plus sûr en termes de talents et écosystème.
JSX en 2 minutes
Section intitulée « JSX en 2 minutes »function Card({ title, children }) { return ( <article className="card"> <h2>{title}</h2> {children} </article> );}- Camel-case pour les attributs :
className(pasclass),htmlFor(pasfor). - Les expressions JS dans
{...}. - Conditionnel :
{cond && <X />}, ternaire{cond ? <A /> : <B />}. - Listes :
arr.map(x => <li key={x.id}>...</li>)— toujours unekeystable.
{users.map((u) => ( <UserCard key={u.id} user={u} />))}Erreur classique : key={index} dans une liste qui peut se réordonner. Préfère un ID stable.
Les hooks essentiels
Section intitulée « Les hooks essentiels »useState — état local
Section intitulée « useState — état local »const [count, setCount] = useState(0);
setCount(5); // remplacesetCount(prev => prev + 1); // basé sur la valeur précédente — préfère çaSi le nouveau state dépend de l’ancien, utilise la forme fonctionnelle : sinon en cas de mises à jour rapides, tu peux perdre des updates.
useEffect — effets de bord
Section intitulée « useEffect — effets de bord »useEffect(() => { // au montage et à chaque changement de userId fetchUser(userId).then(setUser);
return () => { // cleanup au démontage et avant chaque re-exécution };}, [userId]); // tableau de dépendances| Dépendances | Quand l’effet tourne |
|---|---|
[] | Au montage uniquement |
[a, b] | Quand a ou b change |
| (omis) | À chaque rendu (rarement souhaité) |
Règle d’or : les valeurs utilisées dans l’effet doivent être dans les dépendances (le linter react-hooks/exhaustive-deps te le dit).
useMemo et useCallback — mémoïsation
Section intitulée « useMemo et useCallback — mémoïsation »const expensive = useMemo(() => { return computeExpensiveValue(data);}, [data]); // recalcule uniquement si data change
const handleClick = useCallback(() => { setCount(c => c + 1);}, []);À utiliser uniquement si le profiler montre un goulot. Mémoïser partout = code plus lourd, gain négligeable, dépendances qui s’accumulent.
useRef — accéder au DOM, conserver une valeur entre rendus
Section intitulée « useRef — accéder au DOM, conserver une valeur entre rendus »// Référence DOMconst inputRef = useRef(null);useEffect(() => inputRef.current?.focus(), []);return <input ref={inputRef} />;
// Valeur persistante sans re-renderconst renderCount = useRef(0);renderCount.current++; // muter ne déclenche PAS de renduuseReducer — état complexe
Section intitulée « useReducer — état complexe »Quand useState devient cumbersome (plusieurs sous-états interdépendants) :
function reducer(state, action) { switch (action.type) { case 'add': return { ...state, items: [...state.items, action.item] }; case 'clear': return { ...state, items: [] }; default: return state; }}
const [state, dispatch] = useReducer(reducer, { items: [] });dispatch({ type: 'add', item: { id: 1, name: 'Hello' } });useContext — partager sans drill
Section intitulée « useContext — partager sans drill »const ThemeContext = createContext('light');
// Provider en haut de l'arbre<ThemeContext.Provider value="dark"> <App /></ThemeContext.Provider>
// Consommer n'importe oùfunction Button() { const theme = useContext(ThemeContext); return <button className={theme}>...</button>;}Piège réel rencontré — `key={index}` casse les listes réordonnables Performance
🩹 Symptôme : input qui ne suit pas son item après réordonnement, state local mélangé entre items, animations buggées.
{tasks.map((task, index) => ( <Task key={index} task={task} />))}🔍 Cause : React utilise key pour identifier chaque élément entre 2 renders. Avec index, supprimer l’item 0 fait que l’item 1 hérite de la key=0 — React croit que c’est le même composant et conserve son state.
🩺 Fix : toujours utiliser un id stable (souvent task.id venant de la DB).
{tasks.map(task => ( <Task key={task.id} task={task} />))}🧠 Leçon : key={index} est un piège qui ne se révèle pas immédiatement. Tout marche jusqu’au jour où l’utilisateur réordonne sa liste, puis « bizarrement » l’input value se mélange. Préviens le bug en utilisant des IDs stables dès le départ.
Piège réel rencontré — useEffect avec dépendance manquante = stale closure Tests
🩹 Symptôme :
useEffect(() => { const id = setInterval(() => { console.log(count); // affiche toujours 0 ! }, 1000); return () => clearInterval(id);}, []); // ← deps vides🔍 Cause : le useEffect capture count au 1er render (valeur 0). Le [] empêche la ré-exécution. L’interval continue de logger 0 même quand count change.
🩺 Fix : 3 options.
- Mettre la dep :
useEffect(() => { ... }, [count]);
- Setter callback (recommandé pour incrémenter) :
setCount(c => c + 1);
- Ref si tu veux la valeur courante sans re-render :
const countRef = useRef(count);useEffect(() => { countRef.current = count; });
🧠 Leçon : 99 % des bugs React du quotidien viennent de stale closures. Active react-hooks/exhaustive-deps en error (pas warning) dans ESLint. Si tu dois le désactiver, c’est presque toujours le signal d’un refacto nécessaire.
useOptimistic — mises à jour optimistes (React 19)
Section intitulée « useOptimistic — mises à jour optimistes (React 19) »Affiche immédiatement l’état attendu, puis confirme (ou annule) quand le serveur répond :
import { useOptimistic } from 'react';
function Likes({ likes, addLike }) { const [optimisticLikes, addOptimistic] = useOptimistic( likes, (current, increment) => current + increment );
async function handleClick() { addOptimistic(1); // l'UI affiche likes + 1 immédiatement await addLike(); // appel réseau (peut prendre 500 ms) // À la fin, optimisticLikes revient à la "vraie" valeur de likes }
return <button onClick={handleClick}>👍 {optimisticLikes}</button>;}✅ UX bien plus snappy. ✅ Si le serveur échoue, React revient automatiquement à l’état précédent. ✅ Idéal en combinaison avec une Server Action (panier, like, follow…).
useFormStatus — état d’un form depuis ses descendants (React 19)
Section intitulée « useFormStatus — état d’un form depuis ses descendants (React 19) »import { useFormStatus } from 'react-dom';
// Composant qui peut être profondément imbriqué dans un <form>function SubmitButton() { const { pending } = useFormStatus(); return <button disabled={pending}>{pending ? 'Envoi…' : 'Envoyer'}</button>;}Évite de prop-driller le pending jusqu’au bouton.
Hooks personnalisés
Section intitulée « Hooks personnalisés »Une fonction qui commence par use et qui appelle d’autres hooks. C’est la réutilisation la plus puissante en React.
function useFetch(url) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { let cancelled = false; setLoading(true); fetch(url) .then(r => r.json()) .then(d => { if (!cancelled) setData(d); }) .catch(e => { if (!cancelled) setError(e); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [url]);
return { data, error, loading };}
// Usagefunction Profile({ id }) { const { data, loading } = useFetch(`/api/users/${id}`); if (loading) return <Spinner />; return <h1>{data.name}</h1>;}(En vrai : utilise TanStack Query. C’est juste un exemple pédagogique.)
Règles d’or des hooks
Section intitulée « Règles d’or des hooks »- Toujours au top-level d’une fonction. Pas dans un
if, une boucle, un callback. - Toujours dans un composant (ou autre hook).
- Toujours dans le même ordre entre les rendus.
Le linter eslint-plugin-react-hooks les vérifie automatiquement. Active-le.
Server Components (RSC) — le grand changement
Section intitulée « Server Components (RSC) — le grand changement »Depuis Next.js 13 + React 19, par défaut, les composants sont Server Components : ils s’exécutent sur le serveur, n’envoient pas leur JS au client.
// app/products/page.tsx — Server Component par défautexport default async function ProductsPage() { const products = await db.product.findMany(); // SQL direct côté serveur return ( <ul> {products.map(p => <li key={p.id}>{p.name}</li>)} </ul> );}✅ Pas de JS bundle pour la liste. ✅ Accès direct à la DB / fichiers / clés API. ✅ HTML pré-rendu envoyé au client.
Client Components — quand on en a besoin
Section intitulée « Client Components — quand on en a besoin »Pour de l’interactivité (état, événements, hooks), il faut un Client Component :
'use client'; // ← directive en haut du fichier
import { useState } from 'react';
export function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>;}Combiner les deux
Section intitulée « Combiner les deux »// app/products/page.tsx (Server)import { ProductFilter } from './ProductFilter'; // Client
export default async function Page() { const products = await db.product.findMany(); return ( <> <ProductFilter /> {/* interactif */} <ul> {products.map(p => <li key={p.id}>{p.name}</li>)} {/* statique */} </ul> </> );}Règle pratique : commence en Server Component, ajoute 'use client' uniquement quand tu as besoin de hooks ou d’événements DOM.
Suspense et streaming
Section intitulée « Suspense et streaming »Suspense permet d’afficher un fallback pendant qu’un composant async charge.
import { Suspense } from 'react';
<Suspense fallback={<Spinner />}> <SlowAsyncComponent /></Suspense>En streaming SSR (Next.js App Router), le serveur envoie d’abord le shell de la page + le fallback, puis remplace par le vrai contenu dès qu’il est prêt. L’utilisateur voit quelque chose en moins d’une seconde même si la DB met 3 secondes.
Server Actions — formulaires sans API explicite
Section intitulée « Server Actions — formulaires sans API explicite »Next.js 13+ permet d’écrire des fonctions serveur appelées directement depuis un formulaire client :
async function sendFeedback(formData: FormData) { 'use server'; const message = formData.get('message'); await db.feedback.create({ data: { message } });}
export default function Page() { return ( <form action={sendFeedback}> <textarea name="message" required /> <button type="submit">Envoyer</button> </form> );}✅ Pas d’endpoint API à écrire. ✅ Validation côté serveur (toujours). ✅ Marche sans JS si le formulaire est en HTML standard.
TanStack Query — le must pour les données serveur
Section intitulée « TanStack Query — le must pour les données serveur »npm install @tanstack/react-queryimport { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() { return ( <QueryClientProvider client={queryClient}> <Users /> </QueryClientProvider> );}
function Users() { const { data, isLoading, error } = useQuery({ queryKey: ['users'], queryFn: () => fetch('/api/users').then(r => r.json()), staleTime: 60_000, // 1 minute avant refetch });
const deleteUser = useMutation({ mutationFn: (id) => fetch(`/api/users/${id}`, { method: 'DELETE' }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }), });
if (isLoading) return <Spinner />; if (error) return <p>{error.message}</p>; return data.map(u => ( <div key={u.id}> {u.name} <button onClick={() => deleteUser.mutate(u.id)}>Supprimer</button> </div> ));}Tu as gratuitement : cache, dedup des requêtes simultanées, refetch on focus/reconnect, retry, mutations optimistes, devtools graphiques.
Formulaires — React Hook Form + Zod
Section intitulée « Formulaires — React Hook Form + Zod »npm install react-hook-form @hookform/resolvers zodimport { useForm } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';import { z } from 'zod';
const Schema = z.object({ email: z.string().email(), password: z.string().min(8),});
type FormData = z.infer<typeof Schema>;
function LoginForm() { const { register, handleSubmit, formState: { errors } } = useForm<FormData>({ resolver: zodResolver(Schema), });
const onSubmit = (data: FormData) => { console.log(data); // typé ! };
return ( <form onSubmit={handleSubmit(onSubmit)}> <label htmlFor="email">E-mail</label> <input id="email" {...register('email')} /> {errors.email && <p>{errors.email.message}</p>}
<label htmlFor="password">Mot de passe</label> <input id="password" type="password" {...register('password')} /> {errors.password && <p>{errors.password.message}</p>}
<button type="submit">Se connecter</button> </form> );}Validation schema-driven, types inférés, performances excellentes (pas de re-render à chaque keystroke).
État global — Zustand
Section intitulée « État global — Zustand »import { create } from 'zustand';
const useStore = create<{ count: number; inc: () => void }>((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })),}));
// Usage — pas de providerfunction Counter() { const { count, inc } = useStore(); return <button onClick={inc}>{count}</button>;}
// Selector pour ne re-render que sur le morceau utiliséconst count = useStore((s) => s.count);Plus simple que Redux, suffisant dans 90 % des cas. Si tu hérites d’un projet Redux Toolkit, ça reste une option valide pour les très grosses équipes.
Tester un composant — Vitest + Testing Library
Section intitulée « Tester un composant — Vitest + Testing Library »npm install -D vitest @testing-library/react @testing-library/jest-dom jsdomimport { describe, it, expect } from 'vitest';import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import { Counter } from './Counter';
describe('Counter', () => { it('démarre à 0', () => { render(<Counter />); expect(screen.getByRole('button')).toHaveTextContent('0'); });
it('incrémente au clic', async () => { render(<Counter />); await userEvent.click(screen.getByRole('button')); expect(screen.getByRole('button')).toHaveTextContent('1'); });});Principe : teste le comportement (ce que voit l’utilisateur), pas l’implémentation (state interne, méthodes privées). Si tu refactors le composant sans changer le comportement, le test ne devrait pas casser.
Auto-évaluation
Section intitulée « Auto-évaluation »Pour aller plus loin
Section intitulée « Pour aller plus loin »- react.dev — la nouvelle doc officielle, excellente
- Patterns.dev — patterns React modernes
- Tanstack Query Docs — tanstack.com/query
- React Hook Form Docs — react-hook-form.com
- Kent C. Dodds — Epic React — formation payante mais réputée excellente
- Dan Abramov — Overreacting (blog) — articles profonds sur React
Suite : 7.3 — Alternatives — Vue, Svelte, Solid, Astro, Angular.