Skip to content

React Aide-mémoire

Bibliothèque JavaScript pour construire des interfaces utilisateur avec des composants.

01

JSX & Composants

Composants Fonction

Les composants React sont des fonctions JavaScript qui retournent du JSX. Les noms de composants doivent commencer par une majuscule (minuscule = balises HTML). Les props sont passées comme attributs et déstructurées dans le paramètre. JSX est du sucre syntaxique pour React.createElement(). Retournez toujours un seul élément racine (ou utilisez des Fragments). Les composants doivent être purs — mêmes props = même sortie.

react
// Basic function component
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function component
const Greeting = ({ name = 'Guest' }) => (
  <p>Welcome, {name}!</p>
);

// Composing components
function App() {
  return (
    <div>
      <Welcome name="Alice" />
      <Welcome name="Bob" />
      <Greeting />
    </div>
  );
}

Expressions JSX

JSX permet d'intégrer des expressions JavaScript dans des accolades {}. Vous pouvez mettre des variables, des appels de fonction, des opérateurs ternaires et toute expression qui retourne une valeur. Les instructions (if, for, switch) ne sont pas autorisées directement — utilisez ternaire ou IIFE. Les valeurs booléennes (true, false), null et undefined ne rendent rien. Les nombres et chaînes rendent comme texte. Les objets ne sont pas des enfants React valides.

react
function Expression({ user, items }) {
  const fullName = user.first + ' ' + user.last;
  const itemCount = items.length;

  return (
    <div>
      {/* Expressions in curly braces */}
      <h1>{fullName}</h1>
      <p>{itemCount} items</p>

      {/* Conditional */}
      <p>{itemCount > 0 ? 'In stock' : 'Sold out'}</p>

      {/* Method calls */}
      <p>{fullName.toUpperCase()}</p>

      {/* Numbers and booleans render as nothing */}
      <p>{false}{null}{undefined}</p>
    </div>
  );
}

Fragments & Listes en JSX

Les Fragments (<>...</>) regroupent plusieurs éléments sans ajouter de nœuds DOM supplémentaires — plus propre que d'envelopper dans une <div>. Utilisez <Fragment key={...}> quand vous devez passer une clé. Les tableaux d'éléments JSX nécessitent des props key uniques. Bien que l'index du tableau comme clé fonctionne pour les listes statiques, utilisez des IDs stables pour les listes dynamiques afin d'éviter les bugs de rendu. Les Fragments améliorent la performance en réduisant les éléments wrapper inutiles.

react
// Fragment: group without extra DOM node
function App() {
  return (
    <>
      <header>Header</header>
      <main>Content</main>
      <footer>Footer</footer>
    </>
  );
}

// Array of elements (needs keys)
function List() {
  const fruits = ['Apple', 'Banana', 'Cherry'];
  return (
    <ul>
      {fruits.map((fruit, i) => (
        <li key={i}>{fruit}</li>
      ))}
    </ul>
  );
}

Rendu Conditionnel

React offre plusieurs patterns de rendu conditionnel. Les retours anticipés pour la logique if/else. Le ternaire (cond ? A : B) pour l'un ou l'autre. Le ET logique (cond && <Component/>) pour afficher/masquer. IIFE pour les branchements complexes. Évitez d'intégrer une logique complexe dans JSX — extrayez vers des variables ou des fonctions helper. Pour les instructions switch, utilisez un objet de lookup ou extrayez vers une fonction séparée. Les valeurs falsy (0, '') rendent, donc utilisez le ternaire au lieu de && pour les nombres.

react
function Greeting({ isLoggedIn, user }) {
  // 1. If/else (use early return)
  if (!isLoggedIn) return <Login />;

  // 2. Ternary operator
  return (
    <div>
      {user ? <Dashboard user={user} /> : <Loading />}

      {/* 3. Logical AND (render if truthy) */}
      {user.isAdmin && <AdminPanel />}

      {/* 4. IIFE for complex logic */}
      {(() => {
        if (user.role === 'admin') return <Admin />;
        if (user.role === 'mod') return <Mod />;
        return <User />;
      })()}
    </div>
  );
}

Children & Render Props

La prop children contient les éléments entre les balises ouvrantes et fermantes — essentielle pour les composants composables (cards, modals, layouts). Les render props passent une fonction comme prop qui reçoit des données et retourne du JSX — une alternative aux HOCs et hooks pour partager la logique. Bien que les render props soient moins courantes avec les hooks, elles restent utiles pour les patterns d'injection de composants. children est une prop spéciale qui n'a pas besoin d'être passée explicitement.

react
// children prop: content between tags
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="Profile">
  <p>Name: Alice</p>
  <p>Age: 30</p>
</Card>

// Render prop pattern
function DataProvider({ render }) {
  const data = fetchData();
  return <div>{render(data)}</div>;
}
02

Props

Passer des Props

Les props sont des données en lecture seule passées du parent à l'enfant. Elles peuvent être n'importe quelle valeur JavaScript : chaînes, nombres, booléens, tableaux, objets ou fonctions. Les valeurs chaîne utilisent des guillemets (name='Alice'), toutes les autres valeurs utilisent des accolades (age={30}). Les fonctions comme props permettent la communication enfant-vers-parent (callbacks). Les props descendent — les enfants ne peuvent pas modifier les props. Pour la liaison de données bidirectionnelle, remontez l'état au parent commun.

react
// Parent passes props to child
function App() {
  return (
    <User
      name="Alice"
      age={30}
      isActive={true}
      tags={['admin', 'dev']}
      onClick={() => console.log('clicked')}
    />
  );
}

// Child receives props
function User({ name, age, isActive, tags, onClick }) {
  return (
    <div onClick={onClick}>
      <h1>{name}</h1>
      <p>Age: {age}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
}

Props par Défaut & Optionnelles

Les valeurs de props par défaut sont définies via la déstructuration (param = defaultValue). Si une prop n'est pas passée, elle est undefined. Utilisez le court-circuit (bio && <p>) ou le ternaire pour rendre conditionnellement les props optionnelles. PropTypes (legacy) ou les interfaces TypeScript peuvent valider les types de props au développement. defaultProps (composants classe) est déprécié pour les composants fonction — utilisez les valeurs par défaut de déstructuration à la place.

react
// Default values via destructuring
function Button({ color = 'blue', size = 'md', children }) {
  return (
    <button className={'btn btn-' + color + ' btn-' + size}>
      {children}
    </button>
  );
}

// Optional props (undefined if not passed)
function Profile({ name, bio }) {
  return (
    <div>
      <h1>{name}</h1>
      {bio && <p>{bio}</p>}
    </div>
  );
}

// Usage
<Button>Click</Button>           {/* color='blue', size='md' */}
<Profile name="Alice" />         {/* bio is undefined */}

Spread & Rest Props

L'opérateur spread (...props) passe toutes les props à un élément enfant — utile pour les composants wrapper (HOCs, styled components). L'opérateur rest collecte les props restantes après déstructuration de celles spécifiques. Ce pattern est courant dans les design systems où un composant wrapper transmet les props inconnues à un élément DOM. Attention : le spread peut remplacer les attributs explicites — l'ordre compte ({...props} className='x' vs className='x' {...props}).

react
// Spread: pass all props to child
function Input(props) {
  return <input {...props} className="input" />;
}

// Usage
<Input type="text" placeholder="Name" value="Alice" />

// Rest: collect remaining props
function Button({ label, ...rest }) {
  return <button {...rest}>{label}</button>;
}

// Selective spreading
function Card({ title, children, ...divProps }) {
  return (
    <div {...divProps}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Prop Types & TypeScript

Les interfaces TypeScript fournissent une vérification de type à la compilation pour les props — l'approche recommandée pour les nouveaux projets React. Les props optionnelles utilisent ? (isActive?: boolean). PropTypes fournit une validation à l'exécution (uniquement en développement) et sont utiles pour les projets JavaScript sans TypeScript. isRequired garantit que la prop est fournie. TypeScript intercepte les erreurs de type avant l'exécution, le rendant supérieur pour les grandes bases de code.

react
// TypeScript interface (recommended)
interface UserProps {
  name: string;
  age: number;
  isActive?: boolean;       // optional
  onClick: (id: number) => void;
}

function User({ name, age, isActive = true, onClick }: UserProps) {
  return <div onClick={() => onClick(1)}>{name}, {age}</div>;
}

// PropTypes (runtime checking, legacy)
import PropTypes from 'prop-types';
User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  isActive: PropTypes.bool,
};

Prop Drilling & Context

Le prop drilling se produit quand les props traversent plusieurs couches de composants qui ne les utilisent pas. Pour 2-3 niveaux, c'est acceptable. Pour les arbres plus profonds, utilisez Context API, les bibliothèques de gestion d'état (Redux, Zustand) ou la composition de composants. La composition (passer des composants comme props ou children) résout souvent le drilling plus élégamment que Context. Demandez-vous : chaque composant intermédiaire a-t-il besoin de ces données ? Si non, reconsidérez votre structure de composants.

react
// Prop drilling: passing through multiple levels
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserInfo user={user} />;
}

// Solution: Context API (see State Management section)
// Avoid drilling more than 2-3 levels
03

useState & État

useState de Base

useState est le hook fondamental pour ajouter de l'état aux composants fonction. Il retourne un tableau : [currentValue, setterFunction]. La valeur initiale peut être de n'importe quel type. Appeler le setter déclenche un re-render avec la nouvelle valeur. Les mises à jour d'état sont asynchrones — la valeur ne change pas immédiatement après setCount. Chaque instance de composant a son propre état indépendant. Le setter est stable (même référence à travers les rendus).

react
import { useState } from 'react';

function Counter() {
  // [currentValue, setterFunction] = useState(initialValue)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Mises à Jour Fonctionnelles

Quand le nouvel état dépend de l'état précédent, utilisez une mise à jour fonctionnelle : setCount(prev => prev + 1). Cela garantit que vous travaillez avec le dernier état, même si plusieurs mises à jour sont batchées. Sans mises à jour fonctionnelles, des appels successifs rapides peuvent utiliser un état périmé. React 18 batche automatiquement les mises à jour d'état (même dans les promises et timeouts), donc les mises à jour fonctionnelles sont essentielles pour l'exactitude.

react
function Counter() {
  const [count, setCount] = useState(0);

  // BAD: may not work correctly with rapid updates
  const incrementBad = () => setCount(count + 1);

  // GOOD: functional update uses previous state
  const incrementGood = () => setCount(prev => prev + 1);

  // Batch updates
  const addThree = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return <button onClick={addThree}>Count: {count}</button>;
}

État avec Objets & Tableaux

Ne mutez jamais l'état directement — créez toujours un nouvel objet/tableau. Pour les objets, utilisez l'opérateur spread pour copier les propriétés existantes : {...prev, [field]: value}. Pour les tableaux, utilisez spread pour ajouter ([...prev, newItem]), filter pour retirer, et map pour mettre à jour. React compare les références pour détecter les changements — les objets mutés ont la même référence, donc React ne re-renderera pas. C'est la source #1 de bugs React pour les débutants.

react
function Form() {
  const [form, setForm] = useState({ name: '', email: '', age: 0 });

  // BAD: mutates state directly
  // form.name = 'Alice'; setForm(form);

  // GOOD: spread to create new object
  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  return (
    <input
      value={form.name}
      onChange={e => updateField('name', e.target.value)}
    />
  );
}

// Array state
function TodoList() {
  const [todos, setTodos] = useState([]);
  const addTodo = (text) => setTodos(prev => [...prev, { id: Date.now(), text }]);
  const removeTodo = (id) => setTodos(prev => prev.filter(t => t.id !== id));
}

État Initial Paresseux

Si l'état initial nécessite un calcul coûteux, passez une fonction à useState (initialisation paresseuse). La fonction s'exécute uniquement au premier rendu, pas à chaque re-render. C'est important pour parser localStorage, récupérer depuis IndexedDB ou toute configuration CPU-intensive. La forme fonctionnelle : useState(() => initialValue). Pour les valeurs simples (nombres, chaînes), passez juste la valeur directement — l'init paresseux est inutile.

react
import { useState } from 'react';

function ExpensiveInit() {
  // BAD: runs on every render (even though result is ignored)
  const [data, setData] = useState(computeExpensiveValue());

  // GOOD: lazy initialization — function runs only once
  const [data2, setData2] = useState(() => computeExpensiveValue());

  // Reading from localStorage
  const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
  });

  return <div>{data2}</div>;
}

Variables d'État Multiples

Utilisez plusieurs appels useState pour les valeurs indépendantes plutôt qu'un gros objet. Cela rend les mises à jour plus simples (pas besoin de spread) et évite les re-renders inutiles. Groupez les valeurs liées dans un seul objet d'état (ex. champs de formulaire). Pour la logique d'état complexe avec plusieurs sous-valeurs, considérez useReducer à la place. Règle empirique : si les mises à jour d'état sont indépendantes, utilisez des useState séparés ; si elles sont liées/interdépendantes, utilisez useReducer ou un seul objet.

react
function LoginForm() {
  // Multiple independent state variables
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [rememberMe, setRememberMe] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    // ... validation and submission
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password}
        onChange={e => setPassword(e.target.value)} />
      <button disabled={isSubmitting}>Submit</button>
    </form>
  );
}
04

useEffect & Effets de Bord

useEffect de Base

useEffect effectue des effets de bord après le rendu. La fonction d'effet s'exécute après que le composant soit peint. La fonction de nettoyage (retournée) s'exécute avant le prochain effet et au démontage — essentielle pour nettoyer les timers, abonnements et listeners. Le tableau de dépendances contrôle quand l'effet se ré-exécute : [] = une fois au montage, [dep] = quand dep change, pas de tableau = à chaque rendu. Nettoyez toujours pour éviter les fuites de mémoire.

react
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  // Runs after every render
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function runs before next effect or unmount
    return () => clearInterval(interval);
  }, []); // empty array = run once on mount

  return <p>Seconds: {seconds}</p>;
}

Tableau de Dépendances

Le tableau de dépendances est critique pour le comportement de useEffect. Tableau vide [] = montage uniquement (comme componentDidMount). Avec dépendances [a, b] = s'exécute au montage et quand a ou b change. Pas de tableau = à chaque rendu (rarement ce que vous voulez). Les dépendances manquantes causent des closures périmées. Inclure des dépendances inutiles cause des ré-exécutions excessives. Utilisez la règle ESLint exhaustive-deps pour intercepter les erreurs. Toute valeur de la portée du composant utilisée dans l'effet devrait être dans les deps.

react
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // Runs once on mount (empty deps)
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  // Runs when userId changes
  useEffect(() => {
    fetch('/api/users/' + userId)
      .then(r => r.json())
      .then(setUser);
  }, [userId]); // re-run when userId changes

  // Runs on every render (no deps) - rarely needed
  useEffect(() => {
    console.log('Every render');
  });

  return <div>{user?.name}</div>;
}

Nettoyage & Abonnements

Le nettoyage est essentiel pour les abonnements, listeners d'événements, timers et connexions WebSocket. Sans nettoyage, vous obtenez des fuites de mémoire et des handlers dupliqués. La fonction de nettoyage s'exécute : (1) avant la ré-exécution de l'effet, (2) au démontage du composant. Pour les listeners WebSocket/événements, retirez-les toujours dans le nettoyage. Pour l'état qui dépend de l'effet, réinitialisez-le dans le nettoyage pour éviter d'afficher des données périmées d'un roomId précédent.

react
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket('wss://chat.example.com/' + roomId);

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // Cleanup: close connection when roomId changes or unmount
    return () => {
      ws.close();
      setMessages([]); // reset for new room
    };
  }, [roomId]);

  // Window event listener
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>{messages.length} messages</div>;
}

Récupération de Données

La récupération de données dans useEffect nécessite un indicateur d'annulation pour éviter de définir l'état après le démontage (cause des avertissements React). Le flag 'cancelled' garantit que setUsers/setError/setLoading ne s'exécutent que si le composant est encore monté. Pour les applications de production, considérez utiliser une bibliothèque de récupération de données (React Query, SWR) qui gère le cache, la déduplication et les conditions de course automatiquement. Le tableau de dépendances vide [] garantit que la récupération se fait une fois au montage.

react
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        const res = await fetch('/api/users');
        const data = await res.json();
        if (!cancelled) setUsers(data);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();
    return () => { cancelled = true; };
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

useLayoutEffect vs useEffect

useEffect s'exécute asynchronement après que le navigateur peigne — les utilisateurs peuvent voir un bref flash si vous mesurez des éléments DOM. useLayoutEffect s'exécute synchronement après les mutations DOM mais avant le paint — empêchant le scintillement visuel. Utilisez useLayoutEffect pour les mesures DOM (getBoundingClientRect, position de défilement) qui affectent la disposition. Utilisez useEffect pour tout le reste (ne bloque pas le painting). Sur le serveur, useLayoutEffect avertit — utilisez le pattern useIsomorphicLayoutEffect.

react
import { useState, useEffect, useLayoutEffect } from 'react';

function Tooltip({ text }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  // useEffect: runs AFTER paint (user may see flash)
  useEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  // useLayoutEffect: runs BEFORE paint (no flash)
  useLayoutEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  return <div id="tip" style={{ left: position.x, top: position.y }}>{text}</div>;
}
05

useRef, useMemo & useCallback

Bases de useRef

useRef retourne un objet mutable { current: value } qui persiste à travers les rendus. Contrairement à l'état, modifier ref.current NE déclenche PAS de re-render. Usages courants : (1) accéder aux éléments DOM (via l'attribut ref), (2) stocker des valeurs mutables qui n'affectent pas le rendu (timers, valeurs précédentes), (3) stocker la dernière valeur pour utilisation dans les callbacks. L'objet ref a la même identité à travers les rendus. La valeur initiale est passée à useRef(initialValue).

react
import { useRef } from 'react';

function FocusInput() {
  // ref to access DOM element
  const inputRef = useRef(null);

  const focus = () => inputRef.current.focus();
  const clear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
      <button onClick={clear}>Clear</button>
    </div>
  );
}

useRef pour les Valeurs Mutables

useRef stocke des valeurs mutables qui persistent à travers les rendus sans déclencher de re-renders. C'est parfait pour : les IDs de timer, les références WebSocket, le suivi de l'état précédent et le comptage des rendus. Puisque modifier ref.current ne cause pas de re-render, l'UI ne se met pas à jour quand vous le changez — utilisez l'état pour les valeurs qui devraient affecter l'UI. Le pattern de comptage de rendus (ref.current++) est utile pour le débogage mais ne devrait pas être utilisé dans la logique de production.

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const renderCount = useRef(0);

  // Track render count (doesn't trigger re-render)
  renderCount.current++;

  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>{seconds}s (render #{renderCount.current})</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useMemo

useMemo mémoïse (cache) une valeur calculée, recalculant seulement quand les dépendances changent. Utilisez-le pour les calculs coûteux (filtrage, tri, maths complexes) pour éviter de ré-exécuter à chaque rendu. Le tableau de dépendances fonctionne comme useEffect. La surutilisation peut nuire à la performance (la mémoïsation a son propre coût) — ne mémoïsez que les opérations véritablement coûteuses. useMemo est aussi utile pour préserver les références d'objets afin d'empêcher les re-renders enfants.

react
import { useState, useMemo } from 'react';

function ProductList({ products, filter }) {
  const [search, setSearch] = useState('');

  // Memoize expensive computation
  const filtered = useMemo(() => {
    console.log('Filtering...');
    return products
      .filter(p => p.category === filter)
      .filter(p => p.name.includes(search));
  }, [products, filter, search]); // recompute only when these change

  // Memoize a value
  const totalPrice = useMemo(() =>
    filtered.reduce((sum, p) => sum + p.price, 0),
  [filtered]);

  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      <p>Total: ${totalPrice}</p>
      {filtered.map(p => <div key={p.id}>{p.name}</div>)}
    </div>
  );
}

useCallback

useCallback mémoïse une fonction, retournant la même référence à travers les rendus sauf si les dépendances changent. Cela empêche les re-renders inutiles des composants enfants mémoïsés (enveloppés dans memo()). Sans useCallback, chaque rendu parent crée une nouvelle référence de fonction, causant les enfants memo() à re-render. Utilisez useCallback quand vous passez des callbacks aux composants enfants optimisés. Comme useMemo, ne le surutilisez — seulement pour les fonctions passées comme props aux enfants mémoïsés.

react
import { useState, useCallback, memo } from 'react';

// Memoized child component
const Button = memo(function Button({ onClick, label }) {
  console.log('Button rendered');
  return <button onClick={onClick}>{label}</button>;
});

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Without useCallback: new function every render
  // const handleClick = () => setCount(c => c + 1);

  // With useCallback: stable function reference
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // empty deps = stable forever

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Button onClick={handleClick} label="Click" />
      <p>Count: {count}</p>
    </div>
  );
}

Transfert de Refs

forwardRef permet aux composants parents de passer un ref à un élément DOM d'un composant enfant. useImperativeHandle personnalise ce que le ref expose — au lieu du nœud DOM, vous pouvez exposer des méthodes spécifiques (focus, clear, getValue). C'est utile pour créer des composants d'input réutilisables avec des APIs impératives. React 19 a simplifié les refs (ref est maintenant une prop régulière), mais forwardRef reste nécessaire pour les bibliothèques. Évitez de surutiliser les handles impératifs — préférez les props déclaratives.

react
import { useRef, forwardRef, useImperativeHandle } from 'react';

// forwardRef: pass ref to child component
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

// useImperativeHandle: expose specific methods
const CustomInput = forwardRef(function CustomInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
    getValue: () => inputRef.current.value,
  }));

  return <input ref={inputRef} />;
});

// Usage
function App() {
  const ref = useRef(null);
  return (
    <>
      <CustomInput ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
      <button onClick={() => ref.current.clear()}>Clear</button>
    </>
  );
}
06

useReducer & Context

Bases de useReducer

useReducer est une alternative à useState pour la logique d'état complexe. Un reducer est une fonction pure : (state, action) => newState. Les actions décrivent ce qui s'est passé ; le reducer décide comment mettre à jour l'état. Ce pattern rend les transitions d'état prévisibles et testables. Dispatch est stable (même référence). Retournez toujours un nouvel objet d'état (ne mutez jamais). Le cas par défaut devrait lancer une erreur pour les actions inconnues. Utilisez useReducer quand l'état a plusieurs sous-valeurs ou le prochain état dépend d'une logique complexe.

react
import { useReducer } from 'react';

// Reducer function: (state, action) => newState
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'set', payload: 10 })}>Set 10</button>
    </div>
  );
}

Reducer Complexe

Les reducers complexes gèrent plusieurs parties d'état liées. Chaque type d'action gère une transition d'état spécifique. Spreadez toujours l'état précédent ({...state}) pour préserver les champs non liés. Pour les mises à jour imbriquées (comme basculer un todo), utilisez map pour créer un nouveau tableau avec l'élément mis à jour. Les reducers doivent être purs — pas d'effets de bord, pas d'appels API. Extrayez le reducer dans un fichier séparé pour la testabilité. Considérez des bibliothèques comme Redux Toolkit pour un état très complexe.

react
const initialState = {
  todos: [],
  filter: 'all',
  loading: false,
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return { ...state, todos: [...state.todos, action.todo] };
    case 'toggle':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.id ? { ...t, done: !t.done } : t
        ),
      };
    case 'delete':
      return { ...state, todos: state.todos.filter(t => t.id !== action.id) };
    case 'set_filter':
      return { ...state, filter: action.filter };
    case 'set_loading':
      return { ...state, loading: action.loading };
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  // dispatch({ type: 'add', todo: { id: 1, text: 'Learn React', done: false } })
}

Context API

Context API partage l'état à travers l'arbre des composants sans prop drilling. Créez avec createContext(defaultValue). Enveloppez les consommateurs dans Provider avec une prop value. Consommez avec useContext(Context). Les changements de valeur de Context déclenchent les re-renders de tous les consommateurs. Pour la performance, séparez les contexts (ThemeContext, UserContext) pour que les composants ne re-render que quand leur context spécifique change. La valeur par défaut est utilisée quand aucun Provider n'enveloppe le consommateur.

react
import { createContext, useContext, useState } from 'react';

// 1. Create context with default value
const ThemeContext = createContext('light');
const UserContext = createContext(null);

// 2. Provider component
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 3. Consume context
function Page() {
  const { theme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);
  return <div className={'page theme-' + theme}>Hello {user?.name}</div>;
}

Context avec Reducer

Combiner Context avec useReducer crée un système de gestion d'état global léger (mini Redux). Le Provider expose à la fois l'état et dispatch. Un hook personnalisé (useStore) fournit la gestion d'erreurs s'il est utilisé hors du provider. Ce pattern est idéal pour les applications de taille moyenne. Pour les très grandes applications avec des mises à jour fréquentes, considérez séparer les contexts ou utiliser Redux/Zustand pour éviter de re-render tous les consommateurs à chaque changement d'état.

react
import { createContext, useContext, useReducer } from 'react';

// Combine Context + useReducer for global state
const StoreContext = createContext(null);

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

// Custom hook for easy consumption
function useStore() {
  const context = useContext(StoreContext);
  if (!context) throw new Error('useStore must be used within StoreProvider');
  return context;
}

// Usage
function Component() {
  const { state, dispatch } = useStore();
  return <button onClick={() => dispatch({ type: 'action' })}>Click</button>;
}

Performance de useContext

Quand la valeur de context change, TOUS les consommateurs re-render — même s'ils n'utilisent qu'une petite partie de la valeur. Pour optimiser : (1) Séparez les contexts pour que les composants ne s'abonnent qu'à ce dont ils ont besoin. (2) Mémoïsez la valeur de context avec useMemo pour empêcher les re-renders quand la valeur n'a pas réellement changé. (3) Utilisez des sélecteurs (bibliothèque use-context-selector) pour des abonnements fin-grain. Pour les mises à jour haute-fréquence (comme la position de la souris), Context peut causer des problèmes de performance — considérez les refs ou les stores externes.

react
// SPLIT contexts for performance
const ThemeContext = createContext();
const UserContext = createContext();
const CartContext = createContext();

// Each provider manages its own state
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <CartProvider>
          <App />
        </CartProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// Component only re-renders when its context changes
function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  // Won't re-render when user or cart changes
  return <button className={theme}>Button</button>;
}

// Memoize context value to prevent unnecessary re-renders
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
07

Événements & Formulaires

Gestion d'Événements

Les événements React utilisent camelCase (onClick, pas onclick). L'objet événement est un SyntheticEvent (wrapper autour de l'événement natif). e.preventDefault() arrête le comportement par défaut (soumission de formulaire, navigation de lien). e.stopPropagation() empêche le bouillonnement d'événement. Pour passer des paramètres aux handlers, utilisez les fonctions fléchées : onClick={() => handleDelete(id)}. Évitez de définir des handlers complexes inline — extrayez-les pour la lisibilité. Les événements React sont poolés (pre-17), donc appelez e.persist() si vous avez besoin d'un accès asynchrone.

react
function App() {
  // Click event
  const handleClick = (e) => {
    e.preventDefault();
    console.log('Button clicked', e.target);
  };

  // With parameters (use arrow function)
  const handleDelete = (id) => {
    console.log('Delete item', id);
  };

  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <button onClick={() => handleDelete(42)}>Delete</button>
      <div onMouseEnter={() => console.log('hover')}
           onMouseLeave={() => console.log('leave')}>
        Hover me
      </div>
    </div>
  );
}

Inputs Contrôlés

Les inputs contrôlés ont leur valeur contrôlée par l'état React. La prop value définit la valeur de l'input, et onChange met à jour l'état. Cela fait de React la « source unique de vérité » pour les données de formulaire. Chaque frappe déclenche une mise à jour d'état et un re-render. Pour les formulaires complexes, cela peut être verbeux — considérez des bibliothèques comme React Hook Form ou Formik. Les inputs contrôlés permettent la validation en temps réel et le comportement dynamique. Utilisez toujours onChange avec value (ou readOnly) pour éviter les avertissements React.

react
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', email, 'Password:', password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

Formulaire avec Champs Multiples

Pour les formulaires avec de nombreux champs, utilisez un seul objet d'état et une fonction handleChange générique. L'attribut name sur chaque input correspond à la clé d'état. Le handler utilise les noms de propriété calculés ([name]: value) pour mettre à jour le bon champ. Pour les cases à cocher, utilisez checked au lieu de value. Ce pattern réduit considérablement le boilerplate. Pour les inputs de fichier, utilisez des inputs non contrôlés (ils ne peuvent pas être entièrement contrôlés). Considérez React Hook Form pour les formulaires complexes avec validation.

react
function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    country: 'us',
    agree: false,
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={formData.username} onChange={handleChange} />
      <input name="email" type="email" value={formData.email} onChange={handleChange} />
      <select name="country" value={formData.country} onChange={handleChange}>
        <option value="us">USA</option>
        <option value="uk">UK</option>
      </select>
      <label>
        <input type="checkbox" name="agree" checked={formData.agree} onChange={handleChange} />
        Agree to terms
      </label>
      <button type="submit">Register</button>
    </form>
  );
}

Inputs Non Contrôlés

Les inputs non contrôlés utilisent les refs pour accéder à la valeur DOM directement, sans état React. La prop defaultValue définit la valeur initiale (pas value). C'est plus simple pour les formulaires qui n'ont pas besoin de validation en temps réel ou de comportement dynamique. Les inputs de fichier doivent être non contrôlés (leur valeur est en lecture seule pour la sécurité). Les inputs non contrôlés sont aussi utiles pour l'intégration avec du code non-React. Le compromis : vous ne pouvez pas facilement valider ou transformer l'input en temps réel. Préférez les inputs contrôlés dans la plupart des cas.

react
import { useRef } from 'react';

function UncontrolledForm() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', emailRef.current.value);
    console.log('Password:', passwordRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} type="email" defaultValue="" />
      <input ref={passwordRef} type="password" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

// File input (must be uncontrolled)
function FileUpload() {
  const fileRef = useRef(null);
  return <input ref={fileRef} type="file" />;
}

Validation & Gestion d'Erreurs

La validation de formulaire peut se faire à la soumission ou à chaque changement. La fonction validate retourne un objet d'erreurs — vide signifie valide. Affichez les erreurs conditionnellement à côté de chaque champ. Pour une meilleure UX, validez au blur (après que l'utilisateur quitte le champ) plutôt qu'à chaque frappe. Des bibliothèques comme React Hook Form + Zod, ou Formik + Yup, fournissent des schémas de validation robustes, la gestion d'erreurs et le suivi touch/blur. Validez toujours côté serveur aussi — la validation client est pour l'UX, pas la sécurité.

react
function ValidatedForm() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const errs = {};
    if (!values.email) errs.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(values.email)) errs.email = 'Invalid email';
    if (!values.password) errs.password = 'Password is required';
    else if (values.password.length < 8) errs.password = 'Min 8 characters';
    return errs;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate();
    setErrors(errs);
    if (Object.keys(errs).length === 0) {
      console.log('Form valid', values);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={values.email}
        onChange={e => setValues(v => ({ ...v, email: e.target.value }))} />
      {errors.email && <span className="error">{errors.email}</span>}
      <input type="password" value={values.password}
        onChange={e => setValues(v => ({ ...v, password: e.target.value }))} />
      {errors.password && <span className="error">{errors.password}</span>}
      <button type="submit">Submit</button>
    </form>
  );
}
08

Listes & Rendu Conditionnel

Rendu de Listes

Utilisez .map() pour transformer les tableaux en éléments JSX. Chaque élément a besoin d'une prop key unique — utilisez des IDs stables (todo.id), pas les indices de tableau. Les keys aident React à identifier quels éléments changent (ajoutés, retirés, réordonnés) pour des mises à jour DOM efficaces. Utiliser l'index comme key cause des bugs quand les éléments de liste sont réordonnés ou insérés au début. Pour les listes vides, rendez un message de repli. Considérez useMemo pour les listes filtrées/triées afin d'éviter la re-computation à chaque rendu.

react
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => toggle(todo.id)}>
            {todo.done ? 'Undo' : 'Done'}
          </button>
        </li>
      ))}
    </ul>
  );
}

// Filtering and sorting
function FilteredList({ items, filter }) {
  const visible = items
    .filter(item => item.category === filter)
    .sort((a, b) => a.name.localeCompare(b.name));

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

Keys Expliquées

Les keys doivent être uniques parmi les siblings (même parent), mais peuvent se répéter entre différentes listes. Les keys aident l'algorithme de réconciliation de React : quand une key change, React détruit et recrée le composant (perdant l'état). Avec les keys d'index, insérer un élément au début décale tous les indices, causant React à tout re-render. Avec les keys d'ID stable, React ne rend que le nouvel élément. Les keys n'ont pas besoin d'être globalement uniques — juste uniques dans la liste. N'utilisez pas de keys aléatoires (Math.random()) — elles changent à chaque rendu.

react
// GOOD: stable, unique keys
{todos.map(todo => (
  <TodoItem key={todo.id} todo={todo} />
))}

// BAD: index as key (causes bugs with reordering)
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}

// When index keys are OK:
// - Static list (never reordered/filtered)
// - List items have no state
// - List is never prepended to

// Key must be unique among siblings
function List() {
  return (
    <div>
      {users.map(u => <User key={u.id} user={u} />)}
      {posts.map(p => <Post key={p.id} post={p} />)}
      {/* IDs can repeat across different lists */}
    </div>
  );
}

Patterns de Rendu Conditionnel

Plusieurs patterns de rendu conditionnel existent. Les retours anticipés sont les plus propres pour les clauses de garde (loading, error, auth). Les variables d'élément fonctionnent pour if/else au milieu du composant. Le ternaire (cond ? A : B) pour l'un ou l'autre dans JSX. Le ET logique (cond && <X/>) pour afficher/masquer. Le lookup d'objet pour un comportement de type switch. Évitez les ternaires imbriqués — extrayez vers des variables ou composants. Pour les nombres, utilisez le ternaire au lieu de && (0 && <X/> rend 0).

react
function UserDashboard({ user, loading, error }) {
  // 1. Early returns for loading/error states
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <Login />;

  // 2. Element variables
  let greeting;
  if (user.isAdmin) {
    greeting = <h1>Welcome Admin {user.name}</h1>;
  } else {
    greeting = <h1>Welcome {user.name}</h1>;
  }

  return (
    <div>
      {greeting}

      {/* 3. Ternary for either/or */}
      {user.hasNotifications ? <NotificationBadge /> : null}

      {/* 4. && for show/hide */}
      {user.isAdmin && <AdminPanel />}

      {/* 5. Switch via object lookup */}
      {{ free: <FreePlan />, pro: <ProPlan />, enterprise: <EnterprisePlan /> }
        [user.plan]}
    </div>
  );
}

Filtrage & Recherche de Liste

Les listes recherchables/filtrables combinent useState pour les filtres avec useMemo pour la performance. La fonction de filtre vérifie à la fois la requête et la catégorie. Gérez toujours l'état vide (aucun résultat). Pour les grandes listes (1000+ éléments), considérez la virtualisation (react-window, react-virtualized) pour ne rendre que les éléments visibles. Debouncez l'input de recherche pour les appels API. La recherche insensible à la casse utilise toLowerCase(). Pour le filtrage complexe, extrayez vers une fonction séparée ou un hook personnalisé.

react
function SearchableList({ items }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');

  // Memoize filtered results
  const filtered = useMemo(() => {
    return items.filter(item => {
      const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
      const matchesCategory = category === 'all' || item.category === category;
      return matchesQuery && matchesCategory;
    });
  }, [items, query, category]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <select value={category} onChange={e => setCategory(e.target.value)}>
        <option value="all">All</option>
        <option value="food">Food</option>
        <option value="tech">Tech</option>
      </select>

      {filtered.length === 0 ? (
        <p>No results found</p>
      ) : (
        <ul>
          {filtered.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
      )}
    </div>
  );
}

Composants Dynamiques

Le rendu de composant dynamique utilise un objet de lookup pour mapper les types aux composants. C'est courant dans le contenu piloté par CMS, les form builders et les page builders. La carte de composants évite les longues chaînes switch/if. Gérez toujours les types inconnus avec un composant de repli. Spreadez les props ({...block.props}) pour passer toutes les propriétés au composant dynamique. Ce pattern est flexible et extensible — ajouter un nouveau type de bloc nécessite juste d'ajouter à la carte. Capitalisez la variable (Component) pour que JSX la traite comme un composant.

react
// Render different components based on type
const componentMap = {
  text: TextBlock,
  image: ImageBlock,
  video: VideoBlock,
  quote: QuoteBlock,
};

function ContentRenderer({ blocks }) {
  return (
    <div>
      {blocks.map(block => {
        const Component = componentMap[block.type];
        if (!Component) return <UnknownBlock key={block.id} type={block.type} />;

        return <Component key={block.id} {...block.props} />;
      })}
    </div>
  );
}

// Usage
const blocks = [
  { id: 1, type: 'text', props: { content: 'Hello' } },
  { id: 2, type: 'image', props: { src: 'pic.jpg', alt: 'Picture' } },
];
09

Optimisation de Performance

React.memo

React.memo enveloppe un composant pour empêcher les re-renders quand les props n'ont pas changé (comparaison superficielle). Utilisez-le pour les composants qui rendent souvent avec les mêmes props. Le second argument est une fonction de comparaison personnalisée : retournez true pour skipper le re-render, false pour re-render. memo aide seulement si le composant est coûteux à rendre ou est un enfant d'un parent qui rend fréquemment. N'enveloppez pas chaque composant — la mémoïsation a un coût. Combinez avec useCallback/useMemo pour un effet maximal.

react
import { memo } from 'react';

// Memoized component: only re-renders if props change
const ExpensiveCard = memo(function ExpensiveCard({ title, content }) {
  console.log('Card rendered');
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
});

// Custom comparison function
const MyComponent = memo(function MyComponent(props) {
  return <div>{props.value}</div>;
}, (prevProps, nextProps) => {
  // Return true if props are equal (skip re-render)
  return prevProps.value === nextProps.value;
});

Code Splitting

Le code splitting réduit la taille du bundle initial en chargeant les composants à la demande. React.lazy + Suspense permet les imports dynamiques. La prop fallback s'affiche pendant que le composant se charge. Le splitting basé sur les routes (chargement paresseux des composants de page) est le plus impactant. Le splitting basé sur les composants est utile pour les composants lourds (charts, éditeurs) qui ne sont pas nécessaires immédiatement. Chaque import paresseux crée un chunk séparé. Utilisez React.lazy pour les exports par défaut ; pour les exports nommés, enveloppez dans un module.

react
import { lazy, Suspense } from 'react';

// Lazy load component (code-split)
const AdminPanel = lazy(() => import('./AdminPanel'));
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/admin" element={<AdminPanel />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Router>
    </Suspense>
  );
}

// Route-based splitting (most common)
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));

function Page({ showChart }) {
  return (
    <div>
      {showChart && (
        <Suspense fallback={<Spinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Virtualisation pour les Longues Listes

La virtualisation ne rend que les éléments visibles dans une longue liste, améliorant considérablement la performance. react-window et react-virtualized sont des bibliothèques populaires. Au lieu de rendre 10 000 nœuds DOM, seulement ~12 (les visibles) sont rendus, avec un conteneur défilable. Cela réduit la taille DOM et le temps de rendu. Utilisez la virtualisation pour les listes avec 100+ éléments. Le compromis : implémentation plus complexe, problèmes potentiels avec recherche/find (éléments pas dans le DOM). Les listes à hauteur variable nécessitent VariableSizeList.

react
import { FixedSizeList } from 'react-window';

// Virtualized list: only renders visible items
function BigList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

// Without virtualization: rendering 10,000 items is slow
// With virtualization: only ~12 visible items are rendered

Debouncing & Throttling

Le debouncing retarde l'exécution jusqu'à une pause dans l'activité (ex. l'utilisateur arrête de taper). Le throttling limite l'exécution à une fois par intervalle. Les deux empêchent les appels API ou computations excessifs. Le hook useDebounce met à jour la valeur debouncée seulement après que l'utilisateur arrête de taper pendant le délai spécifié. C'est essentiel pour les inputs de recherche, les handlers de resize et les événements de défilement. Pour le throttling, utilisez une bibliothèque comme lodash.throttle ou implémentez avec des timestamps. Nettoyez toujours les timers dans useEffect.

react
import { useState, useEffect } from 'react';

// Debounce hook: delays execution until user stops typing
function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  // API call only fires when user stops typing for 300ms
  useEffect(() => {
    if (debouncedQuery) {
      fetch('/api/search?q=' + debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Profiling & Optimisation

Le composant Profiler mesure les temps de rendu. phase est 'mount' ou 'update'. actualDuration est le temps de rendu en millisecondes. Utilisez React DevTools Profiler pour des flame charts visuels. Avant d'optimiser, profilez pour trouver les vrais goulots d'étranglement — ne devinez pas. Problèmes de performance courants : (1) re-renders inutiles (corrigez avec memo/useMemo/useCallback), (2) calculs coûteux (corrigez avec useMemo), (3) grandes listes (corrigez avec virtualisation), (4) grands bundles (corrigez avec code splitting). L'optimisation prématurée gaspille du temps — mesurez d'abord.

react
import { Profiler } from 'react';

function App() {
  const onRender = (id, phase, actualDuration) => {
    console.log(id + ' ' + phase + ': ' + actualDuration + 'ms');
  };

  return (
    <Profiler id="App" onRender={onRender}>
      <ExpensiveComponent />
    </Profiler>
  );
}

/* Optimization checklist:
1. React.memo for expensive components
2. useMemo for expensive calculations
3. useCallback for props passed to memoized children
4. Code splitting (lazy) for routes
5. Virtualization for long lists
6. Debounce rapid events (search, resize)
7. Avoid inline objects/functions as props
8. Use keys correctly in lists
9. Profile with React DevTools Profiler
10. Check unnecessary re-renders with why-did-you-render
*/
10

Patterns & Error Boundaries

Hooks Personnalisés

Les hooks personnalisés extraient la logique stateful réutilisable dans une fonction préfixée par 'use'. Ils peuvent appeler d'autres hooks. Les hooks personnalisés sont le moyen principal de partager la logique entre composants (remplaçant les HOCs et render props). Retournez un objet pour plusieurs valeurs, ou une valeur/tableau pour des valeurs uniques. Gérez toujours les états de loading et d'erreur. Le pattern d'annulation (flag cancelled) empêche les mises à jour d'état après le démontage. Nommez les hooks avec le préfixe 'use' pour que les règles ESLint fonctionnent.

react
import { useState, useEffect } from 'react';

// Reusable data fetching hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return res.json();
      })
      .then(data => { if (!cancelled) { setData(data); setError(null); }})
      .catch(err => { if (!cancelled) setError(err.message); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch('/api/users/' + id);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <h1>{user.name}</h1>;
}

Hook useLocalStorage

useLocalStorage persiste l'état vers localStorage. L'initialiseur paresseux lit depuis localStorage au montage. Le useEffect écrit vers localStorage chaque fois que la valeur change. Le try/catch gère les erreurs de quota dépassé et les erreurs de parse JSON (données corrompues). Ce pattern fonctionne pour tout état persistant : thèmes, préférences utilisateur, contenu de brouillon. Pour la compatibilité SSR, vérifiez typeof window !== 'undefined'. Pour la synchronisation inter-onglets, écoutez l'événement 'storage'. Hooks similaires : useSessionStorage, useCookie.

react
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved ? JSON.parse(saved) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error('LocalStorage error:', e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

Error Boundaries

Les error boundaries interceptent les erreurs dans les méthodes render/lifecycle des composants enfants, empêchant l'application entière de crasher. Ils doivent être des composants classe (pas d'équivalent hook encore). getDerivedStateFromError met à jour l'état pour afficher l'UI de repli. componentDidCatch logge les erreurs (envoyez à Sentry, LogRocket, etc.). Les error boundaries N'interceptent PAS : les handlers d'événements, le code asynchrone, setTimeout, les erreurs dans la boundary elle-même. Enveloppez des sections spécifiques pour isoler les échecs. Le bouton 'Try again' réinitialise l'état d'erreur.

react
import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Caught error:', error, errorInfo);
    // Send to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div>
            <h1>Something went wrong.</h1>
            <p>{this.state.error?.message}</p>
            <button onClick={() => this.setState({ hasError: false })}>
              Try again
            </button>
          </div>
        )
      );
    }
    return this.props.children;
  }
}

// Usage: wrap components
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

Composants d'Ordre Supérieur (HOC)

Les Composants d'Ordre Supérieur (HOCs) sont des fonctions qui prennent un composant et retournent un composant amélioré. Ils étaient le pattern principal pour partager la logique avant les hooks. Usages courants : authentification, états de loading, theming. Les HOCs peuvent causer le 'wrapper hell' (composants profondément imbriqués) et les collisions de props. Pour le nouveau code, préférez les hooks personnalisés — ils sont plus simples, plus composables et n'ajoutent pas à l'arbre des composants. Les HOCs restent utiles pour les composants classe ou l'intégration avec des bibliothèques qui les nécessitent.

react
// HOC: function that takes a component and returns a new one
function withLoading(Component) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) return <div>Loading...</div>;
    return <Component {...props} />;
  };
}

// HOC for authentication
function withAuth(Component) {
  return function WithAuth(props) {
    const { user } = useContext(AuthContext);
    if (!user) return <Redirect to="/login" />;
    return <Component {...props} user={user} />;
  };
}

// Usage
const UserList = withLoading(withAuth(BaseUserList));

// Note: Prefer hooks over HOCs for new code
// HOCs are mainly for class components or library compatibility

Composants Composés

Les composants composés permettent aux utilisateurs de composer un composant complexe à partir de parties simples. Le parent (Select) fournit le context, et les composants enfants (Trigger, Options, Option) le consomment. Ce pattern est utilisé par des bibliothèques comme Radix UI, Headless UI et React Aria. Avantages : API flexible (les utilisateurs peuvent réordonner/omettre des parties), partage d'état implicite via context, JSX propre. Les composants sont attachés comme propriétés statiques (Select.Trigger). C'est un pattern avancé — utilisez-le pour les bibliothèques UI réutilisables, pas les composants ponctuels.

react
// Compound components: components that work together
function Select({ children, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const context = { value, onChange, isOpen, setIsOpen };

  return (
    <SelectContext.Provider value={context}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}

Select.Trigger = function Trigger({ children }) {
  const { isOpen, setIsOpen } = useContext(SelectContext);
  return <button onClick={() => setIsOpen(!isOpen)}>{children}</button>;
};

Select.Options = function Options({ children }) {
  const { isOpen } = useContext(SelectContext);
  return isOpen ? <div className="options">{children}</div> : null;
};

Select.Option = function Option({ value, children }) {
  const { onChange, setIsOpen } = useContext(SelectContext);
  return (
    <div onClick={() => { onChange(value); setIsOpen(false); }}>
      {children}
    </div>
  );
}

// Usage: clean, declarative API
<Select value={val} onChange={setVal}>
  <Select.Trigger>Choose...</Select.Trigger>
  <Select.Options>
    <Select.Option value="a">Option A</Select.Option>
    <Select.Option value="b">Option B</Select.Option>
  </Select.Options>
</Select>
11

Hooks Personnalisés

Hook useFetch

Les hooks personnalisés extraient la logique stateful réutilisable dans une fonction préfixée par 'use'. useFetch encapsule la récupération de données avec les états loading/error. L'AbortController annule les requêtes en cours quand le composant se démonte ou l'URL change (empêchant les conditions de course et les fuites de mémoire). Incluez toujours le nettoyage dans useEffect pour les opérations asynchrones. Les hooks personnalisés peuvent appeler d'autres hooks (useState, useEffect, useContext). Ils sont le moyen principal de partager la logique entre composants sans render props ou HOCs. Nommez-les avec le préfixe 'use' pour que le linter rules-of-hooks de React fonctionne.

react
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let abortController = new AbortController();
    setLoading(true);
    fetch(url, { ...options, signal: abortController.signal })
      .then((res) => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then((data) => { setData(data); setError(null); })
      .catch((err) => {
        if (err.name !== "AbortError") setError(err.message);
      })
      .finally(() => setLoading(false));
    return () => abortController.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function Profile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{data.name}</div>;
}

Hook useLocalStorage

useLocalStorage synchronise l'état React avec localStorage. L'initialiseur paresseux lit depuis localStorage au premier rendu uniquement. Le useEffect écrit vers localStorage chaque fois que la valeur change. Le try/catch gère les cas où localStorage est plein ou désactivé (navigation privée). Ce hook rend l'état persistant aussi facile que useState. Pour la synchronisation inter-onglets, ajoutez un listener d'événement storage. Pour la sécurité SSR, protégez l'accès window. Ce pattern fonctionne pour sessionStorage aussi — changez juste l'API.

react
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error("LocalStorage write failed:", e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage: persists state across page reloads
function Settings() {
  const [theme, setTheme] = useLocalStorage("theme", "light");
  const [fontSize, setFontSize] = useLocalStorage("fontSize", 14);
  return (
    <div>
      <button onClick={() => setTheme("dark")}>Dark</button>
      <button onClick={() => setTheme("light")}>Light</button>
    </div>
  );
}

Hook useDebounce

useDebounce retarde la mise à jour d'une valeur jusqu'à ce que l'utilisateur arrête de taper pendant le délai spécifié. C'est essentiel pour les inputs de recherche, l'autosave et les appels API déclenchés par l'entrée utilisateur — cela empêche les appels excessifs à chaque frappe. La fonction de nettoyage efface le timeout si la valeur change à nouveau avant l'expiration du délai. La valeur debouncée ne se met à jour qu'après la pause, déclenchant les effets en aval (comme les appels API) moins fréquemment. Pour une exécution immédiate avec un appel trailing, utilisez useThrottle à la place. Combinez avec useFetch pour une recherche-as-you-type efficace.

react
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// Usage: debounce search input
function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`)
        .then((res) => res.json())
        .then(setResults);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Hook usePrevious

usePrevious tire parti du fait que useEffect s'exécute après le rendu — ref.current détient encore l'ancienne valeur pendant le rendu, puis se met à jour à la nouvelle valeur après. C'est un pattern courant pour comparer l'état actuel et précédent. useWindowSize suit les dimensions du viewport avec un listener resize. Nettoyez toujours les listeners d'événements dans le retour useEffect pour éviter les fuites de mémoire. Ces hooks utilitaires démontrent comment les hooks personnalisés encapsulent la logique liée au DOM, rendant les composants plus propres et la logique réutilisable et testable.

react
function usePrevious(value) {
  const ref = useRef(null);
  useEffect(() => {
    ref.current = value; // update AFTER render
  }, [value]);
  return ref.current; // returns previous value during render
}

// Usage: compare current vs previous
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Now: {count}, before: {prevCount}</p>
      {count > prevCount && <p>Increased!</p>}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// useWindowSize hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  useEffect(() => {
    const handler = () =>
      setSize({ width: innerWidth, height: innerHeight });
    window.addEventListener("resize", handler);
    return () => removeEventListener("resize", handler);
  }, []);
  return size;
}

useToggle & useClipboard

useToggle simplifie l'état booléen avec une fonction toggle enveloppée dans useCallback pour une identité stable. useClipboard enveloppe l'API clipboard avec un état de feedback 'copied' qui se réinitialise automatiquement après un timeout. Ces petits hooks utilitaires réduisent le boilerplate et standardisent les patterns courants à travers votre application. Le useCallback dans les deux hooks empêche les re-renders inutiles des enfants mémoïsés. Construire une bibliothèque de petits hooks focalisés (useToggle, useClipboard, useMediaQuery, useOnClickOutside) accélère le développement et assure un comportement cohérent.

react
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((p) => !p), []);
  return [on, toggle, setOn];
}

function useClipboard(timeout = 2000) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback((text) => {
    navigator.clipboard.writeText(text).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), timeout);
    });
  }, [timeout]);
  return [copied, copy];
}

// Usage
function CopyButton({ text }) {
  const [copied, copy] = useClipboard();
  return (
    <button onClick={() => copy(text)}>
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

function Modal({ children }) {
  const [isOpen, toggle] = useToggle(false);
  return (
    <>
      <button onClick={toggle}>Open</button>
      {isOpen && <div className="modal">{children}</div>}
    </>
  );
}
12

Portals

Créer un Portal

createPortal rend les enfants dans un nœud DOM hors de la hiérarchie du composant actuel (typiquement document.body). C'est essentiel pour les modals, tooltips et dropdowns qui doivent échapper aux contraintes CSS parentes (overflow: hidden, contextes d'empilement z-index, transform créant de nouveaux contextes). Malgré le rendu ailleurs dans le DOM, le bouillonnement d'événements React du portal fonctionne toujours comme s'il était dans l'arbre original — les handlers onClick sur les ancêtres se déclenchent toujours. Cela vous donne le meilleur des deux mondes : échappement visuel des contraintes parentes, mais flux d'événements logique préservé.

react
import { createPortal } from "react-dom";

function Modal({ children, onClose }) {
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button onClick={onClose}>✕</button>
        {children}
      </div>
    </div>,
    document.body  // render target outside the DOM hierarchy
  );
}

// Usage: the modal renders at body level,
// escaping any overflow:hidden or z-index stacking contexts
function App() {
  const [show, setShow] = useState(false);
  return (
    <div style={{ overflow: "hidden", position: "relative" }}>
      <button onClick={() => setShow(true)}>Open Modal</button>
      {show && <Modal onClose={() => setShow(false)}>Hello!</Modal>}
    </div>
  );
}

Modal avec Portal & Piège de Focus

Un modal de production nécessite plus qu'un portal : gestion du focus (piéger le focus à l'intérieur, restaurer à la fermeture), gestion de la touche Échap, verrouillage du défilement du body, et clic-extérieur-pour-fermer. Cette implémentation sauvegarde l'élément précédemment focalisé, focalise le modal à l'ouverture, et restaure le focus à la fermeture — essentiel pour les utilisateurs de lecteurs d'écran. Body overflow hidden empêche le défilement de l'arrière-plan. La fonction de nettoyage restaure tout. Pour le piégeage complet du focus (cyclage tab dans le modal), utilisez une bibliothèque comme focus-trap-react. Retournez toujours null quand fermé pour retirer du DOM.

react
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;
    const modal = modalRef.current;
    const previouslyFocused = document.activeElement;
    modal.focus();

    const handleKey = (e) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handleKey);

    // Prevent body scroll
    document.body.style.overflow = "hidden";

    return () => {
      document.removeEventListener("keydown", handleKey);
      document.body.style.overflow = "";
      previouslyFocused.focus(); // restore focus
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div ref={modalRef} tabIndex={-1} className="modal">
        {children}
      </div>
    </div>,
    document.body
  );
}

Tooltips avec Portals

Les tooltips bénéficient des portals car ils doivent déborder des conteneurs parents et éviter le clipping. La position du tooltip est calculée depuis getBoundingClientRect() du trigger et rendue avec position: fixed au niveau du body. Cela évite les problèmes z-index et overflow. Pour le positionnement dynamique (flip près du bord de l'écran), utilisez une bibliothèque comme Floating UI (anciennement Popper.js). Le portal assure que le tooltip n'est jamais clippé par les ancêtres overflow: hidden. Les coordonnées de positionnement fixed sont relatives au viewport, rendant le calcul simple.

react
function Tooltip({ children, text }) {
  const [visible, setVisible] = useState(false);
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  const targetRef = useRef(null);

  const show = () => {
    const rect = targetRef.current.getBoundingClientRect();
    setCoords({ x: rect.left, y: rect.top - 40 });
    setVisible(true);
  };

  return (
    <>
      <span
        ref={targetRef}
        onMouseEnter={show}
        onMouseLeave={() => setVisible(false)}
      >
        {children}
      </span>
      {visible && createPortal(
        <div style={{ position: "fixed", left: coords.x, top: coords.y }}
             className="tooltip">
          {text}
        </div>,
        document.body
      )}
    </>
  );
}

// Usage
<Tooltip text="Click to save">💾</Tooltip>

Menus Dropdown avec Portals

Les menus dropdown font face aux mêmes problèmes overflow/z-index que les tooltips. Les portals résolvent le problème visuel. Le handler clic-extérieur vérifie si la cible du clic est hors du trigger ref. Le listener de défilement en phase de capture (true comme troisième argument) ferme le menu à tout défilement, empêchant le menu de se détacher de son trigger. Pour la production, utilisez Floating UI qui gère la détection de bord, le flipping, le shifting et les mises à jour automatiques de positionnement au défilement/resize. Portals + logique de positionnement appropriée = dropdowns robustes qui fonctionnent dans tout contexte de disposition.

react
function Dropdown({ trigger, children }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState({ top: 0, left: 0 });
  const ref = useRef(null);

  const handleOpen = () => {
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 4, left: rect.left });
    setOpen(true);
  };

  useEffect(() => {
    if (!open) return;
    const handleClick = (e) => {
      if (!ref.current?.contains(e.target)) setOpen(false);
    };
    const handleScroll = () => setOpen(false); // close on scroll
    document.addEventListener("mousedown", handleClick);
    window.addEventListener("scroll", handleScroll, true);
    return () => {
      document.removeEventListener("mousedown", handleClick);
      window.removeEventListener("scroll", handleScroll, true);
    };
  }, [open]);

  return (
    <>
      <div ref={ref} onClick={handleOpen}>{trigger}</div>
      {open && createPortal(
        <div style={{ position: "fixed", ...pos }} className="dropdown">
          {children}
        </div>,
        document.body
      )}
    </>
  );
}

Bouillonnement d'Événements Portal

Une caractéristique clé des React Portals : le bouillonnement d'événements suit l'arbre des composants React, pas l'arbre DOM. onClick sur un composant parent se déclenche même quand l'enfant est portalé vers document.body. Cela signifie que context, état et délégation d'événements fonctionnent naturellement. Cependant, l'héritage CSS NE traverse PAS la limite du portal — les styles sur le parent ne se propagent pas au contenu portalé car ils sont dans des sous-arbres DOM différents. Vous devez explicitement appliquer le CSS (via classes ou variables CSS sur :root) pour styliser le contenu du portal. Cette séparation est généralement souhaitable pour modals/tooltips.

react
function PortalExample() {
  // Despite rendering in document.body, events bubble
  // through the React tree, not the DOM tree
  return (
    <div onClick={() => console.log("Parent clicked!")}>
      <p>Click the button — parent handler fires!</p>
      {createPortal(
        <button onClick={() => console.log("Button clicked!")}>
          I'm in a portal
        </button>,
        document.body
      )}
    </div>
  );
}
// Clicking logs: "Button clicked!" then "Parent clicked!"

// This means context, state, and event delegation
// all work as if the portal were inline

// But CSS inheritance does NOT cross the portal boundary:
// document.body styles won't inherit into the portal content
// unless you explicitly apply them
13

Suspense & Chargement Paresseux

React.lazy & Suspense

React.lazy importe dynamiquement un composant, créant un bundle séparé qui se charge à la demande (code splitting). Enveloppez les composants paresseux dans <Suspense> avec un fallback (état de chargement) affiché pendant le téléchargement du chunk. Cela réduit la taille du bundle initial — les utilisateurs ne téléchargent que le code pour les pages qu'ils visitent. Chaque appel lazy() crée un chunk séparé. Pour le splitting basé sur les routes, chargez paresseusement chaque composant de page. Le fallback peut être n'importe quel nœud React (spinner, skeleton, texte). Suspense peut envelopper plusieurs composants paresseux — le fallback s'affiche jusqu'à ce que tous soient prêts.

react
import { lazy, Suspense } from "react";

// Lazy-load component (code-split)
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

function App() {
  return (
    <Suspense fallback={<div>Loading page...</div>}>
      <nav>
        <button onClick={() => setPage("dash")}>Dashboard</button>
        <button onClick={() => setPage("settings")}>Settings</button>
      </nav>
      {page === "dash" && <Dashboard />}
      {page === "settings" && <Settings />}
    </Suspense>
  );
}

Suspense Imbriqué

Les limites Suspense imbriquées créent un effet de 'pelage' où le contenu se révèle progressivement à mesure que chaque chunk se charge. Le Suspense externe affiche son fallback en premier ; à mesure que les composants internes se chargent, ils se révèlent indépendamment. Cela empêche un seul composant lent de bloquer toute la page. Placez les limites Suspense stratégiquement : autour des pages au niveau route (grossier), autour des sections majeures (moyen), et autour des widgets indépendants (fin). Trop de limites créent un chargement saccadé ; trop peu créent de longues attentes. La clé est de faire correspondre les limites aux unités de contenu perçues par l'utilisateur.

react
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Suspense fallback={<MainSkeleton />}>
    <MainContent />  {/* loads first */}
    <Suspense fallback={<CommentsSkeleton />}>
      <Comments />  {/* loads independently, doesn't block MainContent */}
    </Suspense>
  </Suspense>
  <Sidebar />
</Suspense>

// Suspense "peeling" effect:
// 1. PageSkeleton shows
// 2. Header + Sidebar load → PageSkeleton peels away
// 3. MainSkeleton shows until MainContent loads
// 4. MainContent shows, CommentsSkeleton shows
// 5. Comments load → everything visible

Lazy avec Error Boundaries

Le chargement paresseux peut échouer (problèmes réseau, déploiements invalidant les URLs de chunk). Les Error Boundaries interceptent ces erreurs et affichent l'UI de repli. Enveloppez toujours Suspense + lazy dans un ErrorBoundary. Le componentDidCatch logge les erreurs pour la surveillance. Pour la logique de retry, vous pouvez réinitialiser l'état de l'ErrorBoundary ou recharger la page. Un pattern courant est un bouton retry qui ré-importe le chunk. Sans error boundaries, un échec de chargement de chunk fait crasher toute l'application. C'est critique pour la production — la fiabilité réseau n'est jamais 100%.

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, info) {
    console.error("Chunk load failed:", error, info);
  }
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>Failed to load. {this.state.error.message}</p>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap lazy components: network failures need handling
<ErrorBoundary>
  <Suspense fallback={<Loader />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

Récupération de Données avec Suspense

Le hook use() de React 19 permet Suspense pour la récupération de données. Contrairement à useEffect, use() suspend le composant jusqu'à ce que la promise se résolve — la limite Suspense la plus proche affiche son fallback. Plusieurs appels use() dans le même composant se résolvent en parallèle (récupération parallèle). Cela élimine la gestion manuelle de l'état de loading. La promise peut être cachée hors React pour empêcher la re-récupération au re-render. Note : use() ne peut être appelé que dans le rendu ou à l'intérieur des hooks. Pour React 18, utilisez des bibliothèques comme React Query ou SWR qui s'intègrent à Suspense.

react
// React 18+ Suspense for data fetching (experimental)
import { use } from "react"; // React 19+

// Wrap a promise with use()
function UserProfile({ userId }) {
  // 'use' suspends until the promise resolves
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

// Parent provides Suspense boundary
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  );
}

// Concurrent: multiple suspends resolve together
function Dashboard() {
  const user = use(fetchUser(1));
  const posts = use(fetchPosts(user.id));
  // Both fetch in parallel, Suspense shows until all resolve
  return <div>{user.name}: {posts.length} posts</div>;
}

Suspense List (Orchestration)

SuspenseList orchestre l'ordre de révélation de multiples limites Suspense. revealOrder='forwards' affiche les éléments dans l'ordre (l'élément 2 ne se révélera pas avant que l'élément 1 soit prêt, même si 2 se charge en premier) — empêche le saut de contenu. 'together' attend tout avant de révéler. 'backwards' révèle de bas en haut. tail='collapsed' masque les états de chargement pour les éléments pas encore commencés ; 'hidden' masque tous les fallbacks. C'est utile pour les feeds et listes où l'ordre compte. Note : SuspenseList était expérimental et son API peut changer — vérifiez les docs React actuelles pour la disponibilité.

react
// SuspenseList controls reveal order of multiple Suspense
import { SuspenseList, Suspense } from "react";

function Article({ id }) {
  const data = use(fetchArticle(id));
  return <article>{data.title}</article>;
}

function Feed() {
  return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
      {/* "forwards": reveals top-to-bottom as they load */}
      {/* "together": waits for all, reveals together */}
      {/* "backwards": reveals bottom-to-top */}

      <Suspense fallback={<Skeleton />}>
        <Article id={1} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={2} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={3} />
      </Suspense>
    </SuspenseList>
  );
}
14

React Router

Routage de Base

React Router v6 utilise <BrowserRouter> comme racine, <Routes> pour définir le matching de routes, et <Route> avec la prop element (pas component). <Link> crée des liens de navigation qui utilisent l'History API (pas de rechargement de page). Les segments dynamiques (:id) sont accessibles via useParams(). path='*' est un catch-all pour les 404s. Les routes sont matchées par meilleure correspondance, pas par ordre. Pour les paramètres de recherche d'URL (?q=search), utilisez useSearchParams(). BrowserRouter nécessite une configuration serveur pour servir index.html pour toutes les routes (SPA fallback).

react
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User 123</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

function UserPage() {
  const { id } = useParams();
  return <h1>User ID: {id}</h1>;
}

Routes Imbriquées & Outlet

Les routes imbriquées créent des hiérarchies de disposition. L'élément de la route parent doit inclure <Outlet /> où les routes enfants rendent. La route index rend au chemin du parent. Les routes profondément imbriquées (users/:id) créent des dispositions imbriquées — la disposition Users enveloppe UserDetail. C'est puissant pour les dashboards avec des barres latérales/en-têtes persistants. useOutlet() donne accès à l'élément enfant. L'URL /users/123 rend Layout → Users → UserDetail, chacun contribuant sa disposition. Cela remplace le rendu conditionnel manuel des dispositions.

react
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserDetail />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <nav>Navigation here</nav>
      <Outlet />  {/* Child routes render here */}
    </div>
  );
}

function Users() {
  return (
    <div>
      <h2>Users</h2>
      <Outlet />  {/* Nested :id route renders here */}
    </div>
  );
}

Navigation & Redirections

useNavigate retourne une fonction pour la navigation programmatique. navigate('/path', { replace: true }) remplace l'historique (pas de bouton retour). Passez state pour transporter des données vers la route suivante (ex. où retourner après login). <Navigate> est le composant de redirection déclaratif — utilisez-le dans le rendu pour les auth guards. NavLink fournit isActive pour styliser les liens actifs. useLocation donne l'URL actuelle, pathname, search, hash et state. Pour les redirections après actions (soumission de formulaire), utilisez navigate. Pour les redirections conditionnelles dans le rendu, utilisez <Navigate>.

react
import { useNavigate, Navigate, NavLink, useLocation } from "react-router-dom";

function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  const handleLogin = async () => {
    await auth.login();
    // Redirect to intended page or home
    const from = location.state?.from || "/";
    navigate(from, { replace: true });
  };
  return <button onClick={handleLogin}>Login</button>;
}

// Declarative redirect
function ProtectedRoute({ user, children }) {
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

// NavLink: active styling
<NavLink to="/about" className={({ isActive }) =>
  isActive ? "nav-active" : "nav"
}>
  About
</NavLink>

Loaders & Chargement de Données

React Router v6.4+ (data router) ajoute les loaders (s'exécutent avant le rendu de la route) et les actions (gèrent les soumissions de formulaire). useLoaderData() accède aux données du loader — plus de useEffect pour la récupération de données de route. Les loaders s'exécutent en parallèle pour les routes imbriquées. errorElement intercepte les erreurs des loaders/actions. Les Actions traitent les soumissions de formulaire via <Form method='post'> — useActionData() retourne le résultat. Ce pattern (inspiré de Remix) co-localise la logique de données avec les routes. Les APIs de données nécessitent createBrowserRouter/createHashRouter, pas <BrowserRouter>.

react
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/users/:id",
    element: <UserPage />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/users/${params.id}`);
      if (!res.ok) throw new Response("Not found", { status: 404 });
      return res.json();
    },
    errorElement: <ErrorPage />,
  },
]);

function UserPage() {
  const user = useLoaderData(); // data from loader
  return <h1>{user.name}</h1>;
}

// Action for form submissions
{
  path: "/users/new",
  element: <NewUser />,
  action: async ({ request }) => {
    const formData = await request.formData();
    const res = await fetch("/api/users", {
      method: "POST",
      body: formData,
    });
    return redirect(`/users/${res.id}`);
  },
}

function App() {
  return <RouterProvider router={router} />;
}

Route Guards & Routes Protégées

Les route guards protègent les routes selon l'état d'auth ou les rôles. RequireAuth redirige les utilisateurs non authentifiés vers le login, préservant la destination prévue dans location.state pour la redirection post-login. RequireRole ajoute le contrôle d'accès basé sur les rôles. Composez les guards en les enveloppant (RequireAuth > RequireRole > component). Pour les routes de disposition, vous pouvez aussi utiliser element={<RequireAuth><Outlet/></RequireAuth>} pour protéger toutes les routes enfants à la fois. Vérifiez toujours l'auth côté serveur aussi — les guards côté client sont pour l'UX, pas la sécurité. Le pattern s'étend à toute condition : abonnement, feature flags, etc.

react
function RequireAuth({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={
            <RequireAuth><Dashboard /></RequireAuth>
          } />
          <Route path="admin" element={
            <RequireAuth><RequireRole role="admin"><Admin /></RequireRole></RequireAuth>
          } />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function RequireRole({ role, children }) {
  const { user } = useAuth();
  if (user?.role !== role) return <Navigate to="/forbidden" />;
  return children;
}
15

Gestion d'État (Context & Redux)

Pattern Context API

Context fournit un état global sans prop drilling. Créez un context, enveloppez les consommateurs dans un Provider, et accédez via useContext. Le hook personnalisé useAuth ajoute la vérification d'erreurs et est la surface d'API recommandée. Context est idéal pour les mises à jour basse-fréquence (auth, thème, locale). Pour les changements d'état haute-fréquence, Context fait re-render tous les consommateurs à chaque changement — utilisez useReducer pour l'état complexe ou séparez les contexts. Co-localisez toujours le provider avec l'état qu'il gère. La valeur de Context devrait être mémoïsée avec useMemo/useCallback si elle contient des fonctions.

react
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const login = async (credentials) => {
    const user = await api.login(credentials);
    setUser(user);
  };
  const logout = () => setUser(null);
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be inside AuthProvider");
  return ctx;
}

// Usage
function Navbar() {
  const { user, logout } = useAuth();
  return user
    ? <button onClick={logout}>Logout {user.name}</button>
    : <Link to="/login">Login</Link>;
}

// Wrap app: <AuthProvider><App /></AuthProvider>

useReducer + Context

useReducer + Context est le pattern recommandé pour l'état global complexe sans bibliothèques externes. Le reducer centralise la logique d'état (transitions prévisibles, testables). Le provider mémoïse la valeur pour empêcher les re-renders inutiles. Les valeurs dérivées (total) sont calculées dans useMemo. Ce pattern gère le panier, l'état de formulaire, les wizards multi-étapes, etc. Pour les applications vraiment complexes avec middleware, time-travel debugging, ou de nombreuses slices indépendantes, considérez Redux Toolkit ou Zustand. Mais pour la plupart des applications, useReducer + Context suffit et n'a aucune dépendance.

react
const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD":
      const existing = state.find((i) => i.id === action.item.id);
      if (existing) {
        return state.map((i) =>
          i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...state, { ...action.item, qty: 1 }];
    case "REMOVE":
      return state.filter((i) => i.id !== action.id);
    case "CLEAR":
      return [];
    default:
      return state;
  }
}

function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);
  const value = useMemo(() => ({
    items,
    total: items.reduce((s, i) => s + i.price * i.qty, 0),
    addItem: (item) => dispatch({ type: "ADD", item }),
    removeItem: (id) => dispatch({ type: "REMOVE", id }),
  }), [items]);
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

Bases de Redux Toolkit

Redux Toolkit (RTK) est la façon moderne et recommandée d'utiliser Redux. createSlice auto-génère les action creators et reducers. Il utilise Immer en interne, donc vous « mutez » l'état directement (state.value += 1) et Immer produit la mise à jour immuable. configureStore configure le store avec des valeurs par défaut sensées (Redux DevTools, thunk middleware). useSelector lit l'état ; useDispatch dispatche les actions. RTK élimine le boilerplate Redux (pas de switch, pas de constantes de type d'action). Pour la logique asynchrone, utilisez createAsyncThunk. RTK Query (inclus) gère la récupération de données et le cache.

react
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; }, // Immer: mutate safely
    decrement: (state) => { state.value -= 1; },
    addBy: (state, action) => { state.value += action.payload; },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

export const { increment, decrement, addBy } = counterSlice.actions;

// Usage in component
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(addBy(5))}>+5</button>
    </div>
  );
}

Zustand (Alternative Légère)

Zustand est une bibliothèque de gestion d'état minimale — pas de providers, pas de boilerplate. Créez un store avec create(), accédez via des hooks avec des fonctions sélecteur. Les sélecteurs empêchent les re-renders : seuls les composants utilisant la slice modifiée re-render. Cela résout le problème de re-render de Context sans la complexité de Redux. Pour les sélecteurs d'objet (retournant {a, b}), utilisez la comparaison shallow pour empêcher les re-renders inutiles. Zustand supporte le middleware (persist, devtools, immer). Idéal pour les applications petites à moyennes où Redux est excessif mais Context cause trop de re-renders. L'API est minuscule mais puissante.

react
import { create } from "zustand";

const useStore = create((set, get) => ({
  count: 0,
  user: null,
  increment: () => set((state) => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
  reset: () => set({ count: 0, user: null }),
  // Access other state with get()
  doubleCount: () => get().count * 2,
}));

// Usage: select only what you need (prevents re-renders)
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}

// Multiple selections
function Profile() {
  const { user, setUser } = useStore(
    (state) => ({ user: state.user, setUser: state.setUser })
  );
  // Use shallow comparison for object selectors
  // import { shallow } from "zustand/shallow";
  // useStore(selector, shallow);
}

React Query (État Serveur)

React Query (TanStack Query) gère l'état serveur — données récupérées depuis les APIs. Il gère le cache, la re-récupération en arrière-plan, les données périmées, les mises à jour optimistes et la pagination automatiquement. queryKey identifie les données cachées (comme une clé de cache). staleTime contrôle combien de temps les données sont considérées fraîches. invalidateQueries après les mutations re-récupère les requêtes dépendantes. Contrairement à Redux (état client), React Query est conçu pour les données serveur asynchrones. Il élimine les états manuels loading/error, la récupération useEffect et la gestion de cache. Pour la plupart des applications, React Query + état local (useState/useReducer) remplace Redux entièrement.

react
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

function Users() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then((r) => r.json()),
    staleTime: 60000, // fresh for 60s
    refetchOnWindowFocus: true,
  });

  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch("/api/users", { method: "POST", body: JSON.stringify(newUser) }),
    onSuccess: () => queryClient.invalidateQueries(["users"]),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <div>
      {data.map((u) => <div key={u.id}>{u.name}</div>)}
      <button onClick={() => mutation.mutate({ name: "New" })}>Add</button>
    </div>
  );
}
16

Tests (React Testing Library)

Test de Composant de Base

React Testing Library (RTL) teste les composants comme les utilisateurs interagissent avec eux — par rôle, label et texte, pas les détails d'implémentation. getByRole est la requête préférée (teste l'accessibilité aussi). userEvent simule les interactions utilisateur réelles (frappe, clic) plus précisément que fireEvent. Les tests devraient éviter de tester l'état interne ; à la place, vérifiez la sortie visible et le comportement. Si vous ne pouvez pas requêter par rôle, utilisez getByLabelText, getByText ou getByDisplayValue. Évitez getByTestId sauf si nécessaire. Cette approche rend les tests résistants au refactoring — ils testent ce que les utilisateurs voient et font.

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";

test("counter increments on click", async () => {
  const user = userEvent.setup();
  render(<Counter />);

  // Find by accessible role (not test-id)
  expect(screen.getByRole("heading")).toHaveTextContent("0");

  await user.click(screen.getByRole("button", { name: /increment/i }));

  expect(screen.getByRole("heading")).toHaveTextContent("1");
});

test("displays error for invalid input", async () => {
  const user = userEvent.setup();
  render(<Form />);

  await user.type(screen.getByLabelText(/email/i), "not-an-email");
  await user.click(screen.getByRole("button", { name: /submit/i }));

  expect(screen.getByRole("alert")).toHaveTextContent(/invalid email/i);
});

Tester les Hooks

renderHook teste les hooks personnalisés en isolation. result.current détient la valeur de retour du hook. Toutes les mises à jour d'état doivent être enveloppées dans act() pour assurer que React les traite synchronement. rerender vous permet de tester les effets qui dépendent du changement de props. Pour les hooks asynchrones (useEffect avec fetch), utilisez waitFor ou les requêtes findBy (qui attendent les mises à jour). Tester les hooks directement est plus rapide et plus focalisé que de tester à travers un composant. Cependant, testez aussi les hooks à travers des tests d'intégration de composant pour vérifier l'usage réel. renderHook est disponible dans @testing-library/react v13+.

react
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  // Wrap state updates in act()
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);

  act(() => result.current.reset());
  expect(result.current.count).toBe(0);
});

// Testing with initial props that change
test("useEffect runs on dependency change", () => {
  const { result, rerender } = renderHook(
    ({ id }) => useFetchUser(id),
    { initialProps: { id: 1 } }
  );
  rerender({ id: 2 });
  // Effect re-ran with new id
});

Tests Async & Mocking

MSW (Mock Service Worker) intercepte les requêtes réseau au niveau du service worker — les tests utilisent le vrai fetch() mais obtiennent des réponses mockées. C'est plus réaliste que de mocker fetch directement. setupServer pour Node (Jest), setupWorker pour le navigateur. Le lifecycle beforeAll/afterAll gère le serveur. server.use() remplace les handlers par test. Les requêtes findBy (async) attendent que les éléments apparaissent — utilisez pour le rendu async. queryBy retourne null si non trouvé (pour affirmer l'absence). waitFor interroge pour une condition. MSW peut aussi être utilisé pour le mocking de développement et Storybook.

react
import { render, screen, waitFor } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { UserProfile } from "./UserProfile";

// Mock API with MSW (Mock Service Worker)
const server = setupServer(
  rest.get("/api/users/:id", (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: "Alice" }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test("displays user after fetch", async () => {
  render(<UserProfile id={1} />);

  // findBy waits for async update
  expect(await screen.findByText("Alice")).toBeInTheDocument();
  expect(screen.queryByText("Loading")).not.toBeInTheDocument();
});

test("shows error on fetch failure", async () => {
  server.use(
    rest.get("/api/users/:id", (req, res, ctx) =>
      res(ctx.status(500))
    )
  );
  render(<UserProfile id={1} />);
  expect(await screen.findByText(/error/i)).toBeInTheDocument();
});

Tester Context & Providers

Créez un utilitaire de rendu personnalisé qui enveloppe les composants avec les providers requis (Theme, Auth, Router, etc.). Cela évite de répéter la configuration des providers dans chaque test. Ré-exportez les fonctions RTL depuis votre fichier test-utils pour que les tests importent depuis là. Pour les tests de router, utilisez MemoryRouter (pas BrowserRouter) avec initialEntries pour définir l'URL de départ — pas d'historique de navigateur réel nécessaire. Pour Redux, enveloppez dans un Provider de test avec un store réel ou mock. Ce pattern garde les tests propres et assure que tous les composants ont leur context requis. C'est la configuration standard pour toute infrastructure de test React.

react
// Custom render that wraps with providers
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";

function customRender(ui, { theme = "light", ...options } = {}) {
  function Wrapper({ children }) {
    return <ThemeProvider initialTheme={theme}>{children}</ThemeProvider>;
  }
  return render(ui, { wrapper: Wrapper, ...options });
}

// Re-export everything
export * from "@testing-library/react";
export { customRender as render };

// In test files, import from your test-utils:
// import { render, screen } from "../test-utils";

test("button uses theme color", () => {
  customRender(<Button>Click</Button>, { theme: "dark" });
  expect(screen.getByRole("button")).toHaveClass("btn-dark");
});

// Testing with router
import { MemoryRouter } from "react-router-dom";
render(
  <MemoryRouter initialEntries={["/users/123"]}>
    <App />
  </MemoryRouter>
);

Tester les Événements & Interactions

userEvent.setup() crée une instance utilisateur pour des interactions réalistes : type (caractère par caractère), click, tab, keyboard (avec key codes comme {Escape}, {Enter}), selectOptions, upload et plus. Attendez toujours les interactions utilisateur — elles sont async. Tester la navigation au clavier est crucial pour l'accessibilité. Pour les tests de formulaire, remplissez tous les champs et vérifiez que onSubmit reçoit les bonnes données. userEvent est préféré à fireEvent car il simule le comportement réel du navigateur (focus, blur, input events dans le bon ordre). Testez le flux utilisateur complet, pas les handlers d'événements individuels.

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("form submission flow", async () => {
  const onSubmit = jest.fn();
  const user = userEvent.setup();
  render(<LoginForm onSubmit={onSubmit} />);

  // Fill form fields
  await user.type(screen.getByLabelText(/email/i), "[email protected]");
  await user.type(screen.getByLabelText(/password/i), "password123");

  // Submit
  await user.click(screen.getByRole("button", { name: /login/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: "[email protected]",
    password: "password123",
  });
});

test("keyboard navigation", async () => {
  const user = userEvent.setup();
  render(<Modal />);
  await user.tab(); // focus first element
  expect(screen.getByRole("button", { name: /close/i })).toHaveFocus();
  await user.keyboard("{Escape}"); // press Escape
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
17

TypeScript + React

Typage des Props de Composant

TypeScript avec React fournit la sécurité de type pour les props. Utilisez des interfaces ou des alias de type pour les props. Les props optionnelles utilisent ?. Les types union (variant) contraignent les valeurs. React.ReactNode accepte tout contenu rendable (chaînes, éléments, tableaux). Étendre les attributs HTML (React.InputHTMLAttributes) permet à votre composant d'accepter tous les attributs natifs (placeholder, onChange, etc.) tout en ajoutant des props personnalisées. Le spread {...rest} passe les attributs restants à l'élément natif. Ce pattern crée des composants flexibles et type-safe. Exportez toujours les types de props pour que les consommateurs puissent les référencer.

react
// Basic props
interface ButtonProps {
  text: string;
  onClick: () => void;
  variant?: "primary" | "secondary"; // union type
  disabled?: boolean;
}

function Button({ text, onClick, variant = "primary", disabled }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
}

// Children prop
interface CardProps {
  title: string;
  children: React.ReactNode; // any renderable content
}

// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
      {error && <span className="error">{error}</span>}
    </label>
  );
}

Hooks avec TypeScript

TypeScript ajoute la sécurité de type aux hooks. useState<T> spécifie le type d'état ; useState<T | null>(null) pour l'état nullable. useRef<T>(null) type la ref — current est T | null. Pour useContext, définissez un type de context et lancez si undefined (pour que les consommateurs obtiennent le type non-undefined). Pour useReducer, tapez l'Action comme une union discriminée — le switch sur action.type rétrécit le type dans chaque case, vous donnant un accès type-safe au payload. Ces patterns éliminent les erreurs d'exécution dues à l'accès undefined et aux payloads d'action incorrects.

react
// useState with types
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

// useRef
const inputRef = useRef<HTMLInputElement>(null);
// Access: inputRef.current?.focus()

// useContext
interface ThemeContextType {
  theme: "light" | "dark";
  toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
  return ctx; // ctx is now ThemeContextType, not undefined
}

// useReducer
type Action = { type: "increment" } | { type: "set"; value: number };
const [state, dispatch] = useReducer((state: number, action: Action) => {
  switch (action.type) {
    case "increment": return state + 1;
    case "set": return action.value;
  }
}, 0);

Composants Génériques

Les composants et hooks génériques fonctionnent avec n'importe quel type de données tout en maintenant la sécurité de type. Le paramètre de type <T> est inféré depuis la prop items, donc renderItem et keyExtractor reçoivent automatiquement le bon type. C'est comment TypeScript recrée les composants utilitaires génériques (List, Table, Select) avec une sécurité de type complète. Les hooks génériques (useArray<T>) préservent de même les types à travers les opérations. L'insight clé : TypeScript infère T depuis l'usage, donc les consommateurs ont rarement besoin de le spécifier explicitement. Ce pattern est essentiel pour construire des bibliothèques de composants réutilisables et type-safe.

react
// Generic component: works with any type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage: TypeScript infers T from items
<List
  items={[{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

// Generic hook
function useArray<T>(initial: T[]) {
  const [array, setArray] = useState(initial);
  const push = (item: T) => setArray((prev) => [...prev, item]);
  return { array, push, setArray };
}

Types d'Événement & Refs

Les types d'événement React sont spécifiques : ChangeEvent pour les inputs, FormEvent pour les formulaires, MouseEvent pour les clics. Chacun est générique sur le type d'élément (e.target est correctement typé). forwardRef avec TypeScript nécessite deux paramètres de type : le type de ref et le type de props. forwardRef est nécessaire quand un composant doit exposer une ref à un élément DOM (pour focus, mesure, etc.). Définissez toujours displayName pour les composants forwardRef/memo pour un meilleur débogage DevTools. React 19 permet ref comme prop régulière, réduisant le besoin de forwardRef, mais il reste courant dans le code existant.

react
// Event types
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // string
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
  console.log(e.clientX, e.clientY);
}

// forwardRef with TypeScript
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...props }, ref) => (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  )
);
Input.displayName = "Input";

// Usage: const ref = useRef<HTMLInputElement>(null);
// <Input ref={ref} label="Email" />

Types Utilitaires pour les Props

Les types utilitaires TypeScript sont puissants pour la composition de props. Pick sélectionne des props spécifiques (pour les composants sous-ensemble). Omit exclut des props (pour remplacer le comportement). Partial rend toutes les props optionnelles (pour les patterns de props par défaut). ComponentProps<typeof Component> extrait le type de props d'un composant — utile pour envelopper/étendre des composants existants. Record<K, V> crée un type mappant les clés aux valeurs (idéal pour les maps variant-vers-classe). Ces utilitaires permettent des définitions de props DRY et type-safe sans répéter les interfaces. Maîtrisez-les pour écrire du code React + TypeScript maintenable.

react
// Pick: select specific props
interface ButtonProps {
  text: string;
  onClick: () => void;
  color: string;
  size: "sm" | "md" | "lg";
}
type IconButtonProps = Pick<ButtonProps, "onClick" | "size"> & {
  icon: React.ReactNode;
};

// Omit: exclude specific props
type LinkButtonProps = Omit<ButtonProps, "onClick"> & {
  href: string;
};

// Partial: all props optional (for defaults)
type DefaultProps = Partial<ButtonProps>;

// ComponentProps: extract props from existing component
type MyButtonProps = React.ComponentProps<typeof Button> & {
  variant?: "custom";
};

// ReturnType: type of a function's return
type User = ReturnType<typeof fetchUser>;

// Record for prop maps
type ButtonVariants = Record<"primary" | "danger" | "ghost", string>;
18

Fonctionnalités Concurrentes (useTransition, useDeferredValue)

useTransition

useTransition marque une mise à jour d'état comme non-urgente (transition). Les mises à jour urgentes (valeur d'input) rendent immédiatement pour la réactivité ; les mises à jour non-urgentes (filtrage de 10 000 éléments) peuvent être interrompues si l'utilisateur tape à nouveau. isPending indique que la transition est en cours (affiche un indicateur de loading subtil). Cela empêche l'UI de geler pendant les rendus coûteux. L'insight clé : React peut interrompre et discarder les transitions périmées, gardant l'UI réactive. Utilisez pour le filtrage de recherche, le changement d'onglet et toute mise à jour d'état qui déclenche un rendu lourd. N'enveloppez pas les mises à jour urgentes (frappe, clic) dans les transitions.

react
import { useTransition, useState } from "react";

function SearchResults() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = (value) => {
    setQuery(value); // urgent: update input immediately
    startTransition(() => {
      // non-urgent: heavy filtering can be interrupted
      const filtered = heavyFilter(allItems, value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <span>Updating...</span>}
      <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
}

useDeferredValue

useDeferredValue est la contrepartie déclarative à useTransition. Il retourne une copie différée d'une valeur qui se met à jour avec une priorité plus basse. L'input se met à jour immédiatement (urgent) ; la liste coûteuse re-render avec la valeur différée (non-urgent). React.memo sur le composant Results est crucial — il empêche le re-render à chaque frappe, seulement quand deferredQuery change. isStale (comparant courant vs différé) vous permet d'afficher un indicateur visuel (atténué, spinner). Utilisez useDeferredValue quand vous ne pouvez pas contrôler la mise à jour d'état (ex. la valeur vient des props). Utilisez useTransition quand vous contrôlez la mise à jour.

react
function Search() {
  const [query, setQuery] = useState("");
  // query updates immediately; deferredQuery lags behind
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results query={deferredQuery} isStale={isStale} />
    </div>
  );
}

// Memoize the expensive component so it only re-renders
// when deferredQuery changes (not on every keystroke)
const Results = React.memo(function Results({ query, isStale }) {
  const items = expensiveSearch(query); // heavy computation
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {items.map((i) => <div key={i.id}>{i.name}</div>)}
    </div>
  );
});

useOptimistic (React 19)

useOptimistic (React 19) implémente les mises à jour optimistes — l'UI se met à jour immédiatement avec le résultat attendu, puis se réconcilie avec la réponse réelle du serveur. L'état optimiste s'affiche pendant l'opération asynchrone ; quand les données réelles arrivent (le composant re-render avec les nouvelles props), la valeur optimiste est automatiquement remplacée. Si l'opération échoue, la mise à jour optimiste revient simplement au prochain rendu avec les props inchangées. Cela élimine la logique manuelle de mise à jour optimiste (suivi de l'état pending, revertion sur erreur). Marquez les éléments pending (pending: true) pour afficher les indicateurs de chargement. Parfait pour les likes, commentaires et toggles.

react
import { useOptimistic } from "react";

function ThumbsUp({ likes, addLike }) {
  // Optimistic state: updates immediately, reverts on error
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state, newLike) => [...state, newLike]
  );

  const handleClick = async () => {
    const newLike = { id: Date.now(), pending: true };
    addOptimisticLike(newLike); // instant UI update
    try {
      await addLike(newLike); // actual API call
    } catch {
      // Reverts automatically on re-render with real data
    }
  };

  return (
    <div>
      <button onClick={handleClick}>👍 {optimisticLikes.length}</button>
      {optimisticLikes.some((l) => l.pending) && <span>Saving...</span>}
    </div>
  );
}

use (Hook React 19)

use() est le nouveau hook de React 19 qui lit le context ou les promises. Contrairement à useContext, use() peut être appelé conditionnellement (à l'intérieur des if, boucles) — il n'a pas la restriction rules-of-hooks. Pour les promises, use() suspend le composant jusqu'à ce que la promise se résolve (nécessite une limite Suspense). La promise est créée dans le parent et passée comme prop — cela commence la récupération pendant le rendu (pas dans useEffect), permettant aux cascades de commencer plus tôt. La même promise peut être passée à plusieurs composants (déduplication). use() comble le fossé entre le context synchrone et les données async.

react
import { use } from "react";

// Read context with use (works in conditions!)
function Theme() {
  // Unlike useContext, use() can be inside conditions
  if (showTheme) {
    const theme = use(ThemeContext); // conditional context!
    return <div style={{ background: theme.color }} />;
  }
  return null;
}

// Read promises with use (Suspense integration)
function UserProfile({ userPromise }) {
  // Suspends until promise resolves
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

// Parent passes promise (starts fetching during render)
function App() {
  const userPromise = fetchUser(); // starts immediately
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Patterns de Rendu Concurrent

Patterns concurrents : useTransition pour les changements d'onglet non-urgents (garde la nav réactive pendant que le contenu lourd rend). useSyncExternalStore s'abonne en toute sécurité aux stores externes (API navigateur, Redux, Zustand) en mode concurrent — il fournit une fonction snapshot pour client et serveur (SSR-safe). N'utilisez jamais l'état mutable externe directement dans le rendu (risque de tearing) ; passez toujours par useSyncExternalStore. Les trois arguments : subscribe (retourne le nettoyage), getSnapshot (valeur actuelle), getServerSnapshot (valeur initiale SSR). Cela assure des lectures cohérentes pendant le rendu concurrent. Des bibliothèques comme Redux et Zustand utilisent cela en interne.

react
// Pattern 1: Deferred search with transition
function App() {
  const [tab, setTab] = useState("home");
  const [isPending, startTransition] = useTransition();

  return (
    <>
      <nav>
        <button
          onClick={() => startTransition(() => setTab("analytics"))}
          disabled={isPending}
        >
          {isPending ? "Loading..." : "Analytics"}
        </button>
      </nav>
      {tab === "home" && <Home />}
      {tab === "analytics" && <HeavyAnalytics />}
    </>
  );
}

// Pattern 2: useSyncExternalStore for external state
function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("online", callback);
      window.addEventListener("offline", callback);
      return () => {
        window.removeEventListener("online", callback);
        window.removeEventListener("offline", callback);
      };
    },
    () => navigator.onLine, // client snapshot
    () => true // server snapshot (SSR)
  );
}
19

Context API Approfondi

createContext & Provider

createContext crée un objet context avec une valeur par défaut utilisée quand aucun Provider n'est trouvé. La prop value du Provider est consommée par tous les descendants. Enveloppez la valeur dans useCallback/useMemo pour empêcher les re-renders inutiles.

react
const ThemeContext = React.createContext({ theme: 'light', toggle: () => {} });

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggle = useCallback(() => setTheme((t) => (t === 'light' ? 'dark' : 'light')), []);
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

useContext

useContext lit la valeur du Provider le plus proche et re-render le composant quand cette valeur change. Imbriquez plusieurs Providers pour différents concerns. Séparez les contexts par fréquence de mise à jour pour la performance.

react
function ThemedButton() {
  const { theme, toggle } = useContext(ThemeContext);
  return (
    <button onClick={toggle} style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
      Toggle Theme
    </button>
  );
}

Context avec Reducer

Associer useReducer avec Context crée un store global sans Redux. Le reducer centralise la logique d'état ; le Context distribue l'état et dispatch. Les consommateurs peuvent dispatcher des actions sans prop-drilling.

react
const StoreContext = React.createContext(null);

function storeReducer(state, action) {
  switch (action.type) {
    case 'add': return { items: [...state.items, action.item] };
    case 'remove': return { items: state.items.filter((_, i) => i !== action.index) };
    default: return state;
  }
}

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(storeReducer, { items: [] });
  return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}

Optimiser les Rendus de Context

Quand l'état et dispatch vivent dans le même context, chaque changement d'état re-render tous les consommateurs. Les séparer signifie que les composants dispatch-only ne re-render jamais sur les changements d'état. dispatch de useReducer est stable.

react
const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initial);
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Hook Personnalisé pour Context

Envelopper useContext dans un hook personnalisé donne une API propre et une erreur claire quand le Provider manque. Exportez à la fois le Provider et le hook. C'est la façon recommandée de consommer le context.

react
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}
20

useReducer

useReducer de Base

useReducer est une alternative à useState pour la logique d'état complexe. Le reducer est une fonction pure : (state, action) => newState. dispatch est stable, donc vous pouvez le passer sans vous soucier des re-renders.

react
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset':     return { count: 0 };
    default: throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

Initialisation Paresseuse

Le troisième argument de useReducer est une fonction init qui s'exécute une fois pendant le rendu initial. C'est utile quand l'état initial est coûteux à calculer ou quand vous voulez que reset retourne à un état calculé.

react
function init(initialCount) {
  return { count: initialCount, history: [] };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'reset':     return init(action.payload);
    default: return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialCount, init);

Forme d'État Complexe

useReducer brille quand l'état a plusieurs champs liés. Chaque action décrit une transition d'état complète, rendant la logique plus facile à tracer que des appels setState dispersés. Gardez le reducer pur.

react
const initialState = { users: [], loading: false, error: null, filter: 'all' };

function reducer(state, action) {
  switch (action.type) {
    case 'fetch-start': return { ...state, loading: true, error: null };
    case 'fetch-success': return { ...state, loading: false, users: action.users };
    case 'fetch-error': return { ...state, loading: false, error: action.error };
    case 'set-filter': return { ...state, filter: action.filter };
    default: return state;
  }
}

Reducer avec Context

Combiner useReducer avec Context crée un store léger de type Redux. Le reducer détient la logique ; le Context distribue l'état et dispatch. C'est le pattern recommandé pour l'état à l'échelle de l'application dans les applications moyennes.

react
function todoReducer(state, action) {
  switch (action.type) {
    case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle': return state.map((t) => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'delete': return state.filter((t) => t.id !== action.id);
    default: return state;
  }
}

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return <TodoContext.Provider value={{ todos, dispatch }}>{children}</TodoContext.Provider>;
}

Types d'Action & Patterns

Définissez les types d'action comme constantes pour éviter les typos et activer l'autocomplétion IDE. La forme d'action { type, payload? } est une convention courante. Pour TypeScript, définissez une union discriminée des types d'action.

react
const ACTIONS = { ADD: 'add', UPDATE: 'update', DELETE: 'delete' };

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD:
      return [...state, { id: action.id, ...action.payload }];
    case ACTIONS.UPDATE:
      return state.map((item) => item.id === action.id ? { ...item, ...action.payload } : item);
    case ACTIONS.DELETE:
      return state.filter((item) => item.id !== action.id);
    default: return state;
  }
}
21

useMemo & useCallback

useMemo

useMemo cache le résultat d'un computation et ne recalcule que quand les dépendances changent. Utilisez-le pour les calculs coûteux (tri, filtrage de grands tableaux). Le tableau de dépendances doit inclure tout ce que le callback utilise.

react
function ProductList({ products, filter }) {
  const filtered = useMemo(() => {
    return products.filter((p) => p.category === filter);
  }, [products, filter]);

  const sorted = useMemo(() => [...filtered].sort((a, b) => a.price - b.price), [filtered]);
  return <ul>{sorted.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

useCallback

useCallback mémoïse une fonction pour qu'elle garde la même identité à travers les rendus sauf si les dépendances changent. C'est critique quand on passe des callbacks aux enfants mémoïsés — sans cela, l'enfant re-render à chaque fois.

react
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => setCount((c) => c + 1), []);
  return <MemoizedChild onClick={handleClick} />;
}

React.memo

React.memo enveloppe un composant pour qu'il ne re-render que quand ses props changent (comparaison superficielle). Le second argument est un comparateur personnalisé retournant true pour skipper le re-render. Combinez memo avec useCallback/useMemo pour les props.

react
const ExpensiveItem = React.memo(function ExpensiveItem({ value, onClick }) {
  return <li onClick={onClick}>{value}</li>;
});

// With custom comparison
const DeepChild = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prev, next) => prev.user.id === next.user.id
);

Quand Mémoïser

La mémoïsation a un coût qui peut dépasser les économies. Ne mémoïsez que quand : (1) le calcul est coûteux, (2) la valeur est passée à un enfant mémoïsé, ou (3) la valeur est utilisée comme dépendance dans useEffect/useMemo.

react
// GOOD: expensive computation
const sorted = useMemo(() => heavySort(data), [data]);

// GOOD: callback passed to memoized child
const onSelect = useCallback((id) => setSelected(id), []);

// BAD: cheap operation, no perf issue
const label = useMemo(() => first + ' ' + last, [first, last]);

useMemo pour l'Égalité Référentielle

useMemo assure que les objets et tableaux gardent la même référence à travers les renders, ce qui compte quand ils sont utilisés comme dépendances dans useEffect/useMemo/useCallback. Sans cela, { q: query } crée un nouvel objet à chaque rendu.

react
function Search({ query }) {
  const params = useMemo(() => ({ q: query, limit: 10 }), [query]);
  useEffect(() => {
    api.search(params).then(setData);
  }, [params]); // Without useMemo, this fires every render
}
22

Portals & Refs

createPortal

createPortal rend les enfants dans un nœud DOM hors de l'arbre DOM du composant parent (généralement document.body). C'est essentiel pour les modals, tooltips et dropdowns qui doivent échapper aux contextes d'empilement parents.

react
import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  if (!open) return null;
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>{children}</div>
    </div>,
    document.body
  );
}

useRef

useRef retourne un objet mutable dont le .current persiste à travers les rendus sans causer de re-renders. L'usage principal est d'accéder aux nœuds DOM. Il stocke aussi les valeurs mutables qui n'affectent pas l'UI.

react
function FocusInput() {
  const inputRef = useRef(null);
  const focus = () => inputRef.current?.focus();
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
    </>
  );
}

forwardRef

forwardRef permet à un composant parent de passer une ref à travers un composant wrapper vers un nœud DOM enfant. Sans cela, React interdit de passer ref comme prop. La ref est le second argument de la fonction enveloppée.

react
const FancyInput = React.forwardRef(function FancyInput({ label, ...props }, ref) {
  return (
    <label>{label}<input ref={ref} {...props} className="fancy-input" /></label>
  );
});

useImperativeHandle

useImperativeHandle personnalise l'instance exposée au parent via ref — au lieu du nœud DOM brut, le parent ne voit que les méthodes que vous définissez. Utilisez avec parcimonie ; préférez les props déclaratives quand possible.

react
const VideoPlayer = React.forwardRef(function VideoPlayer(props, ref) {
  const videoRef = useRef(null);
  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => { if (videoRef.current) videoRef.current.currentTime = time; },
  }));
  return <video ref={videoRef} src={props.src} />;
});

Refs pour les Valeurs Mutables

useRef stocke les valeurs mutables qui ne devraient pas déclencher de re-renders — comme les IDs de timer, les instances WebSocket ou les flags « is mounted ». Nettoyez toujours les effets de bord dans la fonction de nettoyage useEffect.

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
  };
  const stop = () => { clearInterval(intervalRef.current); intervalRef.current = null; };
  useEffect(() => () => clearInterval(intervalRef.current), []);
  return <>{seconds}<button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
23

Error Boundaries

Error Boundary Classe

Les error boundaries sont des composants classe qui interceptent les erreurs dans leur arbre de composants enfants pendant le rendu. getDerivedStateFromError met à jour l'état pour rendre le repli ; componentDidCatch logge l'erreur. Ils n'interceptent PAS les erreurs dans les handlers d'événements ou le code async.

react
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { hasError: false, error: null }; }
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  componentDidCatch(error, info) { console.error('Caught:', error, info); }
  render() {
    if (this.state.hasError) return this.props.fallback || <h1>Something went wrong.</h1>;
    return this.props.children;
  }
}

Utiliser les Error Boundaries

Placez les error boundaries stratégiquement pour qu'un crash dans une partie de l'UI ne fasse pas tomber toute l'application. Des limites granulaires autour des widgets laissent le reste de l'application continuer à fonctionner.

react
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Header />
      <ErrorBoundary fallback={<SidebarCrash />}><Sidebar /></ErrorBoundary>
      <ErrorBoundary fallback={<ContentCrash />}><MainContent /></ErrorBoundary>
    </ErrorBoundary>
  );
}

Réinitialiser l'État d'Erreur

Les error boundaries restent dans l'état d'erreur jusqu'à ce que leur état change. Fournissez un bouton « Try again » qui réinitialise hasError à false, laissant React re-render les enfants. Changer la prop key réinitialise aussi.

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  reset = () => this.setState({ hasError: false, error: null });
  render() {
    if (this.state.hasError) return <div><p>Failed.</p><button onClick={this.reset}>Try again</button></div>;
    return this.props.children;
  }
}

Bibliothèque react-error-boundary

La bibliothèque react-error-boundary fournit une error boundary polie et compatible hooks sans écrire de classe. FallbackComponent reçoit l'erreur et une fonction resetErrorBoundary. resetKeys se réinitialise automatiquement quand ces valeurs changent.

react
import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={(error, info) => logError(error, info)}
  onReset={() => window.location.reload()}
  resetKeys={[location.pathname]}
>
  <Routes />
</ErrorBoundary>

Gestion d'Erreur Async

Les error boundaries n'interceptent pas les erreurs dans les promises, setTimeout ou les handlers d'événements. Pour faire remonter les erreurs async à une boundary, stockez l'erreur dans l'état et relancez-la pendant le rendu. La boundary l'intercepte alors.

react
function AsyncComponent() {
  const [state, setState] = useState({ data: null, error: null });
  useEffect(() => {
    let active = true;
    fetchData()
      .then((data) => { if (active) setState({ data, error: null }); })
      .catch((error) => { if (active) setState({ data: null, error }); });
    return () => { active = false; };
  }, []);
  if (state.error) throw state.error;
  if (!state.data) return <Loading />;
  return <div>{state.data}</div>;
}
24

Optimisation de Performance

Virtualisation

La virtualisation ne rend que les lignes visibles d'une longue liste, réduisant considérablement les nœuds DOM. Essentielle pour les listes de plus de 1000 éléments — sans elle, le navigateur s'étouffe sur des dizaines de milliers de nœuds.

react
import { useVirtualizer } from '@tanstack/react-virtual';

function BigList({ items }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((vi) => (
          <div key={vi.key} style={{ position: 'absolute', top: vi.start, height: vi.size }}>
            {items[vi.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Code Splitting

Le code splitting casse le bundle en chunks chargés à la demande. Les splits les plus impactants sont au niveau des routes (chaque page est un chunk séparé) et les widgets lourds (bibliothèques de charts, éditeurs). Mesurez avec des analyseurs de bundle.

react
import { lazy, Suspense } from 'react';
const Admin = lazy(() => import('./Admin'));
const Chart = lazy(() => import('./Chart'));

function Page({ showChart }) {
  return (
    <Suspense fallback={<Skeleton />}>
      {showChart && <Chart data={data} />}
    </Suspense>
  );
}

Profiling avec DevTools

Le React DevTools Profiler enregistre les temps de rendu et montre quels composants ont re-render et pourquoi. Cherchez les rendus « gaspillés » où les props n'ont pas réellement changé — ce sont des candidats pour React.memo.

react
// Use React DevTools Profiler to record renders
// Look for:
// - Components rendering too often
// - Long commit phases
// - Wasted renders (props didn't change)

// Wrap expensive renders to find bottlenecks
function MyComponent({ data }) {
  console.time('render');
  const result = heavyCompute(data);
  console.timeEnd('render');
  return <div>{result}</div>;
}

useDeferredValue

useDeferredValue retarde la mise à jour d'une valeur, laissant les mises à jour urgentes (frappe) se faire en premier. Le rendu coûteux utilise la valeur différée, donc il ne bloque pas l'input. isStale vous permet d'afficher un indice visuel subtil.

react
function Search({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
  return <div style={{ opacity: isStale ? 0.7 : 1 }}>{results.map((r) => <div key={r.id}>{r.name}</div>)}</div>;
}

Fonctionnalités Concurrentes

Les fonctionnalités concurrentes de React 18 gardent l'UI réactive pendant le travail lourd. useTransition et useDeferredValue permettent à React d'interrompre les rendus pour gérer l'input urgent. L'automatic batching groupe plusieurs appels setState en un seul re-render.

react
import { useTransition, useDeferredValue } from 'react';

// useTransition: mark updates as non-urgent
const [isPending, startTransition] = useTransition();
const filterResults = (q) => startTransition(() => setResults(search(q)));

// Automatic batching (React 18): state updates in promises batch automatically
fetch('/api').then(() => {
  setLoading(false);   //  \
  setData(data);       //   > single re-render
  setError(null);      //  /
});
25

Tests (React Testing Library)

Rendu & Requête de Base

render() monte un composant dans un faux DOM ; screen le requête. Préférez les requêtes par rôle (getByRole) — elles reflètent comment la tech d'assistance voit la page et imposent l'accessibilité. userEvent simule les interactions utilisateur réelles.

react
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('increments on click', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  const button = screen.getByRole('button', { name: /increment/i });
  await user.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Variantes de Requête

getBy affirme que l'élément existe (lance sinon). queryBy sert à affirmer l'absence (retourne null). findBy attend que les éléments async apparaissent. Les variantes All gèrent les correspondances multiples.

react
// getBy: throws if 0 or >1 matches (strict)
screen.getByRole('button', { name: 'Submit' });

// queryBy: returns null if 0 matches (for assertions of absence)
expect(screen.queryByText('Error')).not.toBeInTheDocument();

// findBy: returns a Promise, waits for match (async)
const element = await screen.findByText('Loaded');

// getAllBy: returns array (multiple matches)
const items = screen.getAllByRole('listitem');

Déclencher des Événements

userEvent (pas le fireEvent de plus bas niveau) est la façon recommandée de simuler les interactions — il déclenche tous les événements qu'un vrai utilisateur ferait (focus, input, keydown, click) dans le bon ordre. Utilisez toujours await avec les méthodes userEvent.

react
import userEvent from '@testing-library/user-event';

test('form submission', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<Form onSubmit={onSubmit} />);
  await user.type(screen.getByLabelText(/email/i), '[email protected]');
  await user.click(screen.getByRole('button', { name: /submit/i }));
  expect(onSubmit).toHaveBeenCalled();
});

waitFor & Async

waitFor interroge jusqu'à ce que l'assertion passe ou expire. findBy* combine waitFor et getBy pour le cas courant de « attendre que cela apparaisse ». within limite les requêtes à un élément spécifique.

react
import { waitFor, within } from '@testing-library/react';

test('shows data after fetch', async () => {
  render(<UserList />);
  await waitFor(() => {
    const list = screen.getByRole('list');
    expect(within(list).getAllByRole('listitem')).toHaveLength(3);
  });
});

// findBy is often cleaner than waitFor + getBy
test('shows data (cleaner)', async () => {
  render(<UserList />);
  expect(await screen.findAllByRole('listitem')).toHaveLength(3);
});

Mocking & Configuration

MSW (Mock Service Worker) intercepte les requêtes réseau au niveau du service-worker, donc votre code fetch s'exécute non modifié. Configurez les handlers par test, réinitialisez entre les tests, et fermez après tout.

react
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => res(ctx.json({ name: 'Alice' })))
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('shows user name', async () => {
  render(<UserProfile />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.