Le réflexe, et pourquoi il est discutable
Pour lire le thème courant, on écrit souvent :
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-effectle 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 :
subscribe: comment être prévenu d'un changement ;getSnapshot: la valeur actuelle côté navigateur ;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 :
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 :
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
setStatedans un effet pour recopier une valeur. - Fournissez toujours un snapshot serveur pour éviter les écarts d'hydratation.