Articlepar DevBlog

useSyncExternalStore plutôt que useEffect pour lire le thème

Lire une valeur extérieure à React (attribut du DOM, localStorage) sans setState dans un effet : l'exemple du thème clair/sombre.

  • #react
  • #hooks
  • #theme

Le réflexe, et pourquoi il est discutable

Pour lire le thème courant, on écrit souvent :

tsx
const [theme, setTheme] = useState("dark");

useEffect(() => {
  setTheme(document.documentElement.dataset.theme ?? "dark");
}, []);

Cela fonctionne, mais :

  • le premier rendu affiche une valeur fausse avant que l'effet ne corrige ;
  • un rendu supplémentaire est déclenché ;
  • la règle ESLint react-hooks/set-state-in-effect le signale.

La bonne primitive

useSyncExternalStore est conçu pour lire une source extérieure à React et se resynchroniser quand elle change. Il prend trois arguments :

  1. subscribe : comment être prévenu d'un changement ;
  2. getSnapshot : la valeur actuelle côté navigateur ;
  3. getServerSnapshot : la valeur à utiliser pendant le rendu serveur.

Application au thème

Le thème est porté par l'attribut data-theme de <html>. On écoute ses changements avec un MutationObserver :

tsx
function subscribeTheme(onChange: () => void) {
  const observer = new MutationObserver(onChange);
  observer.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ["data-theme"],
  });
  return () => observer.disconnect();
}

function getTheme() {
  return document.documentElement.dataset.theme === "light" ? "light" : "dark";
}

function getServerTheme() {
  return "dark";
}

Puis, dans le composant :

tsx
const theme = useSyncExternalStore(subscribeTheme, getTheme, getServerTheme);

Éviter le flash au chargement

Un script inline, exécuté avant le premier rendu, applique le bon thème (stocké dans localStorage ou issu de prefers-color-scheme). Ainsi, quand React hydrate, l'attribut est déjà correct.

Plusieurs consommateurs, une seule source

ThemeToggle (le bouton) et MermaidDiagram (qui doit refaire son rendu quand le thème change) lisent la même source via le même hook. Aucun contexte ni état dupliqué à synchroniser.

À retenir

  • Source extérieure à React → useSyncExternalStore.
  • Pas de setState dans un effet pour recopier une valeur.
  • Fournissez toujours un snapshot serveur pour éviter les écarts d'hydratation.