Développement Web

React
Bibliothèque UI

Composants, hooks, état, effets, contexte, routing et patterns essentiels pour construire des interfaces modernes avec React 18+.

Qu'est-ce que React ?

React est une bibliothèque JavaScript (pas un framework) créée par Meta pour construire des interfaces utilisateur. Son principe fondateur : l'UI est une fonction de l'étatUI = f(state). Quand l'état change, React recalcule et met à jour uniquement ce qui a changé dans le DOM.

<App />racine de l'application
<Navbar />composant de navigation
<HomePage />page courante
┃ ┣ <HeroSection />
┃ ┣ <CardList />
┃ ┃ ┗ <Card /> × Nréutilisable
<Footer />
Créer un projet React (Vite)
# Créer un projet avec Vite (recommandé)
npm create vite@latest mon-app -- --template react
cd mon-app
npm install
npm run dev       # → http://localhost:5173

# Ou avec Create React App (legacy)
npx create-react-app mon-app
cd mon-app
npm start

# Structure d'un projet Vite+React :
mon-app/
├── public/          ← fichiers statiques
├── src/
│   ├── main.jsx     ← point d'entrée
│   ├── App.jsx      ← composant racine
│   ├── components/  ← composants réutilisables
│   ├── pages/       ← pages/routes
│   ├── hooks/       ← hooks personnalisés
│   └── assets/      ← images, fonts
├── index.html
└── vite.config.js

JSX

Syntaxe JSX
// JSX = JavaScript + XML-like syntax
// Transpilé en React.createElement() par Babel/Vite

// ✅ JSX valide
const element = (
  <div className="card">
    <h1>{titre}</h1>         // {} = expression JS
    <p>{2 + 2}</p>           // → 4
    <p>{user.name}</p>
    <img src={url} alt="photo" />
  </div>
);

// Différences JSX vs HTML :
class       → className
for         → htmlFor
onclick     → onClick  (camelCase)
style="..." → style={{ color: 'red' }}

// Fragment — éviter un div inutile
return (
  <>
    <h1>Titre</h1>
    <p>Paragraphe</p>
  </>
);

// Expressions JS dans JSX
<p>{isAdmin ? 'Admin' : 'Visiteur'}</p>
<div style={{ color: actif ? 'green' : 'red' }}>
Règles JSX importantes
// 1. Un seul élément racine (ou Fragment)
// ✗ Incorrect
return (
  <h1>Titre</h1>
  <p>Texte</p>   // ← erreur !
);

// ✅ Correct — Fragment
return (
  <>
    <h1>Titre</h1>
    <p>Texte</p>
  </>
);

// 2. Les balises doivent être fermées
<input />   // ✅ self-closing obligatoire
<br />
<img src="..." />

// 3. Les commentaires JSX
{/* Ceci est un commentaire JSX */}

// 4. Style inline = objet JS double {{
<div style={{ fontSize: '16px', marginTop: '1rem' }}>

// 5. className pour les classes CSS
<div className="container active">

// 6. Spread props
const props = { id: 'main', role: 'main' };
<div {...props}>

Composants

Composant fonctionnel
// Composant = fonction qui retourne du JSX
// Nom commence par une MAJUSCULE obligatoire

function Bouton({ label, onClick, variant = 'primary' }) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

// Utilisation
<Bouton label="Valider" onClick={handleSubmit} />
<Bouton label="Annuler" variant="danger" />

// Arrow function (aussi valide)
const Card = ({ titre, description, children }) => (
  <div className="card">
    <h2>{titre}</h2>
    <p>{description}</p>
    {children}    // ← contenu fils
  </div>
);

// Utiliser children
<Card titre="Mon titre">
  <p>Contenu personnalisé</p>
</Card>
Props & PropTypes / TypeScript
// Typage avec TypeScript (recommandé)
interface CardProps {
  titre:       string;
  description: string;
  image?:      string;    // optionnel
  onClick?:    () => void;
  children?:   React.ReactNode;
}

function Card({ titre, description, image, onClick, children }: CardProps) {
  return (
    <div className="card" onClick={onClick}>
      {image && <img src={image} alt={titre} />}
      <h2>{titre}</h2>
      <p>{description}</p>
      {children}
    </div>
  );
}

// Règles d'or des composants :
// ✅ Pures — même props → même résultat
// ✅ Ne pas modifier les props reçues
// ✅ Un composant = une responsabilité
// ✅ Nommer clairement (nom du rôle)
// ✅ Extraire si > ~100 lignes

Props — passage de données

Passer & recevoir des props
// Props = propriétés passées à un composant enfant
// Sens unique : parent → enfant (unidirectionnel)

// Parent
function ParentPage() {
  const user = { nom: 'Alice', age: 28 };
  return <ProfilCard user={user} onEdit={handleEdit} />;
}

// Enfant — destructuring des props
function ProfilCard({ user, onEdit }) {
  return (
    <div>
      <p>{user.nom}, {user.age} ans</p>
      <button onClick={onEdit}>Modifier</button>
    </div>
  );
}

// Valeurs par défaut
function Badge({ label, color = 'blue', size = 'md' }) { ... }

// Spread props — transmettre tout
function InputCustom({ label, ...rest }) {
  return (
    <label>
      {label}
      <input {...rest} />   // tout le reste va à input
    </label>
  );
}
Communication enfant → parent (callbacks)
// Pour remonter de l'info vers le parent :
// passer une fonction callback en prop

function Parent() {
  const [message, setMessage] = useState('');

  const handleMessageFromChild = (msg) => {
    setMessage(msg);   // ← reçoit depuis l'enfant
  };

  return (
    <>
      <p>Message reçu : {message}</p>
      <Enfant onEnvoi={handleMessageFromChild} />
    </>
  );
}

function Enfant({ onEnvoi }) {
  return (
    <button onClick={() => onEnvoi('Bonjour !')}>
      Envoyer au parent
    </button>
  );
}

// Règle : les données descendent (props)
//         les événements remontent (callbacks)

useState — l'état local

useState — bases
import { useState } from 'react';

function Compteur() {
  // [valeur, fonctionMàJ] = useState(valeurInitiale)
  const [count, setCount] = useState(0);

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

// État avec objet
const [user, setUser] = useState({ nom: '', email: '' });

// ✅ Toujours créer un nouveau objet (immutabilité)
setUser({ ...user, nom: 'Alice' });

// ✗ Modifier directement = bug silencieux
user.nom = 'Alice';  // ← React ne re-render pas !
setUser(user);

// État avec tableau
const [items, setItems] = useState([]);
setItems([...items, newItem]);         // ajouter
setItems(items.filter(i => i.id !== id)); // supprimer
setItems(items.map(i => i.id === id ? {...i, done: true} : i)); // modifier
useState — pièges courants
// 1. Mise à jour asynchrone — utiliser la forme fonctionnelle
function incrementTrois() {
  // ✗ count est capturé à la valeur initiale
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);  // → count + 1 seulement !

  // ✅ forme fonctionnelle — reçoit la valeur à jour
  setCount(c => c + 1);
  setCount(c => c + 1);
  setCount(c => c + 1);  // → count + 3 ✓
}

// 2. Initialisation coûteuse — lazy initializer
const [data, setData] = useState(() => {
  return JSON.parse(localStorage.getItem('data')) || [];
});
// La fonction n'est appelée qu'une seule fois

// 3. Regrouper les états liés avec useReducer
// Si plusieurs états évoluent toujours ensemble :
const [form, setForm] = useState({
  nom: '', email: '', age: ''
});
// Mise à jour partielle avec spread
setForm(f => ({ ...f, nom: 'Alice' }));

useEffect — effets de bord

MountuseEffect(() => {…}, [])
UpdateuseEffect(() => {…}, [dep])
Unmountreturn () => cleanup()
useEffect — patterns
import { useState, useEffect } from 'react';

// 1. Exécuter à chaque render (rarement utile)
useEffect(() => { console.log('render'); });

// 2. Exécuter une seule fois au montage
useEffect(() => {
  fetchUserData();
}, []);   // ← tableau vide = montage seulement

// 3. Exécuter quand une dépendance change
useEffect(() => {
  document.title = `Résultats pour "${query}"`;
}, [query]);   // ← relancé à chaque changement de query

// 4. Fetch de données avec cleanup
useEffect(() => {
  let cancelled = false;

  async function fetchData() {
    const res = await fetch(`/api/users/${id}`);
    const data = await res.json();
    if (!cancelled) setUser(data);
  }
  fetchData();

  return () => { cancelled = true; };  // cleanup
}, [id]);

// 5. Abonnement / désabonnement
useEffect(() => {
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);
Pièges useEffect
// ⚠ Dépendances manquantes → comportement inattendu
// Le linter ESLint (react-hooks/exhaustive-deps) détecte ça

// ✗ count manquant dans les dépendances
useEffect(() => {
  console.log(count);  // count est toujours 0 !
}, []);

// ✅ Ajouter la dépendance
useEffect(() => {
  console.log(count);
}, [count]);

// ⚠ Objet/tableau en dépendance → boucle infinie
// ✗ Boucle infinie (options recréé à chaque render)
const options = { limit: 10 };
useEffect(() => { fetchData(options); }, [options]);

// ✅ Utiliser useMemo ou déplacer la déclaration
const options = useMemo(() => ({ limit: 10 }), []);

// ⚠ async directement dans useEffect
// ✗ Incorrect
useEffect(async () => { await fetch(...) }, []);

// ✅ Correct — fonction async à l'intérieur
useEffect(() => {
  const load = async () => { await fetch(...) };
  load();
}, []);

useRef, useMemo & useCallback

useRef — référence sans re-render
import { useRef } from 'react';

// 1. Référencer un élément DOM
function SearchInput() {
  const inputRef = useRef(null);

  const focusInput = () => inputRef.current.focus();

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}

// 2. Stocker une valeur sans déclencher de re-render
function Timer() {
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => {...}, 1000);
  };

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

  return <><button onClick={start}>Start</button>
             <button onClick={stop}>Stop</button></>;
}

// 3. Valeur précédente
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}
useMemo & useCallback
import { useMemo, useCallback } from 'react';

// useMemo — mémoïser un calcul coûteux
function ListeFiltrée({ items, filtre }) {
  // Recalcule seulement si items ou filtre change
  const itemsFiltres = useMemo(() => {
    console.log('Calcul du filtre...');
    return items.filter(i => i.nom.includes(filtre));
  }, [items, filtre]);

  return <ul>{itemsFiltres.map(i => <li>{i.nom}</li>)}</ul>;
}

// useCallback — mémoïser une fonction
// Évite de recréer la fonction à chaque render
function Parent() {
  const [count, setCount] = useState(0);

  // ✅ handleClick stable entre les renders
  const handleClick = useCallback(() => {
    console.log('cliqué');
  }, []);  // ← ne dépend de rien → jamais recréé

  return <EnfantMemoise onClick={handleClick} />;
}

// React.memo — éviter les re-renders inutiles
const EnfantMemoise = React.memo(function Enfant({ onClick }) {
  console.log('Render Enfant');  // seulement si onClick change
  return <button onClick={onClick}>Cliquer</button>;
});

useContext — état partagé

Créer et utiliser un contexte
// 1. Créer le contexte
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

// 2. Provider — fournir la valeur
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Navbar />
      <Main />
    </ThemeContext.Provider>
  );
}

// 3. Consumer — utiliser depuis n'importe quel enfant
function BoutonTheme() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Thème actuel : {theme}
    </button>
  );
}
// Pas besoin de passer theme et setTheme via les props !
Contexte avec hook personnalisé
// Pattern recommandé — encapsuler dans un hook

// auth-context.jsx
const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = async (email, password) => {
    const data = await authApi.login(email, password);
    setUser(data.user);
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Hook custom — plus pratique que useContext direct
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth hors AuthProvider');
  return ctx;
}

// Utilisation dans un composant :
const { user, login, logout } = useAuth();

Hooks personnalisés

Hooks utiles à créer
// useFetch — fetch de données générique
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(r => r.json())
      .then(d  => { if (!cancelled) { setData(d); setLoading(false); } })
      .catch(e => { if (!cancelled) { setError(e); setLoading(false); } });

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

  return { data, loading, error };
}

// Utilisation
function UserList() {
  const { data, loading, error } = useFetch('/api/users');
  if (loading) return <Spinner />;
  if (error)   return <ErrorMsg error={error} />;
  return <ul>{data.map(u => <li>{u.name}</li>)}</ul>;
}
useLocalStorage & useDebounce
// useLocalStorage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch { return initialValue; }
  });

  const setValue = (value) => {
    setStoredValue(value);
    localStorage.setItem(key, JSON.stringify(value));
  };

  return [storedValue, setValue];
}

// useDebounce — délai avant d'appliquer une valeur
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

  return debouncedValue;
}

// useDebounce pour la recherche
function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) searchApi(debouncedQuery);
  }, [debouncedQuery]);   // API appelée 500ms après la saisie
}

Listes & clés

Rendu de listes avec .map()
const produits = [
  { id: 1, nom: 'Clavier', prix: 45 },
  { id: 2, nom: 'Souris',  prix: 25 },
  { id: 3, nom: 'Écran',   prix: 320 },
];

function ListeProduits() {
  return (
    <ul>
      {produits.map(produit => (
        <li key={produit.id}>  // ← key OBLIGATOIRE
          {produit.nom} — {produit.prix} €
        </li>
      ))}
    </ul>
  );
}

// ✅ key = identifiant stable et unique
// ✗ key={index} → bug si liste réorganisée

// Composant pour chaque item
function ListeProduits({ produits }) {
  return (
    <ul>
      {produits.map(p => (
        <ProduitItem key={p.id} produit={p} />
      ))}
    </ul>
  );
}

function ProduitItem({ produit }) {
  return <li>{produit.nom} — {produit.prix} €</li>;
}
Rendu conditionnel
// 1. Opérateur ternaire
{isLoggedIn
  ? <Dashboard />
  : <LoginPage />
}

// 2. Court-circuit &&
{isAdmin && <AdminPanel />}
{errors.length > 0 && <ErrorList errors={errors} />}

// 3. If précoce (guard clause)
function UserProfile({ user }) {
  if (!user) return <p>Chargement…</p>;
  if (user.error) return <ErrorPage />;
  return <div>{user.nom}</div>;
}

// 4. Switch / objet lookup
const composants = {
  loading: () => <Spinner />,
  error:   () => <ErrorMsg />,
  success: () => <DataView />,
};
const Comp = composants[status];
return <Comp />;

// 5. Retourner null = ne rien afficher
function Alerte({ message }) {
  if (!message) return null;
  return <div className="alerte">{message}</div>;
}

Formulaires contrôlés

Formulaire contrôlé
function FormulaireContact() {
  const [form, setForm] = useState({
    nom: '', email: '', message: ''
  });
  const [errors, setErrors] = useState({});

  // Handler générique — utilise le name de l'input
  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(f => ({ ...f, [name]: value }));
  };

  const validate = () => {
    const errs = {};
    if (!form.nom)   errs.nom = 'Nom requis';
    if (!form.email.includes('@')) errs.email = 'Email invalide';
    return errs;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate();
    if (Object.keys(errs).length) { setErrors(errs); return; }
    submitApi(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="nom" value={form.nom} onChange={handleChange} />
      {errors.nom && <span>{errors.nom}</span>}
      <input name="email" value={form.email} onChange={handleChange} />
      <button type="submit">Envoyer</button>
    </form>
  );
}
React Hook Form (bibliothèque)
// npm install react-hook-form
// Beaucoup moins de boilerplate, meilleures perfs

import { useForm } from 'react-hook-form';

function FormulaireRHF() {
  const {
    register, handleSubmit,
    formState: { errors }
  } = useForm();

  const onSubmit = (data) => {
    console.log(data);  // { nom: '...', email: '...' }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('nom', {
          required: 'Nom requis',
          minLength: { value: 2, message: 'Trop court' }
        })}
      />
      {errors.nom && <p>{errors.nom.message}</p>}

      <input
        {...register('email', {
          required: 'Email requis',
          pattern: { value: /^[^@]+@[^@]+$/, message: 'Email invalide' }
        })}
      />
      {errors.email && <p>{errors.email.message}</p>}

      <button type="submit">Envoyer</button>
    </form>
  );
}

Lifting state up

Partager l'état entre frères
// Problème : deux composants frères ont besoin
// du même état → le remonter au parent commun

// ✅ État dans le parent commun
function ConverterApp() {
  const [celsius, setCelsius] = useState('');

  const fahrenheit = celsius ? celsius * 9/5 + 32 : '';

  return (
    <>
      <InputTemp
        label="Celsius"
        value={celsius}
        onChange={setCelsius}
      />
      <InputTemp
        label="Fahrenheit"
        value={fahrenheit}
        onChange={f => setCelsius((f - 32) * 5/9)}
      />
    </>
  );
}

function InputTemp({ label, value, onChange }) {
  return (
    <label>
      {label} :
      <input
        value={value}
        onChange={e => onChange(e.target.value)}
      />
    </label>
  );
}
useReducer — état complexe
import { useReducer } from 'react';

const initialState = { items: [], total: 0 };

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        items: [...state.items, action.item],
        total: state.total + action.item.prix
      };
    case 'REMOVE_ITEM':
      const item = state.items.find(i => i.id === action.id);
      return {
        items: state.items.filter(i => i.id !== action.id),
        total: state.total - item.prix
      };
    case 'CLEAR':
      return initialState;
    default:
      return state;
  }
}

function Panier() {
  const [cart, dispatch] = useReducer(cartReducer, initialState);

  return (
    <>
      <button onClick={() => dispatch({ type: 'CLEAR' })}>
        Vider ({cart.items.length})
      </button>
    </>
  );
}

React Router v6

Configuration des routes
// npm install react-router-dom

// main.jsx
import { BrowserRouter } from 'react-router-dom';

createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

// App.jsx
import { Routes, Route, Navigate } from 'react-router-dom';

function App() {
  return (
    <Routes>
      <Route path="/"           element={<HomePage />} />
      <Route path="/about"      element={<AboutPage />} />
      <Route path="/users"      element={<UsersPage />} />
      <Route path="/users/:id"  element={<UserDetail />} />
      <Route path="*"           element={<NotFound />} />
    </Routes>
  );
}

// Routes imbriquées (layout)
<Route path="/dashboard" element={<DashLayout />}>
  <Route index         element={<DashHome />} />
  <Route path="stats"  element={<Stats />} />
  <Route path="users"  element={<Users />} />
</Route>
Navigation & hooks router
import { Link, NavLink, useNavigate,
         useParams, useSearchParams } from 'react-router-dom';

// Liens
<Link to="/about">À propos</Link>
<NavLink                         // ajoute class "active"
  to="/dashboard"
  className={({isActive}) => isActive ? 'active' : ''}
>Dashboard</NavLink>

// Navigation programmée
const navigate = useNavigate();
navigate('/home');
navigate(-1);            // retour arrière
navigate('/login', { replace: true });  // remplace l'historique

// Paramètres d'URL (/users/:id)
const { id } = useParams();
// URL /users/42 → id = "42"

// Query strings (/search?q=react)
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q');       // "react"
setSearchParams({ q: 'angular' });        // mise à jour URL

// Outlet — rendu des routes enfants
import { Outlet } from 'react-router-dom';
function DashLayout() {
  return <><Sidebar /><Outlet /></>;
}

Gestion d'état globale

Zustand — le plus simple
// npm install zustand

import { create } from 'zustand';

// Créer le store
const useCartStore = create((set, get) => ({
  items: [],
  total: 0,

  addItem: (item) => set(state => ({
    items: [...state.items, item],
    total: state.total + item.prix,
  })),

  removeItem: (id) => {
    const item = get().items.find(i => i.id === id);
    set(state => ({
      items: state.items.filter(i => i.id !== id),
      total: state.total - item.prix,
    }));
  },

  clear: () => set({ items: [], total: 0 }),
}));

// Utiliser dans n'importe quel composant
function CartIcon() {
  const { items, clear } = useCartStore();
  return <button onClick={clear}>🛒 {items.length}</button>;
}
SolutionQuand l'utiliserComplexité
useStateÉtat local d'un composant
useContextDonnées partagées simples (thème, user)⭐⭐
useReducerÉtat complexe avec transitions définies⭐⭐
ZustandÉtat global simple, peu de boilerplate⭐⭐
JotaiÉtat atomique, performances optimales⭐⭐
Redux ToolkitLarge app, besoin de devtools avancés⭐⭐⭐
TanStack QueryÉtat serveur (cache, fetch, mutations)⭐⭐
💡

Règle : ne pas sur-ingénier. Commencer par useState + useContext. Ajouter Zustand ou TanStack Query si les besoins le justifient. Redux seulement pour les très grandes applications.

Performance

Lazy loading & Suspense
import { lazy, Suspense } from 'react';

// Charger un composant de manière paresseuse
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings"  element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
// Le bundle Dashboard n'est chargé que quand
// l'utilisateur navigue vers /dashboard

// Error Boundary — attraper les erreurs de rendu
import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary fallback={<ErrorPage />}>
  <MonComposant />
</ErrorBoundary>
Optimisations clés
// 1. React.memo — éviter les re-renders inutiles
const ListItem = React.memo(({ item, onDelete }) => (
  <li>{item.nom} <button onClick={() => onDelete(item.id)}>✕</button></li>
));

// 2. useCallback pour les handlers passés à memo
const handleDelete = useCallback((id) => {
  setItems(items => items.filter(i => i.id !== id));
}, []);   // ← stable, ListItem ne re-rend pas inutilement

// 3. Virtualisation pour les grandes listes
// npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
// Affiche seulement les éléments visibles (100k lignes = ok)

// 4. Code splitting — importer seulement ce dont on a besoin
import debounce from 'lodash/debounce';  // ✅ tree-shakeable
import _ from 'lodash';                  // ✗ bundle entier

// 5. Profiler React DevTools
// Installer l'extension navigateur "React DevTools"
// Onglet Profiler → enregistrer → voir qui re-render

Cheat sheet React

Hooks essentiels

useState(init)État local, re-render au changement
useEffect(fn, deps)Effets de bord après le render
useRef(init)Référence DOM ou valeur stable
useContext(ctx)Lire un contexte
useMemo(fn, deps)Calcul mémoïsé
useCallback(fn, deps)Fonction mémoïsée

Règles des hooks

✅ Au top levelJamais dans if/for/fonctions
✅ Dans composants ReactOu dans des hooks custom
Préfixe use*Obligatoire pour les hooks custom
Dépendances exhaustivesLinter eslint-plugin-react-hooks

État — immutabilité

ObjetsetObj({ ...obj, key: val })
AjoutersetArr([...arr, item])
SupprimersetArr(arr.filter(...))
ModifiersetArr(arr.map(...))
Forme fnsetState(s => s + 1)

Écosystème

React RouterNavigation SPA
TanStack QueryFetch & cache serveur
ZustandÉtat global simple
React Hook FormFormulaires performants
ZodValidation de schémas