React

Les bases

JSX, composants, props et state

Les bases

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

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;
Les bases

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

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>
Les bases

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,
};
Les bases

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

StateProps
Interne au composantVient du parent
ModifiableEn lecture seule
Déclenche un re-renderDéclenche un re-render

Cycle de vie des composants

Mounting, updating, unmounting et équivalents avec les Hooks

Cycle de vie des composants

Vue d'ensemble

Cycle de vie d'un composant React

Chaque composant React passe par trois grandes phases :

PhaseDescription
MountingLe composant est créé et inséré dans le DOM
UpdatingLes props ou le state changent → re-render
UnmountingLe 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)
Cycle de vie des composants

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

  1. La fonction du composant s'exécute (rendu initial)
  2. React met à jour le DOM
  3. Les effets (useEffect) s'exécutent
Cycle de vie des composants

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

  1. Le state ou les props changent
  2. React re-exécute la fonction du composant
  3. React met à jour le DOM (diff)
  4. Les effets dont les dépendances ont changé s'exécutent (cleanup de l'ancien effet d'abord)
Cycle de vie des composants

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

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

Hooks fondamentaux

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

Hooks fondamentaux

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

Hooks fondamentaux

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

useRefuseState
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

Hooks avancés

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 ?

⚠️ 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).

Hooks avancés

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

useStateuseReducer
État simple (string, number, bool)État complexe (objet avec plusieurs champs)
Transitions simplesTransitions liées ou conditionnelles
Pas de logique dans le setterLogique centralisée dans le reducer
Hooks avancés

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 :

Événements et formulaires

Gestion des interactions utilisateur

Événements et formulaires

É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énementDescription
onClickClic souris
onChangeValeur d'input modifiée
onSubmitSoumission de formulaire
onKeyDown / onKeyUpTouches clavier
onFocus / onBlurFocus / perte de focus
onMouseEnter / onMouseLeaveSurvol souris
onScrollDé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>
  );
}
Événements et formulaires

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 :

Routing (React Router v6)

Navigation et gestion des URLs

Routing (React Router v6)

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>
  );
}
Routing (React Router v6)

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

Gestion d'état globale

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>;
}
Gestion d'état globale

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 APIZustand
InstallationNatifnpm install
PerformanceRe-render de tous les consommateursRe-render ciblé
BoilerplateMoyenMinimal
DevToolsReact DevToolsRedux DevTools

Communication avec une API

fetch, axios et React Query

Communication avec une API

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');
Communication avec une API

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

Performance et bonnes pratiques

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>
  );
}
Performance et bonnes pratiques

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

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

Patterns avancés

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}`);
}
Patterns avancés

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.