React 18 a ajouté useSyncExternalStore, un hook qui permet aux composants de lire des données provenant de n'importe quelle source située en dehors de l'arbre React — API du navigateur, WebSockets ou stores tiers — sans les problèmes d'affichage qui affectaient l'ancien modèle consistant à dupliquer ces données avec useState et useEffect. Si vous avez déjà vu une interface utilisateur scintiller parce qu'une partie affichait une ancienne taille de fenêtre alors qu'une autre reflétait déjà la nouvelle, ce hook est la solution que vous attendiez.

Pourquoi l'état externe nécessite un nouveau hook

Les composants React ont toujours pu s'abonner à des éléments existant en dehors de leur propre portée : l'objet window, un store Redux, une connexion WebSocket. L'approche conventionnelle consistait à mettre en place un abonnement à l'intérieur d'un useEffect, à pousser les valeurs entrantes dans l'état local via setState, et à laisser React re-rendre. Cela fonctionne très bien lorsque React effectue un rendu synchrone, mais React 18 a introduit le rendu concurrent (concurrent rendering), où un composant peut être rendu plusieurs fois avant que le navigateur ne procède réellement à l'affichage (paint). Dans ce mode, le modèle de « duplication dans l'état local » peut produire du tearing — différentes parties de l'interface utilisateur lisant des instantanés (snapshots) différents de la même valeur externe au cours d'un seul cycle de rendu.

useSyncExternalStore a été conçu pour prévenir le tearing. Il demande à la source externe un snapshot (la valeur actuelle) et une fonction subscribe (pour être notifié des changements). React appelle subscribe lors du montage du composant et se désabonne automatiquement lors du démontage. Chaque fois que le snapshot renvoyé par getSnapshot change, React planifie un rendu qui lit le même snapshot pour l'ensemble de l'arbre des composants, garantissant ainsi la cohérence.

Les deux fonctions requises

Fonction Ce qu'elle fait
subscribe Reçoit un callback qui doit être appelé chaque fois que la valeur externe change. Elle retourne une fonction unsubscribe que React appellera lors du nettoyage (cleanup).
getSnapshot Retourne la valeur actuelle de la source externe. Elle doit être référentiellement stable — le même objet fonction à chaque rendu — afin que React puisse comparer correctement les snapshots.

Une implémentation minimale pour le statut online du navigateur ressemble à ceci :

function useOnlineStatus() {
  return useSyncExternalStore(
    (onChange) => {
      window.addEventListener('online', onChange);
      window.addEventListener('offline', onChange);
      return () => {
        window.removeEventListener('online', onChange);
        window.removeEventListener('offline', onChange);
      };
    },
    () => navigator.onLine
  );
}

Lorsqu'un composant appelle useOnlineStatus(), React le re-rendra uniquement lorsque navigator.onLine changera réellement, et chaque rendu verra la même valeur.

Les règles pour éviter que le hook ne plante

  1. Retournez la plus petite portion de données dont un composant a besoin.
    Si le store externe contient un objet volumineux mais qu'un composant n'est intéressé que par un seul champ, ne retournez que ce champ. Des retours plus volumineux provoquent des rendus inutiles.

  2. Mémoïsez getSnapshot.
    Si vous recréez la fonction à chaque rendu, React la considérera comme une nouvelle source et pourra boucler indéfiniment. Enveloppez-la dans useCallback ou définissez-la en dehors du composant.

  3. Évitez de retourner de nouveaux objets à chaque appel.
    Retourner un nouvel objet (par exemple, { count: store.getCount() }) crée une nouvelle référence à chaque rendu, ce qui fait croire à React que le snapshot a changé et déclenche une boucle de rendu infinie. Utilisez des primitives, des chaînes de caractères, des nombres ou des objets mémoïsés.

  4. Ne l'utilisez pas pour l'état interne d'un composant.
    useState reste l'outil approprié pour les données qui vivent uniquement à l'intérieur du composant. useSyncExternalStore ajoute une surcharge (overhead) inutile pour l'état local.

Quand l'utiliser

  • API du navigateur – taille de la fenêtre, media queries, navigator.onLine, statut de la batterie.
  • Stores agnostiques au framework – Zustand, Redux, MobX, ou tout store personnalisé exposant une API d'abonnement.
  • Sources mutables qui se mettent à jour en dehors de React – messages WebSocket, événements de changement IndexedDB, notifications de Service Worker.

Si votre source de données réside déjà dans React (par exemple, l'état d'un composant parent), tenez-vous-en à useState ou au context.

Ce que dit la communauté

Les premiers utilisateurs rapportent que useSyncExternalStore élimine le scintillement observé lors du redimensionnement des fenêtres dans une configuration de rendu concurrent. Certaines bibliothèques ont déjà migré leurs hooks internes vers cette API, promettant un comportement plus prévisible à travers les versions de React. La contrepartie est une charge mentale légèrement plus élevée : les développeurs doivent réfléchir à la stabilité des deux fonctions et éviter de retourner de nouveaux objets à chaque appel.

À surveiller ensuite

Les prochaines versions de React pourraient renforcer le contrat autour de useSyncExternalStore, en ajoutant potentiellement des helpers intégrés pour les API de navigateur courantes. Gardez un œil sur le blog officiel de React pour toute dépréciation de l'ancien modèle calqué sur useEffect. En attendant, le hook est stable et fait partie de l'API publique, vous pouvez donc refactoriser en toute sécurité votre code d'état externe existant.

Essai rapide

Une démo en direct présentant la synchronisation de la taille de la fenêtre, la détection du statut en ligne/hors ligne et un store Zustand simple est disponible sur https://usesyncexternalstore.vercel.app/. Le code source, entièrement commenté, est disponible sur https://github.com/dev48v/usesyncexternalstore.

À retenir : useSyncExternalStore offre à React un pont fiable vers toute donnée mutable résidant en dehors de son cycle de rendu, évitant ainsi le tearing et les rendus inutiles. Appliquez-le à des sources externes, gardez le snapshot minimal et stable, et laissez React s'occuper du gros du travail pour assurer la cohérence.