Cours JavaScript et TypeScript · Module 5: JavaScript avancé

Web Storage - localStorage et sessionStorage

14 min de lecture
Dans cette leçon8

Dennis Nedry est assis dans la salle de contrôle de Jurassic Park et, pour la vingtième fois de la journée, il rebascule la carte du parc en vue satellite. « Je n'ai pas l'intention de recliquer la même chose à chaque rechargement de page », grommelle-t-il dans sa barbe. Et il a raison : toute information ne mérite pas un aller-retour jusqu'au serveur. La vue de carte préférée, le thème de couleurs, le dernier secteur consulté, le brouillon d'un formulaire de signalement d'incident - tout cela peut parfaitement rester dans le navigateur du visiteur. JavaScript te donne pour cela deux mécanismes intégrés : localStorage et sessionStorage. Tous deux conservent des paires clé-valeur du côté client, tous deux exposent exactement la même API et tous deux fonctionnent sans une seule ligne de code serveur. Au fond, ils ne diffèrent que sur un point : la durée pendant laquelle ils se souviennent.

localStorage face à sessionStorage

CaractéristiquelocalStoragesessionStorage
PersistanceSurvit à la fermeture et à la réouverture du navigateurEffacé au moment où l'onglet ou la fenêtre se ferme
PortéePartagé entre tous les onglets et fenêtres de la même origineIsolé dans l'onglet courant
Limite de capacité~5-10 Mo par origine~5-10 Mo par onglet
Cas d'usagePréférences et réglages de l'utilisateurDonnées de session temporaires, état de formulaire

Une phrase mérite d'être retenue pour de bon : les données de localStorage persistent en permanence tant que personne ne les supprime volontairement, tandis que celles de sessionStorage disparaissent à la fermeture de l'onglet. C'est là leur seule véritable différence, et il n'y en a pas d'autre.

Profitons-en pour redresser trois malentendus, car ils circulent avec une obstination remarquable. Premièrement, sessionStorage n'offre pas une plus grande capacité de stockage : les deux espaces accueillent à peu près la même chose, soit environ 5 à 10 Mo. Deuxièmement, les deux magasins ne conservent que des chaînes de caractères ; aucun des deux ne sait enregistrer un objet, si bien qu'une affirmation du genre « localStorage ne prend que du texte, sessionStorage prend des objets » est tout simplement fausse. Troisièmement, les deux fonctionnent dans tous les navigateurs modernes : localStorage n'est pas une invention réservée à Chrome, et sessionStorage n'en est pas la variante plus universelle. Tu choisis entre eux uniquement selon que les données doivent survivre ou non à la fermeture de l'onglet.

Les opérations de base

Toute l'API tient dans cinq noms : setItem, getItem, removeItem, clear et key, auxquels s'ajoute la propriété length. Ce même quintette pilote les deux magasins, si bien que tu l'apprends une fois et l'affaire est réglée. setItem reçoit une clé et une valeur, getItem renvoie la valeur enregistrée sous la clé - ou null si cette clé n'a jamais existé. C'est bien null, pas undefined, et un nombre étonnant de conditions trébuchent sur ce détail. removeItem efface une entrée précise, clear balaie tout ce qui appartient à l'origine courante, et key(i) te laisse parcourir le magasin index par index quand tu veux voir ce qui s'y trouve réellement.

1// setItem - enregistre une donnee
2localStorage.setItem('parkTheme', 'dark');
3localStorage.setItem('language', 'en');
4sessionStorage.setItem('currentSector', 'B-4');
5
6// getItem - lit une donnee
7const theme = localStorage.getItem('parkTheme'); // "dark"
8const missing = localStorage.getItem('nothere'); // null
9
10// removeItem - supprime une seule cle
11localStorage.removeItem('parkTheme');
12
13// clear - supprime toutes les donnees
14localStorage.clear();
15
16// length et key() - parcours du magasin
17console.log(localStorage.length); // nombre de cles dans le magasin
18for (let i = 0; i < localStorage.length; i++) {
19  const key = localStorage.key(i);
20  console.log(key, localStorage.getItem(key));
21}

Regarde bien la troisième ligne. sessionStorage s'utilise exactement comme localStorage : seul le nom de l'objet placé avant le point change. Le numéro de secteur enregistré dans sessionStorage s'évanouira avec l'onglet, ce qui en fait l'endroit idéal pour des données de travail que personne n'a envie de retrouver le lendemain matin. clear() est en revanche nettement plus traître : il efface absolument tout ce que ton origine a un jour enregistré, y compris les entrées écrites par des parties complètement différentes de l'application. Dans un vrai projet, il vaut mieux supprimer les clés une par une, par leur nom, ou les regrouper sous un préfixe commun et ne nettoyer que ce préfixe. La boucle sur key() sert précisément à cette seconde approche : elle te livre l'inventaire complet du magasin, et c'est à toi de décider ce qui tombe.

Stocker des données complexes avec JSON

Le magasin n'accepte que des chaînes de caractères et il n'a aucune intention d'en discuter. Si tu lui passes un objet, il ne signalera aucune erreur : il le convertira silencieusement en texte, avec la même conversion que celle que tu connais en collant un objet à une chaîne. Le résultat est toujours identique et toujours inutilisable : le magasin reçoit la chaîne [object Object] et toutes les propriétés disparaissent sans retour possible. C'est une de ces méprises qui ne font pas tomber l'application sur le coup, mais qui attendent patiemment le moment où quelqu'un essaiera de relire les données enregistrées.

1// ATTENTION - cet objet sera converti en "[object Object]"
2const dino = { name: 'Rex', species: 'T-Rex' };
3localStorage.setItem('dino', dino);
4console.log(localStorage.getItem('dino')); // "[object Object]"

La solution s'appelle JSON, un format textuel capable de décrire fidèlement un objet ou un tableau. JSON.stringify transforme la structure en chaîne avant l'enregistrement, JSON.parse la reconstitue à la lecture. Ce duo voyage toujours ensemble, et c'est précisément lui qui te permet de garder dans localStorage tout un jeu de préférences du visiteur, la liste des derniers incidents ou un objet imbriqué de notifications concernant les repas et les alertes de sécurité. L'imbrication ne pose ici aucun problème : JSON gère un objet dans un objet aussi bien qu'une liste plate de clés. La seule chose à garder en tête, c'est que JSON ignore les fonctions, les dates et undefined : une date revient du magasin sous forme de chaîne ordinaire et doit repasser manuellement par new Date(), une fonction s'évapore en chemin sans le moindre avertissement, et une propriété fixée à undefined n'apparaît tout simplement pas dans le texte enregistré. Pour de simples réglages d'utilisateur, aucune de ces limites ne gêne.

1// Enregistrer un objet
2const userPrefs = {
3  theme: 'dark',
4  fontSize: 16,
5  language: 'en',
6  notifications: { security: true, feeding: true }
7};
8
9localStorage.setItem('userPreferences', JSON.stringify(userPrefs));
10
11// Relecture
12const storedPrefs = JSON.parse(localStorage.getItem('userPreferences'));
13console.log(storedPrefs.theme); // "dark"
14
15// Enregistrer un tableau
16const recentIncidents = ['INC-001', 'INC-002', 'INC-003'];
17localStorage.setItem('recentIncidents', JSON.stringify(recentIncidents));
18
19const incidents = JSON.parse(localStorage.getItem('recentIncidents') || '[]');

L'expression qui enregistre l'objet mérite d'être décomposée en quatre morceaux, car c'est exactement dans cet ordre que tu la reconstruiras : d'abord l'objet magasin localStorage, ensuite l'appel .setItem(, puis la clé accompagnée de l'ouverture de la conversion 'key', JSON.stringify(, et tout à la fin l'objet suivi de ses deux parenthèses fermantes object)). Ces deux parenthèses finales ne sont pas une coquille : la première referme JSON.stringify, la seconde setItem. La lecture emprunte le même chemin en sens inverse : JSON.parse enveloppe localStorage.getItem('key') et te rend un objet prêt à l'emploi. La dernière ligne montre encore une habitude à adopter sans hésiter : || '[]' substitue un tableau vide lorsque la clé est absente, ce qui garantit que JSON.parse ne reçoit jamais null.

Lecture et écriture sûres

Dans la salle de contrôle du parc, rien ne fonctionne jamais parfaitement, et le magasin du navigateur ne fait pas exception. JSON.parse explose avec une exception si la clé abrite un texte que personne n'a écrit via stringify - et il suffit pour cela qu'un utilisateur ait farfouillé à la main dans les outils de développement, ou qu'une ancienne version de l'application ait enregistré ses données dans un autre format. setItem sait lui aussi lever une erreur : en navigation privée sur Safari, l'écriture est parfois purement et simplement bloquée, et un dépassement de la limite de capacité te vaudra une exception de débordement. C'est pourquoi, dans du code de production, on emballe ces deux appels dans un try...catch et on renvoie une valeur de repli sensée plutôt que de laisser l'application s'effondrer.

1// Lecture resistante a une cle absente ou a un JSON corrompu
2function getStoredData(key, defaultValue = null) {
3  try {
4    const item = localStorage.getItem(key);
5    return item ? JSON.parse(item) : defaultValue;
6  } catch (error) {
7    console.error(`Analyse JSON impossible pour la cle ${key}:`, error);
8    return defaultValue;
9  }
10}
11
12const settings = getStoredData('userSettings', { theme: 'light', language: 'fr' });
13
14// Ecriture sure - navigation privee Safari, magasin sature
15function safeSetItem(key, value) {
16  try {
17    localStorage.setItem(key, value);
18    return true;
19  } catch (error) {
20    console.error('Ecriture impossible dans localStorage:', error);
21    return false;
22  }
23}

Les deux fonctions partagent la même philosophie : une panne du magasin ne doit jamais arrêter l'application. getStoredData renvoie toujours quelque chose - soit les vraies données, soit la valeur par défaut que tu as toi-même fournie en second argument. safeSetItem renvoie true ou false, de sorte que le code appelant garde la main : à lui de décider s'il affiche un message ou s'il fait simplement comme si de rien n'était. Tu reconnaîtras au passage exactement le schéma que l'on applique au travail en réseau : on part du principe que l'opération peut échouer et on garde un plan B sous la main. Jurassic Park a appris à tout le monde que les systèmes dépourvus de plan B finissent mal.

La classe UserPreferences

Des appels à setItem et getItem éparpillés dans tout le code deviennent vite un problème : personne ne se rappelle sous quelle clé se cache quoi, et les valeurs par défaut se dupliquent à cinq endroits. La réponse tient dans une seule classe, qui garde l'ensemble des préférences sous une unique clé du magasin et veille elle-même sur les valeurs par défaut. Le reste de l'application ne voit alors que get et set ; le fait qu'un JSON.stringify et un JSON.parse tournent en dessous cesse complètement de la concerner. Observe attentivement la méthode get : elle vérifie la présence de la clé avec l'opérateur in et non avec un simple test de véracité, ce qui évite qu'un false enregistré ne soit confondu avec un réglage absent.

1class UserPreferences {
2  constructor() {
3    this.storageKey = 'jurassicParkPrefs';
4    this.defaults = {
5      theme: 'light',
6      language: 'en',
7      alertSound: true,
8      mapZoom: 2,
9      favoriteSection: 'overview'
10    };
11  }
12
13  get(key) {
14    const stored = localStorage.getItem(this.storageKey);
15    const prefs = stored ? JSON.parse(stored) : {};
16    return key in prefs ? prefs[key] : this.defaults[key];
17  }
18
19  set(key, value) {
20    const stored = localStorage.getItem(this.storageKey);
21    const prefs = stored ? JSON.parse(stored) : {};
22    prefs[key] = value;
23    localStorage.setItem(this.storageKey, JSON.stringify(prefs));
24  }
25
26  setMultiple(updates) {
27    const stored = localStorage.getItem(this.storageKey);
28    const prefs = stored ? JSON.parse(stored) : {};
29    Object.assign(prefs, updates);
30    localStorage.setItem(this.storageKey, JSON.stringify(prefs));
31  }
32
33  reset() {
34    localStorage.removeItem(this.storageKey);
35  }
36
37  getAll() {
38    const stored = localStorage.getItem(this.storageKey);
39    const prefs = stored ? JSON.parse(stored) : {};
40    return { ...this.defaults, ...prefs };
41  }
42}
43
44const prefs = new UserPreferences();
45prefs.set('theme', 'dark');
46prefs.set('alertSound', false);
47console.log(prefs.get('theme')); // "dark"
48console.log(prefs.getAll()); // Toutes les preferences, valeurs par defaut comprises

getAll révèle encore une astuce : déballer this.defaults avant les préférences enregistrées garantit que l'utilisateur reçoit toujours un jeu complet de réglages, même si le magasin n'en contient qu'un seul. Le nom des méthodes reste évidemment ta décision. Dans l'exercice pratique, tu construiras un PreferencesManager chargé d'enregistrer et de lire les préférences de l'utilisateur dans Jurassic Park, et ses méthodes s'appellent save(), load(), remove() et clear(). Le patron reste rigoureusement identique : save() prend un objet de préférences, le fait passer par JSON.stringify et le confie à setItem ; load() parcourt le même trajet en sens inverse via getItem et JSON.parse ; remove() efface une clé unique et clear() fait le ménage sur le jeu tout entier.

DataCache avec TTL (Time To Live)

Les préférences ne sont pas les seules données à mériter d'être mémorisées. La fiche d'un dinosaure récupérée depuis une API ne change pas toutes les secondes, si bien qu'interroger le serveur à chaque ouverture du même profil revient à gaspiller du temps et de la bande passante. La solution porte le nom de cache avec TTL, c'est-à-dire avec une durée de vie attachée à chaque entrée. On enregistre la valeur accompagnée d'un marqueur expiresAt calculé comme Date.now() + ttlMs, et à la lecture on le compare à l'heure courante. Une entrée périmée est supprimée sur place et l'on renvoie null, exactement comme si elle n'avait jamais existé. La classe ci-dessous reçoit le magasin dans son constructeur : le même code servira donc aussi bien localStorage que sessionStorage, il suffit de lui passer un autre objet.

1class DataCache {
2  constructor(storage = localStorage, defaultTtlMs = 5 * 60 * 1000) {
3    this.storage = storage;
4    this.defaultTtlMs = defaultTtlMs;
5  }
6
7  set(key, value, ttlMs = this.defaultTtlMs) {
8    const item = {
9      value,
10      expiresAt: Date.now() + ttlMs
11    };
12    this.storage.setItem(`cache:${key}`, JSON.stringify(item));
13  }
14
15  get(key) {
16    const stored = this.storage.getItem(`cache:${key}`);
17    if (!stored) return null;
18
19    const item = JSON.parse(stored);
20
21    if (Date.now() > item.expiresAt) {
22      this.storage.removeItem(`cache:${key}`);
23      return null; // Entree perimee
24    }
25
26    return item.value;
27  }
28
29  getOrFetch(key, fetchFn, ttlMs) {
30    const cached = this.get(key);
31    if (cached !== null) return Promise.resolve(cached);
32
33    return fetchFn().then(value => {
34      this.set(key, value, ttlMs);
35      return value;
36    });
37  }
38
39  clear(prefix) {
40    const keysToRemove = [];
41    for (let i = 0; i < this.storage.length; i++) {
42      const key = this.storage.key(i);
43      if (key && key.startsWith(`cache:${prefix || ''}`)) {
44        keysToRemove.push(key);
45      }
46    }
47    keysToRemove.forEach(key => this.storage.removeItem(key));
48  }
49}
50
51// En pratique
52const cache = new DataCache(localStorage, 10 * 60 * 1000); // TTL de 10 minutes
53
54async function getDinosaurData(id) {
55  return cache.getOrFetch(
56    `dino:${id}`,
57    () => fetch(`/api/dinosaurs/${id}`).then(r => r.json()),
58    5 * 60 * 1000  // 5 minutes
59  );
60}

La méthode la plus intéressante est ici getOrFetch, car elle enferme tout le schéma dans un unique appel : consulter le cache et, s'il est vide, récupérer, enregistrer, renvoyer. Le code appelant n'a pas besoin de savoir d'où viennent les données. Remarque également à quoi sert le préfixe cache: accolé à chaque clé. Grâce à lui, la méthode clear ne nettoie que les entrées du cache, sans toucher aux préférences de l'utilisateur rangées juste à côté : c'est exactement l'alternative plus sûre au localStorage.clear() global dont il était question plus haut. Note enfin que les clés à supprimer sont d'abord rassemblées dans un tableau et effacées seulement ensuite. Supprimer en pleine itération sur length décalerait les index à chaque tour, et une partie des entrées serait oubliée au passage.

Avertissements de sécurité

IMPORTANT - ne conserve jamais de données sensibles dans le stockage Web :

  • les mots de passe et leurs empreintes,
  • les jetons d'authentification (utilise plutôt des cookies httpOnly),
  • les numéros de carte bancaire,
  • les données personnelles (RGPD, CCPA),
  • les jetons de session.

La raison est simple et sans appel : le stockage Web est accessible à n'importe quel JavaScript exécuté sur la page. Il n'y a là ni drapeau de protection, ni chiffrement, ni contrôle d'accès. Un seul script injecté par une attaque XSS - le tien ou celui qu'une bibliothèque tierce a traîné avec elle - suffit pour que tout le contenu du magasin parte vers le serveur de l'attaquant en une seule ligne. Dennis Nedry a démontré dans le parc que le point faible d'un système n'est jamais la clôture, mais la petite porte laissée ouverte par quelqu'un qui était pressé. Le thème de couleurs, la dernière page visitée ou l'état d'un formulaire peuvent y reposer tranquillement. Le jeton d'accès au système des clôtures, jamais.

Résumé

Le stockage Web est un magasin côté client simple et pratique :

  1. localStorage - les données persistent en permanence et sont partagées entre tous les onglets de la même origine
  2. sessionStorage - les données appartiennent à un seul onglet et disparaissent à sa fermeture
  3. JSON.stringify et JSON.parse - obligatoires pour enregistrer objets et tableaux, car le magasin ne connaît que les chaînes
  4. UserPreferences - le patron d'une classe qui garde tout le jeu de réglages sous une clé unique et veille sur les valeurs par défaut
  5. DataCache avec TTL - un cache des réponses d'API qui efface lui-même les entrées périmées et évite les requêtes inutiles
  6. try...catch - l'écriture comme la lecture peuvent échouer (navigation privée, magasin saturé, JSON corrompu), garde donc toujours un plan B
  7. Sécurité - aucun mot de passe, aucun jeton, aucune donnée personnelle, car le moindre script de la page peut les atteindre

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelle est la principale différence entre localStorage et sessionStorage ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un PreferencesManager avec les méthodes save(), load(), remove() et clear() en utilisant JSON.stringify/parse.

  • Clique dans l'ordre

    Ordonne les éléments pour enregistrer un objet dans localStorage :

Articles utiles