Table of Contents
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ère | localStorage | sessionStorage | Cookies | IndexedDB |
|---|---|---|---|---|
| Capacité | ~5-10 MB | ~5-10 MB | ~4 KB | Illimitée |
| Durée de vie | Permanente | Session | Configurable | Permanente |
| Accessibilité | Client seul | Client seul | Client + Serveur | Client seul |
| API | Synchrone | Synchrone | Complexe | Asynchrone |
| Cas d’usage | Préférences, tokens | Données temporaires | Auth cookies | Gros volumes |
| Performance | Rapide | Rapide | Lent (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
In-Article Ad
Dev Mode
Tags
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
Securiser votre application Vue.js avec Vue Router et les jetons JWT
Implementez une authentification securisee avec Vue Router et Vuex. Gerez les jetons JWT et protegez vos routes sensibles efficacement.
Gestion de Formulaires avec Vuex : Mutations, Getters et Actions
Maîtrisez la gestion des formulaires avec Vuex. Créez des mutations pour chaque champ, des getters pour l'état et des actions pour les API.
Migrer vers la Composition API Vue 3 : Guide Etape par Etape
Migrez vos composants Vue vers la Composition API. useStore, ref, onMounted et lifecycle hooks expliques avec des exemples pratiques.