Aller au contenu

localStorage : JSON et structure des données

localStorage ne conserve que des chaînes. Pour des objets, des tableaux, des nombres, des booléens ou des dates, on les sérialise en JSON.

const preferences = { theme: 'dark', langue: 'fr', taillePolice: 16 };
localStorage.setItem('preferences', JSON.stringify(preferences));
const lues = JSON.parse(localStorage.getItem('preferences') ?? 'null');
console.log(lues?.theme); // 'dark'

Sans JSON.stringify, un objet est stocké sous la forme "[object Object]" : toute la donnée est perdue.

JSON.parse lève une exception si le texte est invalide (donnée corrompue ou modifiée à la main) : l’encadrer d’un try/catch.

src/lib/stockage.js
const PREFIXE = 'monapp:';
export function lire(cle, defaut = null) {
try {
const brut = localStorage.getItem(PREFIXE + cle);
return brut === null ? defaut : JSON.parse(brut);
} catch {
return defaut;
}
}
export function ecrire(cle, valeur) {
try {
localStorage.setItem(PREFIXE + cle, JSON.stringify(valeur));
return true;
} catch {
return false;
}
}
export function supprimer(cle) {
try {
localStorage.removeItem(PREFIXE + cle);
} catch {
/* ignoré */
}
}

Le préfixe (monapp:) évite les collisions avec d’autres scripts de la même origine (bibliothèques, autres applications du même domaine) et permet de supprimer uniquement les clés de l’application :

export function toutEffacer() {
const aSupprimer = [];
for (let i = 0; i < localStorage.length; i++) {
const cle = localStorage.key(i);
if (cle?.startsWith(PREFIXE)) aSupprimer.push(cle);
}
aSupprimer.forEach((cle) => localStorage.removeItem(cle));
}

Collecter les clés d’abord, puis les supprimer : modifier le stockage pendant la boucle décale les index.

type Preferences = { theme: 'light' | 'dark'; langue: string };
const PAR_DEFAUT: Preferences = { theme: 'light', langue: 'fr' };
export function lirePreferences(): Preferences {
const lues = lire<Partial<Preferences>>('preferences', {});
return { ...PAR_DEFAUT, ...lues };
}

JSON.parse renvoie une donnée non vérifiée : fusionner avec des valeurs par défaut, ou valider avec une bibliothèque de schémas (Zod) avant de s’en servir.

Type Problème Solution
Booléen Revient en "true" / "false" JSON (JSON.parse('false') vaut false)
Nombre Revient en chaîne Number(...) ou JSON
Date JSON la transforme en chaîne ISO new Date(chaine) à la lecture
undefined, fonctions Perdus par JSON Ne pas les stocker
Map, Set Perdus par JSON [...ensemble], Object.fromEntries(map)
Grands entiers (BigInt) Non pris en charge par JSON Les stocker en chaîne
Références circulaires JSON.stringify échoue Aplatir la donnée
const rendezVous = { titre: 'Réunion', le: new Date().toISOString() };
ecrire('rdv', rendezVous);
const lu = lire('rdv');
const quand = new Date(lu.le);
Approche Exemple Quand
Une clé par réglage monapp:theme, monapp:langue Peu de valeurs indépendantes
Une clé pour un objet monapp:preferences Réglages lus et écrits ensemble
Une clé par élément monapp:note:42 Peu d’éléments (pas de liste interrogeable)

Éviter de stocker de longues listes dans une seule clé : chaque modification réécrit toute la chaîne. Au-delà de quelques centaines d’éléments ou d’une centaine de Ko, utiliser IndexedDB.

Le code évolue, les données stockées restent. Prévoir un numéro de version :

const VERSION = 2;
export function chargerPreferences() {
const brut = lire('preferences', null);
if (!brut) return { version: VERSION, theme: 'light', langue: 'fr' };
return migrer(brut);
}
function migrer(donnees) {
let d = donnees;
if (!d.version || d.version < 2) {
// version 1 : { sombre: boolean } -> version 2 : { theme: 'light' | 'dark' }
d = {
version: 2,
theme: d.sombre ? 'dark' : 'light',
langue: d.langue ?? 'fr',
};
}
return d;
}

Écrire la fonction de migration avant de changer le format : un utilisateur qui revient après plusieurs versions doit pouvoir passer de la version 1 à la version courante.

localStorage n’expire jamais. Pour une durée de validité :

export function ecrireAvecExpiration(cle, valeur, dureeMs) {
ecrire(cle, { valeur, expireLe: Date.now() + dureeMs });
}
export function lireAvecExpiration(cle, defaut = null) {
const entree = lire(cle, null);
if (!entree || Date.now() > entree.expireLe) {
supprimer(cle);
return defaut;
}
return entree.valeur;
}

Un cache peut ainsi être valable une heure (3_600_000 ms). L’expiration est vérifiée à la lecture : les entrées jamais relues restent stockées.

function tailleUtilisee() {
let octets = 0;
for (let i = 0; i < localStorage.length; i++) {
const cle = localStorage.key(i);
octets += (cle.length + (localStorage.getItem(cle) ?? '').length) * 2; // UTF-16 : 2 octets par caractère
}
return octets;
}
console.log((tailleUtilisee() / 1024).toFixed(1), 'Ko');

Les navigateurs comptent en général deux octets par caractère : 5 Mo représentent environ 2,5 millions de caractères.

Symptôme Piste
La valeur lue est "[object Object]" JSON.stringify oublié à l’écriture
SyntaxError: Unexpected token … in JSON Valeur non JSON (ancien format, texte brut) : try/catch et migration
Une date devient une chaîne JSON : reconstruire avec new Date(...)
Les anciennes données cassent la nouvelle version Numéro de version et migration
QuotaExceededError en écrivant une grosse liste Passer à IndexedDB
Une boucle de suppression saute des clés Collecter les clés avant de supprimer

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

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

Aucune.