W
Web Dev Simplified
#React#Hooks#useSyncExternalStore

useSyncExternalStore : Synchronisation d'État Externe dans React

Découvrez useSyncExternalStore, le hook React sous-estimé pour synchroniser l'état de React avec des sources externes comme le DOM ou des stores globaux, simplifiant le code et améliorant les performances.

5 min de lectureGuide IA

Introduction

Introduction
useSyncExternalStore est un hook React qui permet à vos composants de s'abonner à une source de données externe et de lire sa valeur. Il est particulièrement utile pour synchroniser l'état de React avec des APIs de navigateur, des éléments DOM ou des stores globaux, offrant une alternative plus propre et plus performante à l'utilisation excessive de useEffect pour ces cas.

Précis de configuration

Élément Version / Lien
Langage / Runtime JavaScript / TypeScript, Node.js
Librairie principale React (version 18 ou supérieure)
APIs requises APIs de navigateur (ex: navigator.online, window.addEventListener), Élément HTML dialog
Clés / credentials nécessaires Aucune

Guide étape par étape

Guide étape par étape

Étape 1 — Suivi du statut en ligne du navigateur

Pourquoi : La propriété navigator.online est une source d'état externe gérée par le navigateur. La synchroniser avec l'état de React via useState et useEffect peut entraîner des désynchronisations si l'état est mis à jour manuellement ailleurs ou si le nettoyage n'est pas géré correctement. useSyncExternalStore garantit que l'état de React est toujours à jour avec la valeur réelle du navigateur.

import { useSyncExternalStore, useState, useEffect } from "react";

// Fonction pour s'abonner aux changements du statut en ligne
function subscribe(callback: () => void) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  // Retourne une fonction de nettoyage pour désabonner les écouteurs d'événements
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

// Composant principal
export default function App() {
  // Utilise useSyncExternalStore pour synchroniser l'état 'isOnline'
  // Le premier argument est la fonction 'subscribe'
  // Le second argument est la fonction 'getSnapshot' qui retourne la valeur actuelle de l'état externe
  const isOnline = useSyncExternalStore(subscribe, () => navigator.online);

  return (
    <h1 style={{ color: isOnline ? undefined : "red" }}>
      {isOnline ? "Online" : "Offline"}
    </h1>
  );
}

Étape 2 — Synchronisation de l'état d'un élément HTML dialog

Pourquoi : L'élément HTML dialog a son propre état d'ouverture/fermeture, qui peut être modifié par des interactions utilisateur (comme la touche Échap) que React ne gère pas nativement. Tenter de synchroniser cela avec useState peut entraîner un état désynchronisé. useSyncExternalStore permet à React de réagir aux événements natifs du dialog.

import { useSyncExternalStore, useRef } from "react";

export default function App() {
  // Référence à l'élément HTMLDialogElement
  const modalRef = useRef<HTMLDialogElement>(null);

  // Fonction pour s'abonner à l'événement 'toggle' du dialog
  function subscribe(cb: () => void) {
    // [Note de l'éditeur : modalRef.current doit exister avant d'ajouter l'écouteur]
    modalRef.current?.addEventListener("toggle", cb);
    return () => {
      modalRef.current?.removeEventListener("toggle", cb);
    };
  }

  // Fonction pour obtenir le snapshot de l'état 'open' du dialog
  // Le ?? false gère le cas où modalRef.current est null au premier rendu
  const getSnapshot = () => modalRef.current?.open ?? false;

  // Fonction pour le snapshot côté serveur (optionnel, pour SSR)
  // Par défaut, le dialog est considéré comme fermé sur le serveur
  const getServerSnapshot = () => false;

  // Utilise useSyncExternalStore pour synchroniser l'état 'isOpen'
  const isOpen = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

  return (
    <>
      <button onClick={() => {
        // [Note de l'éditeur : modalRef.current doit exister avant d'appeler showModal]
        modalRef.current?.showModal();
      }}>
        Open Modal
      </button>
      <p>{isOpen ? "Opened" : "Closed"}</p>

      <dialog ref={modalRef}>
        <button onClick={() => {
          // [Note de l'éditeur : modalRef.current doit exister avant d'appeler close]
          modalRef.current?.close();
        }}>
          Close
        </button>
      </dialog>
    </>
  );
}

Étape 3 — Création d'un store global personnalisé (liste de tâches)

Pourquoi : Pour gérer un état global (comme une liste de tâches) à travers plusieurs composants sans avoir recours au prop drilling ou à des solutions complexes comme le Context API ou Redux, tout en maintenant de bonnes performances. useSyncExternalStore permet de créer un store simple et réactif.

Fichier todoStore.ts :

// todoStore.ts
let todos: string[] = []; // L'état externe réel
const listeners = new Set<() => void>(); // Ensemble de callbacks pour notifier les composants React

export const TodoStore = {
  // Retourne un snapshot de l'état actuel des tâches
  getTodos(): string[] {
    return todos;
  },
  // Ajoute une tâche et notifie les écouteurs
  addTodo(name: string) {
    todos = [...todos, name]; // Met à jour l'état de manière immuable
    listeners.forEach(cb => cb()); // Appelle tous les écouteurs pour forcer un re-rendu
  },
  // Supprime une tâche et notifie les écouteurs
  removeTodo(index: number) {
    todos = todos.toSpliced(index, 1); // Met à jour l'état de manière immuable
    listeners.forEach(cb => cb()); // Appelle tous les écouteurs
  },
  // S'abonne aux changements du store
  subscribe(cb: () => void) {
    listeners.add(cb);
    // Retourne une fonction de nettoyage pour désabonner le callback
    return () => {
      listeners.delete(cb);
    };
  }
};

Fichier App.tsx :

import { useSyncExternalStore, useRef } from "react";
import { TodoStore } from "./todoStore"; // Importe le store personnalisé

export default function App() {
  // Utilise useSyncExternalStore pour obtenir l'état des tâches depuis le store global
  const todos = useSyncExternalStore(
    TodoStore.subscribe, // Fonction pour s'abonner
    TodoStore.getTodos,  // Fonction pour obtenir le snapshot actuel
    () => []             // Fonction pour le snapshot côté serveur (état initial vide)
  );
  const nameRef = useRef<HTMLInputElement>(null);

  // Gère la soumission du formulaire pour ajouter une tâche
  function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (nameRef.current?.value) {
      TodoStore.addTodo(nameRef.current.value);
      nameRef.current.value = ""; // Efface l'entrée après ajout
    }
  }

  // Gère la suppression d'une tâche
  function removeTodo(index: number) {
    TodoStore.removeTodo(index);
  }

  return (
    <form onSubmit={onSubmit}>
      <input ref={nameRef} />
      <ul>
        {todos.map((todo, index) => (
          <li key={index} onClick={() => removeTodo(index)}>
            {todo}
          </li>
        ))}
      </ul>
    </form>
  );
}

Tableaux comparatifs

Tableaux comparatifs

Caractéristique useSyncExternalStore useEffect
Cas d'usage principal Synchroniser l'état de React avec des sources de données externes (DOM, APIs navigateur, stores globaux). Exécuter des effets secondaires après le rendu (abonnements, requêtes de données, manipulation directe du DOM).
Gestion de la synchronisation Gère automatiquement la souscription et la désouscription, garantissant que l'état de React est toujours à jour avec la source externe. Nécessite une gestion manuelle des abonnements et des nettoyages, sujette aux erreurs de synchronisation et aux fuites de mémoire.
Performance Optimisé pour les mises à jour fréquentes et la prévention des re-rendus inutiles, car il ne re-rend que les composants qui utilisent l'état mis à jour. Peut entraîner des re-rendus excessifs si les dépendances ne sont pas gérées correctement ou si les effets sont coûteux.
Complexité du code Simplifie le code pour la synchronisation d'état externe en encapsulant la logique de souscription et de snapshot. Peut devenir complexe avec des logiques de synchronisation et de nettoyage élaborées, nécessitant souvent des AbortController ou des fonctions de nettoyage.
Server-Side Rendering (SSR) Prend en charge un getServerSnapshot optionnel pour fournir un état initial sur le serveur, évitant les problèmes d'hydratation. Ne s'exécute pas sur le serveur, ce qui peut entraîner des décalages d'état entre le rendu initial du serveur et l'hydratation du client.
Immutabilité Permet de muter directement l'état externe (si la source le permet) sans enfreindre les règles de React, car React ne gère pas directement l'état mais le lit via getSnapshot. Nécessite généralement des mises à jour d'état immuables pour éviter les problèmes de re-rendu et de détection des changements.

⚠️ Erreurs fréquentes et pièges

  1. Désynchronisation de l'état React avec la source externe : L'état de React peut ne pas refléter la source externe si celle-ci est modifiée par des moyens non contrôlés par React (ex: un élément DOM natif). useSyncExternalStore résout ce problème en écoutant les événements de la source externe et en mettant à jour l'état de React en conséquence.
  2. Oubli du nettoyage des abonnements dans useEffect : Dans useEffect, il est facile d'oublier de retourner une fonction de nettoyage pour désabonner les écouteurs d'événements, ce qui peut entraîner des fuites de mémoire. useSyncExternalStore force la définition d'une fonction de nettoyage dans son argument subscribe.
  3. Re-rendus excessifs avec useEffect : Pour les sources d'état qui changent fréquemment, useEffect peut déclencher de nombreux re-rendus. useSyncExternalStore est optimisé pour ces scénarios, ne re-rendant que les composants qui utilisent l'état mis à jour.
  4. Problèmes d'hydratation en Server-Side Rendering (SSR) : useEffect ne s'exécute pas sur le serveur, ce qui peut entraîner un décalage entre le rendu initial du serveur et l'état du client. useSyncExternalStore permet de spécifier un getServerSnapshot pour fournir un état initial cohérent sur le serveur.

Glossaire

useSyncExternalStore : Un hook React qui permet aux composants de s'abonner à une source de données externe et de lire sa valeur, garantissant que l'état de React reste synchronisé avec la source.
subscribe (fonction) : Une fonction passée à useSyncExternalStore qui prend un callback et renvoie une fonction de nettoyage, utilisée pour s'abonner aux changements de la source de données externe.
getSnapshot (fonction) : Une fonction passée à useSyncExternalStore qui renvoie la valeur actuelle de la source de données externe, permettant à React de lire l'état synchronisé.

Points clés à retenir

  • useSyncExternalStore est le hook de choix pour synchroniser l'état de React avec des sources externes, qu'il s'agisse d'APIs de navigateur, d'éléments DOM ou de stores globaux personnalisés.
  • Il simplifie considérablement la gestion des abonnements et des nettoyages par rapport à l'utilisation manuelle de useEffect.
  • Il garantit une synchronisation fiable de l'état, même lorsque la source externe est modifiée en dehors du contrôle direct de React.
  • Ce hook est optimisé pour la performance, réduisant les re-rendus inutiles pour les états qui changent fréquemment.
  • Il offre un support natif pour le Server-Side Rendering (SSR) via la fonction getServerSnapshot.
  • Il permet de créer des solutions de gestion d'état global légères et efficaces sans la complexité des bibliothèques tierces ou du Context API de React.

Ressources