Synchronisation de l'État Vuex avec localStorage en Vue.js

Apprenez à synchroniser votre store Vuex avec localStorage. Utilisez les watchers pour persister l'état et gérer les sessions utilisateur.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 9 min read
Synchronisation de l'État Vuex avec localStorage en Vue.js

Synchronisation de l’État Vuex avec localStorage en Vue.js

Introduction

La persistance de l’état est un défi majeur dans le développement d’applications web modernes. Lorsqu’un utilisateur actualise la page ou ferme son navigateur, tout l’état de votre application Vuex est perdu. C’est particulièrement problématique pour les données critiques comme les tokens d’authentification, les préférences utilisateur ou le contenu d’un panier d’achat.

Dans ce guide complet, nous allons explorer en profondeur les différentes stratégies pour synchroniser votre store Vuex avec le stockage du navigateur. Vous apprendrez à utiliser les watchers Vue, à créer des plugins Vuex personnalisés, et à implémenter un système d’authentification robuste avec persistance automatique.

Pourquoi la Persistance de l’État est Importante

Le Problème de la Volatilité

Par défaut, Vuex stocke l’état en mémoire. Cela signifie que :

  • Rafraîchissement de page : L’état est complètement réinitialisé
  • Fermeture d’onglet : Toutes les données sont perdues
  • Crash du navigateur : Aucune récupération possible
// store/index.js - État initial sans persistance
const store = createStore({
  state: {
    user: null,        // Perdu au refresh
    token: null,       // Perdu au refresh
    cart: [],          // Perdu au refresh
    preferences: {}    // Perdu au refresh
  }
});

Impact sur l’Expérience Utilisateur

Sans persistance, vos utilisateurs doivent :

  • Se reconnecter à chaque visite
  • Reconfigurer leurs préférences
  • Perdre leur panier d’achat
  • Recommencer leur progression

Comparaison des Méthodes de Stockage

Avant de plonger dans l’implémentation, comparons les différentes options de stockage disponibles dans le navigateur.

Tableau Comparatif

CritèrelocalStoragesessionStorageCookiesIndexedDB
Capacité~5-10 MB~5-10 MB~4 KBIllimitée
Durée de viePermanenteSessionConfigurablePermanente
AccessibilitéClient seulClient seulClient + ServeurClient seul
APISynchroneSynchroneComplexeAsynchrone
Cas d’usagePréférences, tokensDonnées temporairesAuth cookiesGros volumes
PerformanceRapideRapideLent (envoi HTTP)Variable

localStorage vs sessionStorage

// localStorage - Persiste après fermeture du navigateur
localStorage.setItem('token', 'abc123');
// Disponible indéfiniment jusqu'à suppression explicite

// sessionStorage - Limité à la session de l'onglet
sessionStorage.setItem('tempData', 'xyz789');
// Supprimé à la fermeture de l'onglet

Quand Utiliser Quoi ?

localStorage est idéal pour :

  • Tokens d’authentification (avec précautions)
  • Préférences utilisateur (thème, langue)
  • Panier d’achat (e-commerce)
  • Données de formulaire en cours

sessionStorage est préférable pour :

  • Données sensibles temporaires
  • État de navigation (wizard multi-étapes)
  • Données de session courte

Cookies sont nécessaires pour :

  • Authentification côté serveur (httpOnly)
  • Tracking et analytics
  • Données partagées avec le backend

Synchronisation avec les Watchers Vue

Les watchers sont le mécanisme fondamental pour réagir aux changements d’état et synchroniser avec localStorage.

Implémentation Basique

// App.vue
<script>
import { computed, watch } from 'vue';
import { useStore } from 'vuex';

export default {
  name: 'App',
  setup() {
    const store = useStore();

    // Propriété calculée pour accéder au token
    const token = computed(() => store.getters.token);
    const user = computed(() => store.getters.user);

    // Watcher pour synchroniser le token avec localStorage
    watch(token, (newToken, oldToken) => {
      if (newToken) {
        localStorage.setItem('authToken', newToken);
        console.log('Token sauvegardé dans localStorage');
      } else {
        localStorage.removeItem('authToken');
        console.log('Token supprimé de localStorage');
      }
    });

    // Watcher pour synchroniser l'utilisateur
    watch(user, (newUser) => {
      if (newUser) {
        localStorage.setItem('user', JSON.stringify(newUser));
      } else {
        localStorage.removeItem('user');
      }
    }, { deep: true }); // deep: true pour les objets imbriqués

    return { token, user };
  }
};
</script>

Watcher avec Options Avancées

// composables/useStatePersistence.js
import { watch } from 'vue';
import { useStore } from 'vuex';

export function useStatePersistence() {
  const store = useStore();

  // Watcher avec debounce pour éviter trop d'écritures
  let saveTimeout = null;

  watch(
    () => store.state.cart,
    (newCart) => {
      // Debounce de 500ms
      clearTimeout(saveTimeout);
      saveTimeout = setTimeout(() => {
        try {
          localStorage.setItem('cart', JSON.stringify(newCart));
        } catch (error) {
          console.error('Erreur de sauvegarde du panier:', error);
          // Gérer le quota dépassé
          if (error.name === 'QuotaExceededError') {
            // Nettoyer les anciennes données
            cleanupOldData();
          }
        }
      }, 500);
    },
    { deep: true }
  );

  function cleanupOldData() {
    // Logique de nettoyage
    const keysToKeep = ['authToken', 'user', 'cart'];
    Object.keys(localStorage).forEach(key => {
      if (!keysToKeep.includes(key)) {
        localStorage.removeItem(key);
      }
    });
  }
}

Sérialisation et Désérialisation JSON

localStorage ne peut stocker que des chaînes de caractères. La sérialisation correcte est donc cruciale.

Utilitaires de Sérialisation

// utils/storage.js
export const StorageUtils = {
  /**
   * Sauvegarde une valeur dans localStorage avec sérialisation JSON
   * @param {string} key - Clé de stockage
   * @param {any} value - Valeur à stocker
   * @param {number} ttl - Durée de vie en millisecondes (optionnel)
   */
  set(key, value, ttl = null) {
    try {
      const item = {
        value,
        timestamp: Date.now(),
        ttl
      };
      localStorage.setItem(key, JSON.stringify(item));
      return true;
    } catch (error) {
      console.error(`Erreur de stockage pour ${key}:`, error);
      return false;
    }
  },

  /**
   * Récupère une valeur depuis localStorage avec désérialisation
   * @param {string} key - Clé de stockage
   * @param {any} defaultValue - Valeur par défaut si non trouvé
   */
  get(key, defaultValue = null) {
    try {
      const itemStr = localStorage.getItem(key);
      if (!itemStr) return defaultValue;

      const item = JSON.parse(itemStr);

      // Vérifier si les données ont un TTL et si elles sont expirées
      if (item.ttl && Date.now() - item.timestamp > item.ttl) {
        localStorage.removeItem(key);
        return defaultValue;
      }

      return item.value;
    } catch (error) {
      console.error(`Erreur de lecture pour ${key}:`, error);
      return defaultValue;
    }
  },

  /**
   * Supprime une entrée du localStorage
   */
  remove(key) {
    localStorage.removeItem(key);
  },

  /**
   * Vérifie si une clé existe et n'est pas expirée
   */
  has(key) {
    return this.get(key) !== null;
  },

  /**
   * Nettoie toutes les entrées expirées
   */
  cleanup() {
    const now = Date.now();
    Object.keys(localStorage).forEach(key => {
      try {
        const itemStr = localStorage.getItem(key);
        if (itemStr) {
          const item = JSON.parse(itemStr);
          if (item.ttl && now - item.timestamp > item.ttl) {
            localStorage.removeItem(key);
          }
        }
      } catch {
        // Ignorer les entrées non-JSON
      }
    });
  }
};

Gestion des Types Complexes

// utils/serializers.js
export const Serializers = {
  /**
   * Sérialiseur pour les objets Date
   */
  serializeWithDates(obj) {
    return JSON.stringify(obj, (key, value) => {
      if (value instanceof Date) {
        return { __type: 'Date', value: value.toISOString() };
      }
      return value;
    });
  },

  /**
   * Désérialiseur avec restauration des Dates
   */
  deserializeWithDates(str) {
    return JSON.parse(str, (key, value) => {
      if (value && value.__type === 'Date') {
        return new Date(value.value);
      }
      return value;
    });
  },

  /**
   * Sérialiseur pour les Map et Set
   */
  serializeCollections(obj) {
    return JSON.stringify(obj, (key, value) => {
      if (value instanceof Map) {
        return { __type: 'Map', value: Array.from(value.entries()) };
      }
      if (value instanceof Set) {
        return { __type: 'Set', value: Array.from(value) };
      }
      return value;
    });
  },

  /**
   * Désérialiseur pour Map et Set
   */
  deserializeCollections(str) {
    return JSON.parse(str, (key, value) => {
      if (value && value.__type === 'Map') {
        return new Map(value.value);
      }
      if (value && value.__type === 'Set') {
        return new Set(value.value);
      }
      return value;
    });
  }
};

Plugins Vuex pour la Persistance

Les plugins Vuex offrent une approche élégante et centralisée pour gérer la persistance.

Plugin de Persistance Personnalisé

// store/plugins/persistencePlugin.js
export function createPersistencePlugin(options = {}) {
  const {
    key = 'vuex-state',
    paths = null, // Chemins spécifiques à persister (null = tout)
    storage = localStorage,
    beforeRestore = (state) => state,
    afterRestore = () => {},
    filter = () => true
  } = options;

  return (store) => {
    // Restaurer l'état au démarrage
    const savedState = storage.getItem(key);
    if (savedState) {
      try {
        let parsedState = JSON.parse(savedState);
        parsedState = beforeRestore(parsedState);
        store.replaceState({
          ...store.state,
          ...parsedState
        });
        afterRestore(store);
      } catch (error) {
        console.error('Erreur de restauration de l\'état:', error);
        storage.removeItem(key);
      }
    }

    // S'abonner aux mutations pour sauvegarder
    store.subscribe((mutation, state) => {
      if (!filter(mutation)) return;

      try {
        const stateToSave = paths
          ? paths.reduce((acc, path) => {
              const value = path.split('.').reduce((obj, key) => obj?.[key], state);
              if (value !== undefined) {
                setNestedValue(acc, path, value);
              }
              return acc;
            }, {})
          : state;

        storage.setItem(key, JSON.stringify(stateToSave));
      } catch (error) {
        console.error('Erreur de sauvegarde de l\'état:', error);
      }
    });
  };
}

// Utilitaire pour définir une valeur imbriquée
function setNestedValue(obj, path, value) {
  const keys = path.split('.');
  let current = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    if (!current[keys[i]]) current[keys[i]] = {};
    current = current[keys[i]];
  }
  current[keys[keys.length - 1]] = value;
}

Utilisation du Plugin

// store/index.js
import { createStore } from 'vuex';
import { createPersistencePlugin } from './plugins/persistencePlugin';

const persistencePlugin = createPersistencePlugin({
  key: 'my-app-state',
  paths: ['auth.token', 'auth.user', 'cart.items', 'preferences'],
  beforeRestore: (state) => {
    // Valider le token avant restauration
    if (state.auth?.token) {
      const tokenExpiry = parseTokenExpiry(state.auth.token);
      if (tokenExpiry < Date.now()) {
        delete state.auth.token;
        delete state.auth.user;
      }
    }
    return state;
  },
  afterRestore: (store) => {
    // Actions post-restauration
    if (store.state.auth.token) {
      store.dispatch('auth/validateToken');
    }
  },
  filter: (mutation) => {
    // Exclure certaines mutations de la persistance
    const excludedMutations = ['SET_LOADING', 'SET_ERROR', 'CLEAR_MESSAGES'];
    return !excludedMutations.includes(mutation.type);
  }
});

export default createStore({
  // ... modules et state
  plugins: [persistencePlugin]
});

Système d’Authentification Complet

Voici une implémentation complète d’un module d’authentification avec persistance.

Module Auth Vuex

// store/modules/auth.js
import { StorageUtils } from '@/utils/storage';
import authApi from '@/api/auth';

const TOKEN_KEY = 'authToken';
const USER_KEY = 'authUser';
const TOKEN_TTL = 7 * 24 * 60 * 60 * 1000; // 7 jours

export default {
  namespaced: true,

  state: () => ({
    token: StorageUtils.get(TOKEN_KEY, null),
    user: StorageUtils.get(USER_KEY, null),
    isLoading: false,
    error: null
  }),

  getters: {
    isAuthenticated: (state) => !!state.token && !!state.user,
    token: (state) => state.token,
    user: (state) => state.user,
    userRole: (state) => state.user?.role || 'guest',
    isAdmin: (state) => state.user?.role === 'admin'
  },

  mutations: {
    SET_TOKEN(state, token) {
      state.token = token;
      if (token) {
        StorageUtils.set(TOKEN_KEY, token, TOKEN_TTL);
      } else {
        StorageUtils.remove(TOKEN_KEY);
      }
    },

    SET_USER(state, user) {
      state.user = user;
      if (user) {
        StorageUtils.set(USER_KEY, user, TOKEN_TTL);
      } else {
        StorageUtils.remove(USER_KEY);
      }
    },

    SET_LOADING(state, isLoading) {
      state.isLoading = isLoading;
    },

    SET_ERROR(state, error) {
      state.error = error;
    },

    CLEAR_AUTH(state) {
      state.token = null;
      state.user = null;
      state.error = null;
      StorageUtils.remove(TOKEN_KEY);
      StorageUtils.remove(USER_KEY);
    }
  },

  actions: {
    async login({ commit, dispatch }, credentials) {
      commit('SET_LOADING', true);
      commit('SET_ERROR', null);

      try {
        const response = await authApi.login(credentials);
        const { token, user } = response.data;

        commit('SET_TOKEN', token);
        commit('SET_USER', user);

        // Charger les données utilisateur après connexion
        await dispatch('loadUserData', null, { root: true });

        return { success: true };
      } catch (error) {
        const message = error.response?.data?.message || 'Erreur de connexion';
        commit('SET_ERROR', message);
        return { success: false, error: message };
      } finally {
        commit('SET_LOADING', false);
      }
    },

    async logout({ commit }) {
      try {
        await authApi.logout();
      } catch (error) {
        console.error('Erreur lors de la déconnexion:', error);
      } finally {
        commit('CLEAR_AUTH');
        // Nettoyer d'autres données liées à l'utilisateur
        localStorage.removeItem('cart');
        localStorage.removeItem('preferences');
      }
    },

    async validateToken({ state, commit, dispatch }) {
      if (!state.token) return false;

      try {
        const response = await authApi.validateToken(state.token);
        if (response.data.valid) {
          // Token valide, rafraîchir les infos utilisateur
          commit('SET_USER', response.data.user);
          return true;
        } else {
          // Token invalide, déconnecter
          await dispatch('logout');
          return false;
        }
      } catch (error) {
        console.error('Erreur de validation du token:', error);
        await dispatch('logout');
        return false;
      }
    },

    async refreshToken({ state, commit }) {
      if (!state.token) return null;

      try {
        const response = await authApi.refreshToken(state.token);
        const newToken = response.data.token;
        commit('SET_TOKEN', newToken);
        return newToken;
      } catch (error) {
        console.error('Erreur de rafraîchissement du token:', error);
        return null;
      }
    },

    // Initialisation au démarrage de l'app
    async initializeAuth({ state, dispatch }) {
      if (state.token) {
        const isValid = await dispatch('validateToken');
        if (!isValid) {
          console.log('Token invalide, utilisateur déconnecté');
        }
      }
    }
  }
};

Composant de Connexion

<!-- components/LoginForm.vue -->
<template>
  <form @submit.prevent="handleSubmit" class="login-form">
    <div class="form-group">
      <label for="email">Email</label>
      <input
        id="email"
        v-model="form.email"
        type="email"
        required
        :disabled="isLoading"
      />
    </div>

    <div class="form-group">
      <label for="password">Mot de passe</label>
      <input
        id="password"
        v-model="form.password"
        type="password"
        required
        :disabled="isLoading"
      />
    </div>

    <div class="form-group">
      <label>
        <input v-model="form.rememberMe" type="checkbox" />
        Se souvenir de moi
      </label>
    </div>

    <div v-if="error" class="error-message">
      {{ error }}
    </div>

    <button type="submit" :disabled="isLoading">
      {{ isLoading ? 'Connexion...' : 'Se connecter' }}
    </button>
  </form>
</template>

<script>
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';

export default {
  name: 'LoginForm',
  setup() {
    const store = useStore();
    const router = useRouter();

    const form = ref({
      email: '',
      password: '',
      rememberMe: false
    });

    const isLoading = computed(() => store.state.auth.isLoading);
    const error = computed(() => store.state.auth.error);

    async function handleSubmit() {
      const result = await store.dispatch('auth/login', {
        email: form.value.email,
        password: form.value.password
      });

      if (result.success) {
        // Rediriger vers la page précédente ou le dashboard
        const redirectPath = router.currentRoute.value.query.redirect || '/dashboard';
        router.push(redirectPath);
      }
    }

    return {
      form,
      isLoading,
      error,
      handleSubmit
    };
  }
};
</script>

Bonnes Pratiques

1. Chiffrement des Données Sensibles

Ne stockez jamais de données sensibles en clair dans localStorage.

// utils/encryption.js
import CryptoJS from 'crypto-js';

const SECRET_KEY = import.meta.env.VITE_ENCRYPTION_KEY;

export const EncryptedStorage = {
  set(key, value) {
    const encrypted = CryptoJS.AES.encrypt(
      JSON.stringify(value),
      SECRET_KEY
    ).toString();
    localStorage.setItem(key, encrypted);
  },

  get(key) {
    const encrypted = localStorage.getItem(key);
    if (!encrypted) return null;

    try {
      const bytes = CryptoJS.AES.decrypt(encrypted, SECRET_KEY);
      return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
    } catch {
      return null;
    }
  }
};

2. Validation des Données Restaurées

Toujours valider les données avant de les utiliser.

function validateRestoredState(state) {
  // Vérifier la structure attendue
  if (typeof state !== 'object') return null;

  // Valider le token
  if (state.token && !isValidJWT(state.token)) {
    delete state.token;
    delete state.user;
  }

  // Valider le panier
  if (state.cart && !Array.isArray(state.cart)) {
    state.cart = [];
  }

  return state;
}

3. Gestion des Quotas

Surveillez et gérez l’espace de stockage disponible.

function checkStorageQuota() {
  if (navigator.storage && navigator.storage.estimate) {
    navigator.storage.estimate().then(({ usage, quota }) => {
      const percentUsed = (usage / quota) * 100;
      console.log(`Stockage utilisé: ${percentUsed.toFixed(2)}%`);

      if (percentUsed > 80) {
        console.warn('Attention: stockage presque plein');
        cleanupOldData();
      }
    });
  }
}

4. Synchronisation Multi-Onglets

Détectez les changements dans d’autres onglets.

// Dans App.vue ou un composable
window.addEventListener('storage', (event) => {
  if (event.key === 'authToken') {
    if (event.newValue === null) {
      // L'utilisateur s'est déconnecté dans un autre onglet
      store.commit('auth/CLEAR_AUTH');
      router.push('/login');
    } else if (event.newValue !== store.state.auth.token) {
      // Nouveau token dans un autre onglet
      store.commit('auth/SET_TOKEN', event.newValue);
    }
  }
});

5. Nettoyage à la Déconnexion

Assurez-vous de nettoyer toutes les données à la déconnexion.

function clearAllUserData() {
  const keysToRemove = [
    'authToken',
    'authUser',
    'cart',
    'preferences',
    'recentSearches',
    'draftMessages'
  ];

  keysToRemove.forEach(key => localStorage.removeItem(key));
  sessionStorage.clear();
}

6. Logging et Monitoring

Suivez les opérations de persistance pour le débogage.

const PersistenceLogger = {
  log(action, key, success) {
    if (import.meta.env.DEV) {
      console.log(`[Persistence] ${action}: ${key} - ${success ? 'OK' : 'FAIL'}`);
    }
  }
};

Pièges Courants

1. Oublier la Sérialisation JSON

// MAUVAIS - Ne fonctionne pas avec les objets
localStorage.setItem('user', { name: 'John' }); // "[object Object]"

// BON - Sérialisation correcte
localStorage.setItem('user', JSON.stringify({ name: 'John' }));

2. Ne Pas Gérer les Erreurs de Stockage

// MAUVAIS - Peut planter si le quota est dépassé
localStorage.setItem('data', largeData);

// BON - Gestion des erreurs
try {
  localStorage.setItem('data', largeData);
} catch (error) {
  if (error.name === 'QuotaExceededError') {
    // Nettoyer ou notifier l'utilisateur
  }
}

3. Stocker des Données Sensibles Sans Protection

// MAUVAIS - Token en clair
localStorage.setItem('token', 'secret-jwt-token');

// BON - Utiliser des cookies httpOnly pour les tokens sensibles
// Ou chiffrer si localStorage est nécessaire

4. Ignorer la Synchronisation Multi-Onglets

// MAUVAIS - L'état peut devenir incohérent entre onglets
store.commit('SET_USER', newUser);

// BON - Écouter les événements storage
window.addEventListener('storage', handleStorageChange);

5. Ne Pas Valider les Données Restaurées

// MAUVAIS - Données potentiellement corrompues
const user = JSON.parse(localStorage.getItem('user'));
store.commit('SET_USER', user);

// BON - Validation avant utilisation
const userData = JSON.parse(localStorage.getItem('user'));
if (userData && isValidUser(userData)) {
  store.commit('SET_USER', userData);
}

Conclusion

La synchronisation de l’état Vuex avec localStorage est essentielle pour offrir une expérience utilisateur fluide et persistante. Dans cet article, nous avons couvert :

  • Les différences entre localStorage, sessionStorage et cookies
  • L’utilisation des watchers pour synchroniser automatiquement l’état
  • La sérialisation JSON correcte avec gestion des types complexes
  • Les plugins Vuex pour une approche centralisée et maintenable
  • Un système d’authentification complet avec persistance sécurisée

En suivant les bonnes pratiques et en évitant les pièges courants, vous pouvez construire des applications Vue.js robustes qui préservent l’état utilisateur de manière fiable et sécurisée.

Prochaines Étapes

  • Explorer Pinia comme alternative moderne à Vuex avec persistance intégrée
  • Implémenter une synchronisation avec le serveur pour les données critiques
  • Étudier IndexedDB pour les volumes de données importants
  • Configurer des tests unitaires pour vos mécanismes de persistance

Ressources Complémentaires

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles