React
- Les bases
- Cycle de vie des composants
- Hooks fondamentaux
- Hooks avancés
- Événements et formulaires
- Routing (React Router v6)
- Gestion d'état globale
- Communication avec une API
- Performance et bonnes pratiques
- Patterns avancés
Les bases
JSX, composants, props et state
JSX
Qu'est-ce que JSX ?
JSX (JavaScript XML) est une extension syntaxique de JavaScript qui permet d'écrire du HTML directement dans du code JS. Il est compilé par Babel en appels React.createElement().
Syntaxe de base
// JSX
const element = <h1 className="title">Bonjour React</h1>;
// Compilé en
const element = React.createElement('h1', { className: 'title' }, 'Bonjour React');
Règles importantes
- Un composant JSX doit retourner un seul élément racine (ou un
<Fragment>/<></>) - Utiliser
classNameau lieu declass - Les attributs sont en camelCase :
onClick,onChange,htmlFor - Les balises auto-fermantes doivent l'être :
<img />,<input /> - Les expressions JS s'écrivent entre accolades :
{variable}
Expressions dans JSX
const nom = "Hugo";
const age = 25;
return (
<>
<p>Bonjour {nom} !</p>
<p>Tu as {age >= 18 ? "plus" : "moins"} de 18 ans.</p>
<ul>
{["pomme", "poire", "banane"].map((fruit, i) => (
<li key={i}>{fruit}</li>
))}
</ul>
</>
);
Rendu conditionnel
// Opérateur ternaire
{isLoggedIn ? <Dashboard /> : <Login />}
// Court-circuit &&
{hasError && <ErrorMessage />}
// Variable
let content;
if (isLoggedIn) content = <Dashboard />;
else content = <Login />;
return content;
Composants
Les composants React
Un composant est une fonction (ou classe) qui retourne du JSX. Il est réutilisable, indépendant et encapsule sa logique.
Composant fonctionnel (recommandé)
// Déclaration avec function
function Bouton({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// Déclaration avec arrow function
const Bouton = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
Composant de classe (héritage, moins utilisé)
import { Component } from 'react';
class Compteur extends Component {
state = { count: 0 };
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
+
</button>
</div>
);
}
}
Règles de nommage
- Un composant doit commencer par une majuscule :
MonComposant - Un nom en minuscule est interprété comme une balise HTML native
Composition
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Utilisation
<Card title="Mon titre">
<p>Contenu quelconque</p>
</Card>
Props
Les Props
Les props (propriétés) sont les paramètres passés à un composant depuis son parent. Elles sont en lecture seule — un composant ne doit jamais modifier ses propres props.
Passage de props
// Parent
<Profil nom="Hugo" age={25} admin={true} />
// Enfant
function Profil({ nom, age, admin }) {
return (
<div>
<p>{nom}, {age} ans {admin && "👑"}</p>
</div>
);
}
Valeurs par défaut
function Bouton({ label = "Cliquer", variant = "primary" }) {
return <button className={variant}>{label}</button>;
}
Spread de props
const props = { nom: "Hugo", age: 25 };
<Profil {...props} />
La prop children
function Panel({ title, children }) {
return (
<section>
<h3>{title}</h3>
{children}
</section>
);
}
<Panel title="Info">
<p>Texte dans le panel</p>
</Panel>
PropTypes (validation)
import PropTypes from 'prop-types';
Profil.propTypes = {
nom: PropTypes.string.isRequired,
age: PropTypes.number,
admin: PropTypes.bool,
};
Profil.defaultProps = {
age: 0,
admin: false,
};
State
Le State (État)
Le state est la mémoire locale d'un composant. Contrairement aux props, il peut changer dans le temps. Chaque mise à jour du state déclenche un re-render du composant.
useState — déclaration
import { useState } from 'react';
function Compteur() {
const [count, setCount] = useState(0); // valeur initiale = 0
return (
<div>
<p>Compteur : {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Mise à jour fonctionnelle
Quand le nouvel état dépend de l'ancien, toujours utiliser la forme fonctionnelle :
// ❌ Peut être stale dans certains cas
setCount(count + 1);
// ✅ Toujours sûr
setCount(prev => prev + 1);
State avec objets
const [user, setUser] = useState({ nom: "Hugo", age: 25 });
// ✅ Toujours créer un nouvel objet (immuabilité)
setUser(prev => ({ ...prev, age: 26 }));
Plusieurs états
const [nom, setNom] = useState("");
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
State vs Props
| State | Props |
|---|---|
| Interne au composant | Vient du parent |
| Modifiable | En lecture seule |
| Déclenche un re-render | Déclenche un re-render |
Cycle de vie des composants
Mounting, updating, unmounting et équivalents avec les Hooks
Vue d'ensemble
Cycle de vie d'un composant React
Chaque composant React passe par trois grandes phases :
| Phase | Description |
|---|---|
| Mounting | Le composant est créé et inséré dans le DOM |
| Updating | Les props ou le state changent → re-render |
| Unmounting | Le composant est retiré du DOM |
Méthodes de classe vs Hooks
| Classe (legacy) | Hook équivalent |
|---|---|
constructor() | useState(valeurInitiale) |
componentDidMount() | useEffect(() => { ... }, []) |
componentDidUpdate(prevProps, prevState) | useEffect(() => { ... }, [dep]) |
componentWillUnmount() | useEffect(() => { return () => { ... } }, []) |
shouldComponentUpdate() | React.memo / useMemo |
componentDidCatch() | Error Boundary (classe uniquement) |
Mounting
Phase de Mounting
Le composant est initialisé et rendu pour la première fois dans le DOM.
Avec les classes
class MonComposant extends Component {
constructor(props) {
super(props);
this.state = { data: null }; // 1. Initialisation du state
}
componentDidMount() {
// 2. Appelé APRÈS le premier render (DOM disponible)
fetch('/api/data')
.then(res => res.json())
.then(data => this.setState({ data }));
}
render() {
return <div>{this.state.data}</div>;
}
}
Avec les Hooks
function MonComposant() {
const [data, setData] = useState(null); // Initialisation
useEffect(() => {
// Équivalent de componentDidMount (tableau vide = exécution unique)
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}, []); // [] = seulement au montage
return <div>{data}</div>;
}
Ordre d'exécution au montage
- La fonction du composant s'exécute (rendu initial)
- React met à jour le DOM
- Les effets (
useEffect) s'exécutent
Updating
Phase de Updating
Le composant se re-rend quand ses props ou son state changent.
Avec les classes
componentDidUpdate(prevProps, prevState) {
// Appelé après chaque re-render
if (prevProps.userId !== this.props.userId) {
// Les props ont changé → réagir
this.fetchUser(this.props.userId);
}
if (prevState.count !== this.state.count) {
// Le state a changé → réagir
console.log("Count changé :", this.state.count);
}
}
Avec les Hooks
// Réagir aux changements de props ou state
useEffect(() => {
fetchUser(userId);
}, [userId]); // S'exécute quand userId change
useEffect(() => {
console.log("Count changé :", count);
}, [count]);
Optimiser les re-renders
// Éviter les re-renders inutiles du composant enfant
const EnfantMemo = React.memo(function Enfant({ nom }) {
return <p>{nom}</p>;
});
// shouldComponentUpdate (classe)
shouldComponentUpdate(nextProps, nextState) {
return nextProps.value !== this.props.value;
}
Ordre d'exécution lors d'une mise à jour
- Le state ou les props changent
- React re-exécute la fonction du composant
- React met à jour le DOM (diff)
- Les effets dont les dépendances ont changé s'exécutent (cleanup de l'ancien effet d'abord)
Unmounting
Phase de Unmounting
Le composant est retiré du DOM. C'est le moment de nettoyer les effets de bord : timers, abonnements, listeners...
Avec les classes
class Timer extends Component {
componentDidMount() {
this.interval = setInterval(() => {
this.setState(prev => ({ count: prev.count + 1 }));
}, 1000);
}
componentWillUnmount() {
// Nettoyage obligatoire pour éviter les memory leaks
clearInterval(this.interval);
}
render() { return <p>{this.state.count}</p>; }
}
Avec les Hooks (fonction de cleanup)
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// La fonction retournée = cleanup (unmount OU avant le prochain effet)
return () => clearInterval(interval);
}, []);
return <p>{count}</p>;
}
Cas d'usage typiques du cleanup
- Annuler des requêtes HTTP (
AbortController) - Désabonner d'un WebSocket / EventEmitter
- Retirer des event listeners (
removeEventListener) - Effacer des timers (
clearTimeout,clearInterval)
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // Annule la requête si démontage
}, []);
Hooks fondamentaux
useState, useEffect et useRef
useState
useState
Permet d'ajouter un état local à un composant fonctionnel.
Syntaxe
const [state, setState] = useState(valeurInitiale);
Exemples
// Booléen
const [isOpen, setIsOpen] = useState(false);
// Tableau
const [items, setItems] = useState([]);
const addItem = (item) => setItems(prev => [...prev, item]);
const removeItem = (id) => setItems(prev => prev.filter(i => i.id !== id));
// Objet
const [form, setForm] = useState({ email: "", password: "" });
const handleChange = (e) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
Initialisation paresseuse (lazy initialization)
Si le calcul de la valeur initiale est coûteux, passer une fonction pour ne l'exécuter qu'une fois :
// ❌ Calculé à chaque render
const [data, setData] = useState(computeExpensiveValue());
// ✅ Calculé une seule fois
const [data, setData] = useState(() => computeExpensiveValue());
Règles importantes
- Ne jamais muter le state directement — toujours créer une nouvelle valeur
- Les mises à jour de state sont asynchrones (batchées)
- Utiliser la forme fonctionnelle quand le nouvel état dépend du précédent
useEffect
useEffect
Permet d'exécuter des effets de bord (appels API, timers, listeners...) après le rendu.
Syntaxe
useEffect(() => {
// effet
return () => { /* cleanup */ }; // optionnel
}, [/* dépendances */]);
Les 3 modes selon le tableau de dépendances
// 1. Sans tableau → s'exécute après CHAQUE render
useEffect(() => {
console.log("render");
});
// 2. Tableau vide → s'exécute UNE FOIS au montage
useEffect(() => {
console.log("monté");
}, []);
// 3. Avec dépendances → s'exécute quand une dépendance change
useEffect(() => {
console.log("userId a changé :", userId);
}, [userId]);
Exemple complet : fetch de données
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => { setUser(data); setLoading(false); })
.catch(err => { if (err.name !== 'AbortError') console.error(err); });
return () => controller.abort();
}, [userId]);
if (loading) return <p>Chargement...</p>;
return <p>{user?.nom}</p>;
}
Pièges courants
- Dépendances manquantes : toujours inclure toutes les variables utilisées dans l'effet
- Boucle infinie : si une dépendance est un objet/tableau créé dans le render, il change à chaque fois → utiliser
useMemoou le déplacer hors du composant - State stale : préférer la forme fonctionnelle de setState dans un useEffect
useRef
useRef
useRef retourne un objet { current: valeur } qui persiste entre les renders sans déclencher de re-render quand il change. C'est sa différence fondamentale avec useState.
Syntaxe
const ref = useRef(valeurInitiale);
// Accès : ref.current
Usage 1 : Accéder à un élément DOM
function InputAutofocus() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // Focus au montage
}, []);
return <input ref={inputRef} placeholder="Tapez ici..." />;
}
// Lire la valeur sans contrôler l'input (uncontrolled)
function Form() {
const nameRef = useRef(null);
const handleSubmit = () => {
console.log(nameRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} />
<button type="submit">Envoyer</button>
</form>
);
}
Usage 2 : Stocker une valeur sans re-render
Idéal pour stocker des valeurs qui ne doivent pas provoquer de re-render : timers, valeurs précédentes, flags...
// Garder trace de l'ID d'un intervalle
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stop = () => clearInterval(intervalRef.current);
return (
<div>
<p>{count}</p>
<button onClick={start}>Démarrer</button>
<button onClick={stop}>Arrêter</button>
</div>
);
}
Usage 3 : Capturer la valeur précédente d'un state
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // Met à jour APRÈS le render
});
return ref.current; // Retourne la valeur PRÉCÉDENTE
}
function Compteur() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <p>Avant: {prevCount} | Maintenant: {count}</p>;
}
useRef vs useState
| useRef | useState | |
|---|---|---|
| Persiste entre renders | ✅ | ✅ |
| Déclenche un re-render | ❌ | ✅ |
| Accès au DOM | ✅ | ❌ |
| Valeur mutable | ✅ (ref.current) | ❌ (setter requis) |
Hooks avancés
useContext, useReducer, useMemo et useCallback
useContext
useContext
Permet de lire une valeur de contexte sans passer par le prop drilling. Le contexte est un état partagé accessible par tous les composants descendants.
Création d'un contexte
import { createContext, useContext, useState } from 'react';
// 1. Créer le contexte
const ThemeContext = createContext('light'); // valeur par défaut
// 2. Fournir le contexte
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Navbar />
<Main />
</ThemeContext.Provider>
);
}
// 3. Consommer le contexte
function Navbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<nav className={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Basculer le thème
</button>
</nav>
);
}
Pattern : custom hook pour le contexte
// Bonne pratique : encapsuler dans un hook custom
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme doit être utilisé dans ThemeProvider");
return ctx;
}
// Utilisation
const { theme } = useTheme();
Quand utiliser le contexte ?
- Thème (dark/light)
- Langue / i18n
- Utilisateur connecté
- Toute donnée vraiment globale
⚠️ Le contexte re-rend tous les consommateurs quand la valeur change. Pour des données très fréquemment mises à jour, préférer un gestionnaire d'état dédié (Zustand, Redux).
useReducer
useReducer
Alternative à useState pour des états complexes avec plusieurs sous-valeurs ou des transitions d'état interdépendantes. Inspiré du pattern Redux.
Syntaxe
const [state, dispatch] = useReducer(reducer, initialState);
Exemple : gestion d'un formulaire
const initialState = { nom: "", email: "", loading: false, error: null };
function reducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value };
case 'SUBMIT_START':
return { ...state, loading: true, error: null };
case 'SUBMIT_SUCCESS':
return { ...initialState };
case 'SUBMIT_ERROR':
return { ...state, loading: false, error: action.error };
default:
return state;
}
}
function Formulaire() {
const [state, dispatch] = useReducer(reducer, initialState);
const handleChange = (e) => dispatch({
type: 'SET_FIELD', field: e.target.name, value: e.target.value
});
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
try {
await submitForm(state);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (err) {
dispatch({ type: 'SUBMIT_ERROR', error: err.message });
}
};
return (
<form onSubmit={handleSubmit}>
<input name="nom" value={state.nom} onChange={handleChange} />
<input name="email" value={state.email} onChange={handleChange} />
{state.error && <p>{state.error}</p>}
<button disabled={state.loading}>Envoyer</button>
</form>
);
}
useReducer vs useState
| useState | useReducer |
|---|---|
| État simple (string, number, bool) | État complexe (objet avec plusieurs champs) |
| Transitions simples | Transitions liées ou conditionnelles |
| Pas de logique dans le setter | Logique centralisée dans le reducer |
useMemo et useCallback
useMemo et useCallback
Ces deux hooks servent à mémoïser des valeurs ou des fonctions pour éviter des recalculs ou recréations inutiles entre les renders.
useMemo
Mémoïse le résultat d'un calcul coûteux.
import { useMemo } from 'react';
function Liste({ items, filtre }) {
// Recalculé SEULEMENT quand items ou filtre change
const itemsFiltres = useMemo(() => {
return items.filter(item => item.nom.includes(filtre));
}, [items, filtre]);
return <ul>{itemsFiltres.map(i => <li key={i.id}>{i.nom}</li>)}</ul>;
}
useCallback
Mémoïse une fonction pour qu'elle garde la même référence entre les renders.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Sans useCallback : nouvelle référence à chaque render → Enfant se re-rend inutilement
// Avec useCallback : même référence si count ne change pas
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // pas de dépendance car on utilise la forme fonctionnelle
return <Enfant onClick={handleClick} />;
}
const Enfant = React.memo(({ onClick }) => {
console.log("Enfant rendu");
return <button onClick={onClick}>+</button>;
});
Règle d'or
N'utilisez pas useMemo/useCallback par défaut sur tout. Ce sont des optimisations qui ont un coût. Utilisez-les seulement quand :
- Le calcul est réellement coûteux (filtres lourds, sorts, calculs mathématiques)
- La valeur/fonction est passée en prop à un composant mémoïsé (
React.memo) - La valeur est une dépendance d'un
useEffect
Événements et formulaires
Gestion des interactions utilisateur
Événements
Gestion des événements
React utilise des événements synthétiques (SyntheticEvent) qui wrappent les événements natifs du navigateur.
Syntaxe
// Passer une référence de fonction (pas d'appel !)
<button onClick={handleClick}>Clic</button>
// Arrow function inline (pour passer des arguments)
<button onClick={() => handleClick(id)}>Supprimer</button>
// Avec l'objet event
function handleClick(e) {
e.preventDefault(); // Empêcher le comportement par défaut
e.stopPropagation(); // Stopper la propagation
console.log(e.target.value);
}
Événements courants
| Événement | Description |
|---|---|
onClick | Clic souris |
onChange | Valeur d'input modifiée |
onSubmit | Soumission de formulaire |
onKeyDown / onKeyUp | Touches clavier |
onFocus / onBlur | Focus / perte de focus |
onMouseEnter / onMouseLeave | Survol souris |
onScroll | Défilement |
Délégation d'événement
// Gérer les clics de plusieurs enfants dans le parent
function Liste({ items, onDelete }) {
return (
<ul onClick={(e) => {
const id = e.target.closest('li')?.dataset.id;
if (id) onDelete(id);
}}>
{items.map(item => (
<li key={item.id} data-id={item.id}>{item.nom}</li>
))}
</ul>
);
}
Formulaires contrôlés
Formulaires contrôlés
Dans un formulaire contrôlé, React gère l'état de chaque champ via le state. C'est le pattern recommandé.
Input contrôlé simple
function Formulaire() {
const [nom, setNom] = useState("");
return (
<input
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Votre nom"
/>
);
}
Formulaire complet
function Inscription() {
const [form, setForm] = useState({
nom: "", email: "", password: "", role: "user"
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const validate = () => {
const newErrors = {};
if (!form.nom) newErrors.nom = "Nom requis";
if (!form.email.includes('@')) newErrors.email = "Email invalide";
if (form.password.length < 6) newErrors.password = "6 caractères min";
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
console.log("Soumission :", form);
};
return (
<form onSubmit={handleSubmit}>
<div>
<input name="nom" value={form.nom} onChange={handleChange} />
{errors.nom && <span>{errors.nom}</span>}
</div>
<div>
<select name="role" value={form.role} onChange={handleChange}>
<option value="user">Utilisateur</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit">S'inscrire</button>
</form>
);
}
Bibliothèques de formulaires
Pour des formulaires complexes, préférer :
- React Hook Form — performant, peu de re-renders, validation facile
- Formik — complet, plus verbeux
- Zod — validation de schéma TypeScript-first, souvent couplé avec RHF
Routing (React Router v6)
Navigation et gestion des URLs
Configuration de base
React Router v6 — Configuration de base
npm install react-router-dom
Structure minimale
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Accueil</Link>
<Link to="/about">À propos</Link>
<Link to="/users">Utilisateurs</Link>
</nav>
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
Routes imbriquées (Nested Routes)
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="stats" element={<Stats />} />
<Route path="settings" element={<Settings />} />
</Route>
// Dans Dashboard.jsx, utiliser <Outlet /> pour afficher les enfants
import { Outlet } from 'react-router-dom';
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Outlet /> {/* Ici s'affichent DashboardHome, Stats ou Settings */}
</div>
);
}
Hooks de navigation
Hooks React Router v6
useParams — paramètres d'URL
// Route : /users/:id
function UserDetail() {
const { id } = useParams();
// id = "42" pour /users/42
return <p>Utilisateur #{id}</p>;
}
useNavigate — navigation programmée
import { useNavigate } from 'react-router-dom';
function Login() {
const navigate = useNavigate();
const handleSubmit = async () => {
await loginUser();
navigate('/dashboard'); // Aller à
navigate(-1); // Page précédente
navigate('/profile', { replace: true }); // Remplacer l'historique
};
}
useLocation — URL courante
import { useLocation } from 'react-router-dom';
function Breadcrumb() {
const location = useLocation();
// location.pathname = "/dashboard/stats"
// location.search = "?page=2"
// location.state = { from: "/login" }
return <p>Vous êtes ici : {location.pathname}</p>;
}
useSearchParams — query string
import { useSearchParams } from 'react-router-dom';
function Recherche() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || '';
return (
<input
value={query}
onChange={(e) => setSearchParams({ q: e.target.value })}
placeholder="Rechercher..."
/>
);
// URL : /recherche?q=react
}
Route protégée
function PrivateRoute({ children }) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// Utilisation
<Route path="/dashboard" element={
<PrivateRoute><Dashboard /></PrivateRoute>
} />
Gestion d'état globale
Context API, Zustand et Redux Toolkit
Context API
Context API — État global natif
Intégré à React. Idéal pour des états peu fréquemment mis à jour (thème, auth, langue).
Pattern complet avec custom provider
// AuthContext.jsx
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = async (credentials) => {
const data = await loginAPI(credentials);
setUser(data.user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
// main.jsx
<AuthProvider>
<App />
</AuthProvider>
// Composant
function Navbar() {
const { user, logout } = useAuth();
return <button onClick={logout}>{user?.nom} — Déconnexion</button>;
}
Zustand
Zustand — État global simple et performant
Bibliothèque légère (~1kb), sans boilerplate, qui évite les re-renders inutiles.
npm install zustand
Création d'un store
import { create } from 'zustand';
const useStore = create((set, get) => ({
// State
count: 0,
user: null,
// Actions
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
// Action async
fetchUser: async (id) => {
const user = await getUser(id);
set({ user });
},
}));
Utilisation dans les composants
function Compteur() {
// Sélection précise = re-render SEULEMENT si count change
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
return <button onClick={increment}>{count}</button>;
}
function Profil() {
const { user, logout } = useStore(state => ({
user: state.user,
logout: state.logout
}));
Zustand vs Context API
| Context API | Zustand | |
|---|---|---|
| Installation | Natif | npm install |
| Performance | Re-render de tous les consommateurs | Re-render ciblé |
| Boilerplate | Moyen | Minimal |
| DevTools | React DevTools | Redux DevTools |
Communication avec une API
fetch, axios et React Query
fetch et axios
Appels API : fetch et axios
fetch natif
async function getUsers() {
const res = await fetch('/api/users', {
method: 'GET',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function createUser(data) {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
axios
npm install axios
import axios from 'axios';
// Instance configurée
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
// Intercepteur pour le token
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Intercepteur pour les erreurs
api.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) logout();
return Promise.reject(error);
}
);
// Utilisation
const users = await api.get('/users');
const user = await api.post('/users', { nom: "Hugo" });
await api.put('/users/1', { nom: "Hugo P." });
await api.delete('/users/1');
React Query (TanStack Query)
React Query (TanStack Query)
La meilleure solution pour gérer l'état serveur : cache automatique, refetch, états de chargement/erreur, pagination, optimistic updates...
npm install @tanstack/react-query
Configuration
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 1000 * 60 * 5 }, // 5 minutes
},
});
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
useQuery — lecture de données
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['users'], // Clé de cache unique
queryFn: () => api.get('/users'),
staleTime: 1000 * 60, // 1 minute avant refetch
});
if (isLoading) return <Spinner />;
if (isError) return <p>Erreur : {error.message}</p>;
return <ul>{data.map(u => <li key={u.id}>{u.nom}</li>)}</ul>;
}
// Avec paramètre (refetch automatique quand userId change)
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => api.get(`/users/${userId}`),
enabled: !!userId, // N'exécute pas si userId est null
});
useMutation — écriture de données
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => api.post('/users', newUser),
onSuccess: () => {
// Invalider le cache pour forcer un refetch
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error) => {
console.error("Erreur création :", error);
},
});
return (
<button
onClick={() => mutation.mutate({ nom: "Hugo" })}
disabled={mutation.isPending}
>
{mutation.isPending ? "Création..." : "Créer"}
</button>
);
}
Performance et bonnes pratiques
Optimisations et patterns à éviter
Optimisations
Optimisations de performance
React.memo — éviter les re-renders inutiles
// L'enfant se re-rend seulement si ses props changent
const ListItem = React.memo(function ListItem({ item, onDelete }) {
return (
<li>
{item.nom}
<button onClick={() => onDelete(item.id)}>Supprimer</button>
</li>
);
});
Code splitting — lazy loading
import { lazy, Suspense } from 'react';
// Chargé seulement quand la route est visitée
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Virtualisation des listes
Pour des listes de centaines/milliers d'éléments, n'afficher que ce qui est visible :
npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
function GrandeListe({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(vItem => (
<div key={vItem.index} style={{ transform: `translateY(${vItem.start}px)` }}>
{items[vItem.index].nom}
</div>
))}
</div>
</div>
);
}
Bonnes pratiques
Bonnes pratiques React
Structure de fichiers
src/
├── components/ # Composants réutilisables
│ ├── Button/
│ │ ├── Button.jsx
│ │ └── Button.css
│ └── Modal/
├── pages/ # Composants de page (1 par route)
├── hooks/ # Custom hooks
├── contexts/ # Contextes React
├── services/ # Appels API
├── utils/ # Fonctions utilitaires
└── types/ # TypeScript types/interfaces
Règles à respecter
- Un composant = une responsabilité — si un composant est trop gros, le découper
- Extraire la logique dans des custom hooks (
useForm,useFetch...) - Remonter l'état (lift state up) au plus proche ancêtre commun qui en a besoin
- Toujours mettre une key sur les éléments d'une liste — idéalement un ID, pas l'index
- Éviter l'état dérivé — ne pas stocker dans le state ce qui peut être calculé
Anti-patterns courants
// ❌ Synchroniser deux états (état dérivé)
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
// Chaque ajout doit mettre à jour les deux
// ✅ Calculer le dérivé
const [items, setItems] = useState([]);
const count = items.length; // Calculé, pas stocké
// ❌ useEffect pour synchroniser des states
useEffect(() => setFullName(first + " " + last), [first, last]);
// ✅ Variable calculée
const fullName = first + " " + last;
// ❌ Key = index (problèmes lors de réordonnancement)
{items.map((item, i) => <Item key={i} />)}
// ✅ Key = ID stable
{items.map(item => <Item key={item.id} />)}
Patterns avancés
HOC, render props et custom hooks
Custom Hooks
Custom Hooks
Un custom hook est une fonction commençant par use qui encapsule de la logique réutilisable avec des hooks React.
useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch { return initialValue; }
});
const setStoredValue = (newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');
useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => { if (!res.ok) throw new Error(res.statusText); return res.json(); })
.then(data => { setData(data); setLoading(false); })
.catch(err => { if (err.name !== 'AbortError') { setError(err); setLoading(false); } });
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
const { data: users, loading } = useFetch('/api/users');
useDebounce
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage : recherche sans spammer l'API
function Recherche() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data } = useFetch(`/api/search?q=${debouncedQuery}`);
}
HOC et Render Props
Higher-Order Components (HOC)
Un HOC est une fonction qui prend un composant et retourne un nouveau composant enrichi. Pattern issu des classes, encore utilisé pour des wrappers génériques.
// withAuth — HOC de protection de route
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
// withLoading — HOC d'affichage de chargement
function withLoading(WrappedComponent) {
return function ({ isLoading, ...props }) {
if (isLoading) return <Spinner />;
return <WrappedComponent {...props} />;
};
}
Render Props
Pattern où un composant reçoit une fonction en prop et l'appelle pour déléguer le rendu. Remplacé en grande partie par les custom hooks.
// DataFetcher avec render prop
function DataFetcher({ url, render }) {
const { data, loading, error } = useFetch(url);
return render({ data, loading, error });
}
// Usage
<DataFetcher
url="/api/users"
render={({ data, loading }) => (
loading ? <Spinner /> : <UserList users={data} />
)}
/>
// Version children-as-function
<DataFetcher url="/api/users">
{({ data, loading }) => loading ? <Spinner /> : <UserList users={data} />}
</DataFetcher>
Recommandation actuelle
Dans React moderne, les custom hooks couvrent la majorité des cas d'usage des HOC et render props avec moins de complexité et de nesting. Utiliser HOC/render props principalement pour l'intégration avec des bibliothèques tierces ou la compatibilité legacy.