Retour au blog

Les hooks React que tu utilises tous les jours (mais probablement mal)

useState, useEffect et useContext sont responsables de 90% des bugs en prod. Voici ce qu'on ne t'a jamais dit.

21 janvier 20268 min de lecture
ReactHooksTypeScriptJavaScript
Les hooks React que tu utilises tous les jours (mais probablement mal)
Le Problème du Snapshot - useState, useEffect, useContext
Le Problème du Snapshot - useState, useEffect, useContext

Ça fait des années que tu utilises useState, useEffect, et useContext. C'est devenu un réflexe. Tu les tapes sans réfléchir.

C'est exactement le problème.

Ces trois hooks sont responsables de 90% des bugs que j'ai debuggé en prod. Pas parce qu'ils sont cassés, mais parce qu'on les a tous mal appris dès le départ.

Voici ce qu'on ne t'a jamais dit quand tu as commencé.


useState : Le mensonge qu'on t'a raconté

Tous les tutos enseignent ça :

const [count, setCount] = useState(0);

function increment() {
  setCount(count + 1);
}

Simple. Propre. Cassé.

Clique assez vite sur ce bouton, et tu verras qu'il saute des chiffres. Pourquoi ? Parce que count est un snapshot, pas une valeur live. Quand React batch tes clics, ils lisent tous le même count périmé.

Le fix qu'on aurait dû t'apprendre dès le début

const [count, setCount] = useState(0);

function increment() {
  setCount(prevCount => prevCount + 1);
}

Le pattern de mise à jour fonctionnelle n'est pas "avancé." C'est la façon par défaut dont tu devrais écrire tes mises à jour de state.

💡 Règle d'or : Si ton nouveau state dépend de l'ancien, utilise une fonction.

Le piège des objets

Celui-là attrape tout le monde :

const [user, setUser] = useState({ name: 'Samy', age: 25 });

// ❌ Ça ne fait rien du tout
function birthday() {
  user.age += 1;
  setUser(user);
}

React utilise Object.is() pour comparer les states. Même référence ? Pas de re-render. Tu as muté l'objet mais donné à React exactement la même adresse mémoire.

// ✅ Nouvel objet, nouvelle référence, React re-render
function birthday() {
  setUser(prev => ({ ...prev, age: prev.age + 1 }));
}

Le modèle mental

Pense à useState comme une photo, pas un flux vidéo live.

À chaque render, tu regardes un moment figé dans le temps. La valeur ne changera pas en plein render, peu importe combien de setCount tu appelles.


useEffect : Le hook le plus mal compris

J'ai vu des devs seniors écrire ça :

useEffect(() => {
  fetchUserData();
}, []);

"Le tableau de dépendances vide veut dire que ça run une seule fois au mount."

Ouais. Mais pourquoi ça run une seule fois ? Le tableau de dépendances fait quoi exactement ?

Ce n'est pas "quand exécuter"

Le tableau de dépendances est une liste des valeurs que ton effect lit depuis le scope du render.

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

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // L'effect lit userId, donc c'est une dépendance
}

Quand tu mens sur les dépendances, tu casses la synchronisation :

// ❌ Tu mens - cet effect lit userId mais ne le déclare pas
useEffect(() => {
  fetchUser(userId).then(setUser);
}, []); // Bug: fetch toujours le premier userId, ignore les changements

Le cleanup que personne n'écrit

Voici une race condition qui se cache en plein jour :

useEffect(() => {
  fetchUser(userId).then(data => {
    setUser(data); // Et si userId a changé avant que ça resolve ?
  });
}, [userId]);

Si l'utilisateur navigue assez vite, tu afficheras des données périmées. Le fix :

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

  fetchUser(userId).then(data => {
    if (!cancelled) {
      setUser(data);
    }
  });

  return () => {
    cancelled = true; // Cleanup: ignore les réponses périmées
  };
}, [userId]);

Cette fonction de cleanup n'est pas une décoration optionnelle. C'est du code porteur.

Le modèle mental

Pense à useEffect comme de la synchronisation, pas du lifecycle.

Ton effect est le pont entre le monde de React (state, props) et le monde extérieur (APIs, subscriptions, manipulation DOM). Le cleanup détruit le pont avant d'en construire un nouveau.

Quand NE PAS utiliser useEffect

C'est probablement la partie la plus importante. Tu n'as pas besoin de useEffect pour :

Transformer des données pour le render :

// ❌ Anti-pattern
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);

useEffect(() => {
  setFilteredItems(items.filter(i => i.active));
}, [items]);

// ✅ Calcule-le directement
const filteredItems = items.filter(i => i.active);

Gérer des événements utilisateur :

// ❌ Surengineering
const [query, setQuery] = useState('');
useEffect(() => {
  search(query);
}, [query]);

// ✅ Appelle-le simplement dans le handler
function handleSearch(e) {
  const value = e.target.value;
  setQuery(value);
  search(value);
}

Réinitialiser le state quand les props changent :

// ❌ Effect pour reset le state
useEffect(() => {
  setComment('');
}, [postId]);

// ✅ La prop key force une nouvelle instance
<CommentForm key={postId} />

useContext : Le tueur de prop drilling (qui peut tuer la performance)

Context semble magique. Passer des données à travers l'arbre de composants sans props ? J'achète.

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <DeepNestedComponent />
    </ThemeContext.Provider>
  );
}

function DeepNestedComponent() {
  const { theme } = useContext(ThemeContext);
  return <div className={theme}>Hello</div>;
}

Magnifique. Jusqu'à ce que ton app rame.

La tempête de re-renders cachée

Chaque fois que la valeur du context change, tous les consumers re-render. Même s'ils n'utilisent qu'une partie de la valeur.

// ❌ Ça re-render TOUT au MOINDRE changement
<ThemeContext.Provider value={{ theme, user, settings, setTheme }}>

Le problème ? Chaque render crée une nouvelle référence d'objet. React voit un objet différent, déclenche des re-renders partout.

Le fix : stabilise tes valeurs

function App() {
  const [theme, setTheme] = useState('light');
  
  // Mémorise la valeur du context
  const contextValue = useMemo(
    () => ({ theme, setTheme }),
    [theme] // Ne recrée que quand theme change
  );

  return (
    <ThemeContext.Provider value={contextValue}>
      <Children />
    </ThemeContext.Provider>
  );
}

Le modèle mental

Pense au context comme un système de broadcast :

Quand le DJ (Provider) change la musique (value), tout le monde dans le club (consumers) réagit. Qu'ils aient voulu entendre cette chanson ou non.

Quand séparer les contexts

Si différentes parties de ton app s'intéressent à des données différentes, sépare-les :

// Au lieu d'un mega-context
const AppContext = createContext({ user, theme, settings });

// Sépare par responsabilité
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const SettingsContext = createContext({});

Maintenant les changements de thème ne re-render pas les composants qui ne s'intéressent qu'aux données user.


La cheat sheet

HookModèle MentalErreur CouranteFix
useStateSnapshot, pas valeur liveMuter les objetsToujours créer de nouvelles références
useEffectSynchronisation, pas lifecycleMentir sur les dépendancesListe tout ce que tu lis
useContextBroadcast à tous les consumersPasser de nouveaux objets à chaque renderMémorise les valeurs du context

La suite ?

Ces trois hooks gèrent 80% de ton code React. Mais quand ils ne suffisent plus, quand la performance compte, quand tu as besoin d'accès au DOM, quand la logique de state devient complexe... c'est là que le tier suivant de hooks devient essentiel.

Dans le prochain article, on couvrira useRef, useMemo, et useCallback. Les hooks que tu devrais utiliser plus souvent.

Ce ne sont pas des "hooks d'optimisation." Ce sont des hooks de correction qui améliorent aussi la performance.

Continuer vers la partie 2 : Les hooks que tu devrais utiliser plus →


Des questions ? Trouvé un bug dans mon code ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.