Aller au contenu

localStorage : avec Astro, React et Next.js

Dans un framework avec rendu serveur, une partie du code s’exécute dans Node.js, où localStorage n’existe pas. La règle : n’y accéder que dans le navigateur.

const theme = localStorage.getItem('theme'); // ReferenceError: localStorage is not defined (rendu serveur)

Solutions : accéder au stockage dans un événement, un effet ou un script client ; tester typeof window !== 'undefined' ; différer la lecture après le montage.

const estNavigateur = typeof window !== 'undefined';
const valeur = estNavigateur ? window.localStorage.getItem('theme') : null;

Le frontmatter --- s’exécute à la compilation ou sur le serveur : jamais de localStorage dedans. Un <script> de composant s’exécute dans le navigateur :

<button id="theme" class="btn">
Thème
</button>
<script>
function lire() {
try {
return localStorage.getItem('theme');
} catch {
return null;
}
}
document.getElementById('theme')?.addEventListener('click', () => {
const racine = document.documentElement;
const suivant = racine.dataset.theme === 'dark' ? 'light' : 'dark';
racine.dataset.theme = suivant;
try {
localStorage.setItem('theme', suivant);
} catch {}
});
</script>

Pour éviter le flash d’un mauvais thème, un petit script en ligne dans le <head> applique la préférence avant l’affichage :

<head>
<script is:inline>
(function () {
try {
var t =
localStorage.getItem('theme') ||
(matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light');
document.documentElement.dataset.theme = t;
} catch (e) {}
})();
</script>
</head>

Avec les transitions de page (ClientRouter), réappliquer l’état à l’événement astro:after-swap : le document est remplacé à chaque navigation (voir Astro : interactivité et intégrations).

document.addEventListener('astro:after-swap', () => {
try {
document.documentElement.dataset.theme =
localStorage.getItem('theme') ?? 'light';
} catch {}
});

Sans rendu serveur, l’accès est simple, mais il faut éviter de lire à chaque rendu :

import { useEffect, useState } from 'react';
export function useStockageLocal<T>(cle: string, valeurInitiale: T) {
const [valeur, setValeur] = useState<T>(() => {
try {
const brut = localStorage.getItem(cle);
return brut === null ? valeurInitiale : (JSON.parse(brut) as T);
} catch {
return valeurInitiale;
}
});
useEffect(() => {
try {
localStorage.setItem(cle, JSON.stringify(valeur));
} catch {}
}, [cle, valeur]);
return [valeur, setValeur] as const;
}
function Preferences() {
const [taille, setTaille] = useStockageLocal('taillePolice', 16);
return (
<input
type="range"
min={12}
max={24}
value={taille}
onChange={(e) => setTaille(Number(e.target.value))}
/>
);
}

La fonction passée à useState n’est exécutée qu’une fois, au premier rendu : elle lit le stockage sans le relire à chaque rendu. Voir React : état et effets.

Synchronisation entre onglets : useSyncExternalStore

Section intitulée « Synchronisation entre onglets : useSyncExternalStore »
import { useSyncExternalStore } from 'react';
function souscrire(rappel: () => void) {
window.addEventListener('storage', rappel);
return () => window.removeEventListener('storage', rappel);
}
export function useTheme() {
return useSyncExternalStore(
souscrire,
() => localStorage.getItem('theme') ?? 'light', // valeur du navigateur
() => 'light', // valeur du rendu serveur
);
}

useSyncExternalStore lit le stockage de façon cohérente et se met à jour quand un autre onglet modifie la valeur (voir Synchronisation entre onglets). La troisième fonction fournit la valeur utilisée pendant le rendu serveur.

Les composants sont des composants serveur par défaut : pas de localStorage. Il faut un composant client et différer la lecture après l’hydratation, sous peine d’erreur d’hydratation (« Hydration failed ») : le HTML serveur ne contient pas la valeur du navigateur.

'use client';
import { useEffect, useState } from 'react';
export function Salutation() {
const [nom, setNom] = useState<string | null>(null);
useEffect(() => {
try {
setNom(localStorage.getItem('nom'));
} catch {}
}, []);
return <p>{nom ? `Bonjour ${nom}` : 'Bonjour'}</p>;
}

Au premier rendu (serveur et navigateur), nom vaut null : les deux HTML sont identiques ; l’effet charge ensuite la valeur. useSyncExternalStore (avec getServerSnapshot) évite cette double passe.

Pour le thème, poser l’attribut avant l’affichage avec un script dans le <head> et ajouter suppressHydrationWarning sur <html> (voir Next.js : styles et assets).

<html lang="fr" suppressHydrationWarning>

Quand la donnée doit être connue du serveur (affichage personnalisé dès le premier HTML), utiliser un cookie (cookies() de next/headers) et non localStorage.

Technique Effet
Script en ligne dans le <head> (thème) Applique la préférence avant le premier affichage
Valeur par défaut neutre au premier rendu Pas d’erreur d’hydratation, mise à jour juste après
Cookie lu côté serveur Le serveur rend directement la bonne valeur
Masquer brièvement la partie dépendante Évite un clignotement (à utiliser avec parcimonie)
  • Désactiver localStorage (paramètres du navigateur : bloquer les données de sites) : l’application doit continuer à fonctionner avec les valeurs par défaut.
  • Tester en navigation privée et avec le stockage plein.
  • Ouvrir deux onglets pour vérifier la synchronisation.
  • Pour les tests automatiques avec Node.js : simuler localStorage (objet avec les mêmes méthodes) ou utiliser un environnement navigateur (jsdom).
Symptôme Piste
localStorage is not defined Accès pendant le rendu serveur : déplacer dans un effet, un événement ou un script client
Hydration failed Valeur du navigateur lue au premier rendu : lire dans useEffect, ou useSyncExternalStore
Flash du mauvais thème Script d’initialisation en ligne dans le <head>
Le thème se réinitialise après une navigation Astro Réappliquer à astro:after-swap
La valeur diffère entre localhost:3000 et 127.0.0.1:3000 Origines différentes : stockages séparés
Écriture inopérante dans un iframe Politique de sécurité de l’iframe (sandbox), blocage des données tierces

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune.