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.
// 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.
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.
// 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.
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.
// 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>;
}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.
// 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.
// 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}).
// 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.
// 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.
// 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 levelsuseState & É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).
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.
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.
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.
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.
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>
);
}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.
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.
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.
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.
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.
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>;
}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).
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.
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.
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.
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.
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>
</>
);
}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.
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.
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.
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.
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.
// 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>;
}É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.
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.
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.
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.
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é.
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>
);
}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.
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.
// 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).
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é.
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.
// 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' } },
];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.
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.
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.
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 renderedDebouncing & 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.
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.
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
*/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.
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.
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.
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.
// 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 compatibilityComposants 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.
// 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>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.
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.
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.
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.
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.
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>}
</>
);
}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é.
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.
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.
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.
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.
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 themSuspense & 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.
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.
<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 visibleLazy 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%.
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 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é.
// 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>
);
}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).
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.
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>.
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>.
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.
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;
}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.
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.
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.
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.
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.
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>
);
}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.
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+.
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.
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.
// 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.
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();
});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.
// 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.
// 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.
// 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.
// 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.
// 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>;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.
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.
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.
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.
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.
// 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)
);
}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.
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.
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.
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.
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.
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}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.
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é.
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.
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.
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.
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;
}
}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.
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.
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.
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.
// 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.
function Search({ query }) {
const params = useMemo(() => ({ q: query, limit: 10 }), [query]);
useEffect(() => {
api.search(params).then(setData);
}, [params]); // Without useMemo, this fires every render
}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.
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.
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.
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.
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.
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></>;
}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.
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.
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.
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.
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.
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>;
}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.
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.
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.
// 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.
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.
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); // /
});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.
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.
// 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.
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.
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.
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();
});Snippets React associés
Copy-paste ready code for common tasks.
Formulaire contrôlé avec validation
Construire un formulaire React contrôlé avec validation en ligne et messages d'erreur.
Gestion d'événements et rendu de listes
Gérer les événements et rendre des listes dynamiques avec des keys en React.
useState
Hook de gestion d'état.
useEffect
Hook d'effet de bord.
useContext
État partagé via le contexte.
useReducer
Gestion d'état complexe.
useMemo
Mémoriser les résultats de calcul.
useCallback
Mémoriser les fonctions de callback.
useRef
Référencer le DOM et les valeurs mutables.
Hooks personnalisés
Extraire la logique réutilisable.
Communication entre composants
Communication entre composants parent-enfant et frères.
Frontières d'erreur
Capturer les erreurs de composant.
Chargement différé
Découpage de code et chargement différé.
Portal
Rendre vers des nœuds DOM en dehors du composant.
Composants d'ordre supérieur
Modèle d'amélioration de composant.
Render Props
Modèle de render props.
Optimisation des performances
Conseils d'optimisation des performances React.
Was this helpful?