Menu

useSyncExternalStore : s'abonner à des données externes

useSyncExternalStore abonne un composant à des données qui vivent hors de React, comme un petit module de store ou une API du navigateur telle que navigator.onLine, et refait le rendu chaque fois que ces données changent. Découvrez subscribe et getSnapshot, pourquoi le snapshot doit être mis en cache, et getServerSnapshot.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

useSyncExternalStore abonne un composant à des données qui vivent hors de React et refait son rendu chaque fois que ces données changent. Vous lui donnez deux fonctions : subscribe, qui indique à React comment écouter les changements, et getSnapshot, qui renvoie la valeur actuelle.

Les deux composants Display ne partagent ni props ni contexte, et pourtant ils se mettent à jour ensemble, car tous deux sont abonnés au même store. Le bouton appelle une simple fonction, pas un setter React. Ajoutez un troisième <Display name="Sidebar" /> et il suit le mouvement sans aucune autre modification.

La syntaxe

const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
  • subscribe(callback) commence l'écoute, appelle callback chaque fois que les données ont pu changer et renvoie une fonction de désabonnement. React l'appelle après le montage du composant et appelle la fonction renvoyée au démontage.
  • getSnapshot() renvoie la valeur actuelle. React l'appelle à chaque rendu et après chaque notification, puis compare le résultat au précédent avec Object.is. Même valeur, pas de rendu.
  • getServerSnapshot() (facultatif) renvoie la valeur à utiliser sur le serveur et pendant l'hydratation.

Définissez subscribe hors du composant, ou gardez-le stable avec useCallback. Si vous passez une nouvelle fonction subscribe à chaque rendu, React se désabonne et se réabonne à chaque fois.

Les API du navigateur comme stores

Tout ce qui a une valeur actuelle et déclenche un événement quand elle change correspond à ce schéma. navigator.onLine avec les événements online et offline est le cas classique.

Coupez votre réseau (ou passez en mode hors ligne dans les outils de développement du navigateur) et le texte change sans rechargement. Le troisième argument dit « supposer qu'on est en ligne » lors du rendu sur le serveur, où il n'y a pas de navigator. Envelopper l'appel du hook dans useOnlineStatus en fait un hook personnalisé que n'importe quel composant peut utiliser.

La largeur de la fenêtre fonctionne de la même façon :

Ajoutez quelques copies et redimensionnez la fenêtre : chaque copie affiche le même nombre au même moment. Chaque copie a son propre écouteur, et chacune lit la largeur pendant le rendu, donc aucune n'a jamais une image de retard.

getSnapshot doit renvoyer une valeur en cache

React appelle souvent getSnapshot et compare les résultats par référence. Une fonction qui construit un nouvel objet ou un nouveau tableau à chaque appel ressemble toujours à un changement :

// Broken: a new object on every call
function getSnapshot() {
    return { count: store.count, user: store.user };
}

// Also broken: filter returns a new array every time
function getSnapshot() {
    return store.todos.filter((t) => !t.done);
}

React fait le rendu, appelle getSnapshot, reçoit une valeur « différente », refait le rendu, et ainsi de suite, jusqu'à s'arrêter avec "Maximum update depth exceeded". En développement, React affiche aussi d'abord "The result of getSnapshot should be cached to avoid an infinite loop". Un build de production, comme l'aperçu ici, saute cet avertissement et ne signale que l'erreur finale sous forme de code court (Minified React error #185).

La solution est de garder les données immuables dans le store : remplacez l'objet quand il change, et renvoyez la référence stockée telle quelle.

getSnapshot renvoie le même objet state jusqu'à ce que add le remplace, donc le composant fait un rendu par changement. Le filtrage a lieu dans le composant, après la lecture du snapshot, ce qui est sans risque. Pour filtrer plutôt dans le store, calculez le tableau filtré quand les données changent et stockez-le, pour que getSnapshot puisse renvoyer la copie stockée.

getServerSnapshot et l'hydratation

Sur le serveur, il n'y a ni fenêtre, ni navigator, ni abonnement. Le troisième argument indique à React quoi y afficher :

const width = useSyncExternalStore(
    subscribe,
    () => window.innerWidth, // in the browser
    () => 1024 // on the server, and during hydration
);

React utilise aussi getServerSnapshot pour le premier rendu dans le navigateur quand il hydrate le HTML du serveur, pour que les deux correspondent. Juste après l'hydratation, il lit getSnapshot et, si la vraie valeur diffère, refait le rendu avec elle. Sans troisième argument, le rendu serveur lève "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering." Si une frontière <Suspense> se trouve au-dessus du composant, le serveur envoie le contenu de repli de cette frontière et le navigateur rend son contenu à la place ; sans frontière, le rendu serveur échoue.

useSyncExternalStore ou useEffect et useState

Vous pouvez vous abonner avec un effet :

function useOnlineStatus() {
    const [online, setOnline] = useState(true);
    useEffect(() => {
        const update = () => setOnline(navigator.onLine);
        update();
        window.addEventListener('online', update);
        window.addEventListener('offline', update);
        return () => {
            window.removeEventListener('online', update);
            window.removeEventListener('offline', update);
        };
    }, []);
    return online;
}

Cela fonctionne, avec deux faiblesses. Le premier rendu affiche toujours la valeur initiale supposée, et la vraie valeur arrive un rendu plus tard, après l'exécution de l'effet. Et avec le rendu concurrent (pendant une transition, par exemple), React peut mettre un rendu en pause en cours de route ; si le store change pendant la pause, les composants rendus avant et après peuvent afficher des valeurs différentes. Cette incohérence s'appelle le tearing. useSyncExternalStore lit la valeur pendant le rendu et oblige React à refaire le rendu de façon synchrone si le store a changé, donc chaque composant voit la même valeur.

Utilisez-le quand les données vivent hors de React : votre propre module de store, une API du navigateur, une bibliothèque tierce. La plupart des bibliothèques d'état (Redux, Zustand et d'autres) l'appellent pour vous dans leurs hooks. Pour les données qui appartiennent à vos composants, useState, useReducer et le contexte restent les bons outils.

Questions fréquentes

À quoi sert useSyncExternalStore ?

À lire des données que React ne possède pas et qui peuvent changer d'elles-mêmes : un store écrit hors de React, une bibliothèque d'état tierce, ou une valeur du navigateur comme navigator.onLine ou la largeur de la fenêtre. Le composant refait son rendu chaque fois que le store signale à React qu'il a changé.

Que font subscribe et getSnapshot ?

subscribe(callback) commence à écouter le store, appelle callback à chaque changement et renvoie une fonction qui arrête l'écoute. getSnapshot() renvoie la valeur actuelle. React appelle getSnapshot pendant le rendu et après chaque notification, et ne refait le rendu que si la valeur a changé selon Object.is.

Pourquoi getSnapshot doit-il renvoyer une valeur mise en cache ?

React compare le résultat de chaque appel à getSnapshot au précédent. S'il renvoie un nouvel objet ou un nouveau tableau à chaque fois, React voit toujours un changement, refait le rendu, rappelle getSnapshot, et boucle jusqu'à lever "Maximum update depth exceeded". Renvoyez la même référence tant que les données ne changent pas vraiment.

Qu'est-ce que getServerSnapshot ?

Le troisième argument, facultatif. Il renvoie la valeur à utiliser pendant le rendu serveur et pendant l'hydratation dans le navigateur, pour que les deux produisent le même HTML. Sans lui, le composant lève "Missing getServerSnapshot" sur le serveur, et le contenu sous la frontière <Suspense> la plus proche est rendu dans le navigateur à la place.

Faut-il utiliser useSyncExternalStore ou useEffect avec useState ?

Pour s'abonner à des données externes, préférez useSyncExternalStore. Il lit la valeur pendant le rendu, donc le premier rendu est déjà correct et chaque composant voit la même valeur même pendant le rendu concurrent. useEffect avec useState fait un rendu avec une valeur périmée et peut afficher brièvement des valeurs différentes dans différents composants.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER