React Context API et performance
React & Performance

🧱 Les erreurs courantes dans la gestion du Context React

Comment éviter les re-renders en cascade, le God Context et les patterns anti-performance. Guide pragmatique et actionnable.

Le Context API de React est puissant pour partager des informations dans une application. Mais mal employé, il peut provoquer des re-renders en cascade, dégrader les performances et rendre le code difficile à faire évoluer. Voici un guide pragmatique et actionnable pour éviter les pièges les plus fréquents.

Pourquoi le contexte re-render "tout" si souvent

  • Un Provider re-render dès que sa valeur change, et tous les consommateurs descendants re-render aussi.
  • Passer des objets/fonctions non mémoïsés crée de nouvelles références à chaque render.
  • Mélanger "état qui bouge tout le temps" et "API stable" dans un même contexte amplifie l'effet domino.

💡 Point clé : Le Context API ne fait PAS de "smart re-rendering". Dès que la valeur du Provider change (référence différente), React re-render tous les composants qui consomment ce contexte, même s'ils n'utilisent qu'une fraction de l'état.

Les erreurs les plus fréquentes

❌ 1. Le "God Context" (contexte fourre-tout)

Regrouper auth, thème, préférences, data, notifications dans un seul contexte monolithique.

Conséquence : Chaque changement (même mineur) re-render TOUS les consommateurs, même ceux qui n'utilisent qu'une fraction de l'état.

❌ 2. Stocker des valeurs à haute fréquence

Inputs contrôlés, données temps réel (websockets), timers dans le contexte.

Conséquence : Des dizaines/centaines de re-renders par seconde sur toute l'arborescence.

❌ 3. useContext partout par défaut

Ajouter useContext dans chaque composant au lieu de découper et rapprocher l'état du composant qui l'utilise.

Conséquence : Surface de re-render maximale, couplage fort, difficile à tester et refactorer.

❌ 4. Oublier la stabilité des références

Valeurs d'un Provider non mémoïsées (objets/fonctions recréés à chaque render).

// ❌ Mauvais : nouvelle référence à chaque render
<MyContext.Provider value={{ user, setUser }}>

// ✅ Bon : référence stable
const value = useMemo(() => ({ user, setUser }), [user, setUser])

❌ 5. Coupler lecture et écriture dans un seul contexte

State + mutations dans le même contexte obligent à propager des mises à jour inutiles.

Conséquence : Composants qui n'affichent que des fonctions (actions) re-render quand l'état change.

❌ 6. Dériver des objets complexes dans le Provider

Créer des objets complexes { a, b, c, helper(a,b) } à chaque render du Provider.

Conséquence : Calculs coûteux répétés, références instables, re-renders en cascade.

Les bonnes pratiques essentielles

✅ 1. Créer plusieurs contextes spécialisés

Exemple : AuthContext, ThemeContext, SettingsContext, PermissionsContext.

Bénéfice : On isole les surfaces de re-render. Un changement de thème ne re-render pas les composants qui consomment uniquement l'auth.

✅ 2. Séparer API (mutations) et State (muable)

Contexte "API" (fonctions) stable : ne change jamais ou très rarement.
Contexte "State" pour la partie qui varie réellement.

Résultat : On peut utiliser les APIs sans causer de re-render.

// Context A : API (stable)
const AuthAPI = createContext({ login, logout, refresh })

// Context B : State (muable)
const AuthState = createContext({ user, isLoading })

✅ 3. Mémoïser la valeur du Provider

Stabiliser objets et fonctions (mémoïsation, références stables). N'exposer que le nécessaire.

const value = useMemo(
  () => ({ user, setUser }),
  [user, setUser]
)
<MyContext.Provider value={value}>

✅ 4. Préférer les props quand c'est local

Si l'information est consommée par quelques composants proches, pas besoin de contexte. Un simple passage de props est plus lisible et performant.

✅ 5. Co-localiser l'état

Garder l'état au plus près du composant qui le consomme. Ne "monter" l'état que si indispensable.

Règle d'or : État local par défaut, contexte uniquement si nécessaire.

✅ 6. Segmenter la consommation

Créer des hooks sélectifs (lecture d'un seul fragment). Éviter de consommer la valeur globale.

// ❌ Mauvais : consomme tout
const { user, theme, settings } = useAppContext()

// ✅ Bon : hook sélectif
const user = useUser()
const theme = useTheme()

Patterns recommandés (simples et efficaces)

🔵Pattern 1 : Dual Contexts (lecture/écriture séparées)

Context A : API (fonctions stables, ne re-render pas).
Context B : State (données mutables).

Les composants qui déclenchent des actions importent l'API ; ceux qui affichent, le State.

// authContext.tsx
const AuthAPIContext = createContext()
const AuthStateContext = createContext()

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

  // API stable (ne change jamais)
  const api = useMemo(() => ({
    login: async (credentials) => { /* ... */ },
    logout: () => { /* ... */ },
    refresh: () => { /* ... */ }
  }), [])

  // State muable
  const state = useMemo(() => ({
    user,
    isLoading
  }), [user, isLoading])

  return (
    <AuthAPIContext.Provider value={api}>
      <AuthStateContext.Provider value={state}>
        {children}
      </AuthStateContext.Provider>
    </AuthAPIContext.Provider>
  )
}

// Hooks sélectifs
export const useAuthAPI = () => useContext(AuthAPIContext)
export const useAuthState = () => useContext(AuthStateContext)

// Usage
// Composant qui affiche : useAuthState() → re-render quand user change
// Composant qui agit : useAuthAPI() → ne re-render JAMAIS

🟣Pattern 2 : Provider minimal

Exposer des scalaires/objets stables, pas des objets reconstruits à la volée. Extraire les calculs dérivés hors de la valeur du Provider.

// ❌ Mauvais : calculs dérivés dans Provider
const value = {
  user,
  isAdmin: user?.role === 'admin', // recalculé à chaque render
  fullName: `${user?.firstName} ${user?.lastName}`
}

// ✅ Bon : exposer uniquement les données brutes
const value = useMemo(() => ({ user }), [user])

// Calculs dérivés dans custom hook ou composant
const useIsAdmin = () => {
  const { user } = useAuthState()
  return user?.role === 'admin'
}

🟠Pattern 3 : External store pour le très dynamique

Pour du state global très mouvant (sockets, formulaires complexes), envisager useSyncExternalStore ou un store externe (Zustand, Redux Toolkit) pour contrôler finement les re-renders.

Bénéfice : Selectors granulaires, performance optimale pour les données temps réel.

✅ Check-list anti re-render

Le contexte contient-il uniquement des valeurs réellement "globales" et stables ?
Les fonctions exposées sont-elles stables dans le temps (références inchangées) ?
Ai-je séparé "API" et "State" en deux contextes distincts ?
Puis-je remplacer un usage de contexte par une prop locale ?
Ai-je évité d'inclure des valeurs haute-fréquence (saisie, horloges) ?
Les composants consommateurs lisent-ils uniquement le strict nécessaire ?

🔄 Stratégie de migration progressive

1

Identifier le "God context" et les re-renders indésirables

Utiliser React DevTools Profiler pour mesurer les re-renders et identifier les hotspots.

2

Découper en contextes spécialisés par domaine fonctionnel

Créer AuthContext, ThemeContext, SettingsContext, etc.

3

Scinder API (fonctions) et State (muable)

Implémenter le pattern Dual Context pour chaque domaine.

4

Stabiliser la valeur des Providers

Ajouter useMemo/useCallback pour stabiliser les références.

5

Ramener l'état au plus près des consommateurs

Descendre l'état local là où il est réellement consommé.

6

Mesurer à nouveau et itérer

Re-profiler avec React DevTools et comparer les résultats avant/après.

⚠️ Quand ne PAS utiliser le contexte

  • Données locales ou éphémères (compteur, toggle, formulaire simple).
  • Données extrêmement volatiles (temps réel, inputs) sans besoin de diffusion globale.
  • Cas où un simple passage de props est plus lisible et plus performant (composants proches).

Règle d'or : État local par défaut. Contexte uniquement si plusieurs composants distants dans l'arborescence ont besoin de partager une information stable.

🎯 Résultat attendu

Performance

Moins de re-renders, meilleure réactivité UI, temps de réponse réduits, Core Web Vitals optimisés.

🧩

Maintenabilité

Code prévisible, domaines bien séparés, tests facilités, refactoring simplifié.

📈

Scalabilité

Architecture prête pour la montée en charge (features, équipes, perfs).

Et vous, plutôt "un seul contexte pour tout" ou "context minimaliste" ?

Choisissez la clarté : plusieurs contextes spécialisés, API stables, state ciblé. Votre UI — et vos Core Web Vitals — vous diront merci.

📚 Articles connexes