Vue.js Avancé

Maîtriser la Réactivité Avancée de Vue.js : Au-delà des Bases

Plongez dans les mécanismes profonds de la réactivité Vue.js pour optimiser performances et déboguer efficacement. Explorez les pièges cachés, les patterns avancés et les techniques de profiling qui séparent les développeurs experts des autres.

Preparetoi.academy 30 min

1. Le Système de Réactivité : Architecture Interne et Proxy

Définition
Le système de réactivité de Vue.js est un mécanisme sophistiqué qui transforme les propriétés d'objet en accesseurs/mutateurs (getters/setters) pour détecter automatiquement les changements de données. À partir de Vue 3, Vue utilise les Proxy JavaScript pour intercepter les opérations sur les objets, remplaçant le système basé sur Object.defineProperty de Vue 2.

Explication détaillée
Vue.js crée un graphe de dépendances qui établit les relations entre les données réactives et les composants qui les consomment. Quand une valeur change, Vue notifie automatiquement tous les dépendants et déclenche les mises à jour du DOM. Comprendre cette architecture est crucial pour éviter les pièges courants comme la création de références inutiles ou les mutations directes qui ne déclenchent pas les mises à jour.

Le système utilise une approche basée sur le suivi des dépendances au moment de l'exécution. Chaque fois qu'une propriété réactive est lue dans une fonction (comme render ou computed), elle se "souscrit" à cette propriété. Lorsque la propriété change, tous les souscripteurs sont notifiés et recalculés. C'est une approche élégante mais qui a des implications importantes en termes de performance et de débogage.

// Vue 3 - Réactivité avec Proxy
import { reactive, ref, effect } from 'vue'

// Approche Proxy avec reactive
const state = reactive({
  count: 0,
  nested: {
    message: 'Hello'
  },
  array: [1, 2, 3]
})

// Approche ref pour les primitives
const count = ref(0)

// Suivi manuel des dépendances avec effect
effect(() => {
  console.log(`Count changed to: ${state.count}`)
  console.log(`Message: ${state.nested.message}`)
})

state.count++ // Déclenche effect
state.nested.message = 'World' // Déclenche effect

// Accès et modification
console.log(count.value) // 0
count.value = 5 // Mise à jour réactive

// Problème : mutations directes sur ref non détectées
const arr = ref([1, 2, 3])
arr.value[0] = 99 // Réactif
arr.value.length = 2 // Peut ne pas être détecté dans tous les cas

Tableau comparatif

Aspect Vue 2 (Object.defineProperty) Vue 3 (Proxy)
Performance Surcharge mémoire (chaque propriété) Plus léger, interception au niveau objet
Tableaux Nécessite des méthodes spéciales Mutation naturelle supportée
Propriétés dynamiques Requirejs Vue.set() Ajout/suppression automatique
Profondeur Récursif, peut être lent Lazy, évalué à la demande
Débogage Difficile à tracer Pile d'appels plus claire

Astuce d'expert
Utilisez le DevTools Vue avec le profiler pour visualiser les dépendances en temps réel. Activez la logging réactive : window.__VUE_PROD_DEVTOOLS__ = true et inspectez la structure du graphe de réactivité pour identifier les dépendances inutiles qui ralentissent votre application.

⚠️ Attention critique
Les Proxy ne détectent pas les modifications de propriétés non configurables ou les opérations qui contournent le Proxy comme Object.freeze(). De plus, la réactivité fonctionne uniquement sur les objets et tableaux ; les primitives nécessitent ref(). Modifier directement des propriétés sans passer par le Proxy les rend transparentes au système de suivi.


2. Computed, Watchers et Dépendances Circulaires

Définition
Les computed sont des propriétés dérivées qui se recalculent automatiquement quand leurs dépendances changent, tandis que les watchers (observateurs) exécutent du code personnalisé en réaction aux changements. Les dépendances circulaires surviennent quand deux ou plusieurs propriétés réactives dépendent l'une de l'autre de manière cyclique.

Explication détaillée
Les propriétés calculées sont une forme de mise en cache intelligent : elles ne se recalculent que si une dépendance a changé. Vue accumule les dépendances lors de chaque exécution de la fonction getter, créant ainsi un ensemble de dépendances dynamique. Les watchers, en revanche, permettent des effets secondaires explicites avec un contrôle granulaire sur les déclencheurs (deep, immediate, flush).

Les dépendances circulaires dans la réactivité Vue peuvent causer des boucles infinies ou des comportements imprévisibles. Par exemple, si le computed A dépend de B, et le watcher de B modifie A, cela crée une boucle. Ces situations doivent être gérées avec soin en utilisant des conditions de garde ou des flags d'état.

// Computed avancé avec getter/setter
import { computed, ref, watch } from 'vue'

const firstName = ref('Jean')
const lastName = ref('Dupont')

// Computed simple - getter uniquement
const fullName = computed(() => {
  console.log('Recalcul fullName')
  return `${firstName.value} ${lastName.value}`
})

// Computed avec setter explicite
const fullNameWithSetter = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    const parts = newValue.split(' ')
    firstName.value = parts[0]
    lastName.value = parts[1]
  }
})

fullNameWithSetter.value = 'Paul Martin' // Déclenche le setter

// Watcher simple
watch(() => firstName.value, (newVal, oldVal) => {
  console.log(`Prénom changé de ${oldVal} à ${newVal}`)
})

// Watcher avancé avec options
watch(
  () => firstName.value,
  (newVal, oldVal) => {
    console.log(`Changement détecté: ${oldVal} -> ${newVal}`)
  },
  {
    immediate: true,      // Exécuter au montage
    flush: 'post',        // Exécuter après les mises à jour DOM
    flush: 'sync'         // Synchrone (rare, peut causer des issues)
  }
)

// Watcher profond sur objet réactif
const user = reactive({
  name: 'Alice',
  profile: { age: 30, city: 'Paris' }
})

watch(
  () => user,
  (newVal) => {
    console.log('User changed:', newVal)
  },
  { deep: true }  // Surveille les changements imbriqués
)

// ATTENTION: Gestion des dépendances circulaires
const value = ref(0)
const doubled = computed(() => value.value * 2)
const isEven = computed(() => doubled.value % 2 === 0)

watch(isEven, (newVal) => {
  if (newVal) {
    // Condition de garde pour éviter la boucle
    value.value += 1
  }
})

// Détection des dépendances circulaires avec watchEffect
import { watchEffect } from 'vue'

watchEffect(() => {
  // Toutes les dépendances sont trackées automatiquement
  console.log(`Count: ${value.value}, Doubled: ${doubled.value}`)
})

// watchEffect avec flush et trackId
watchEffect(
  () => {
    console.log('Effect exécuté')
  },
  {
    flush: 'post',
    onTrack(e) {
      console.log('Dépendance trackée:', e)
    },
    onTrigger(e) {
      console.log('Watcher déclenché par:', e)
    }
  }
)

Tableau comparatif

Feature Computed Watch WatchEffect
Mise en cache ✅ Oui ❌ Non ❌ Non
Dépendances explicites ✅ Automatiques ❌ Manuelles ✅ Automatiques
Getter/Setter ✅ Support natif ❌ Non ❌ Non
Performance 🚀 Optimale ⚠️ À surveiller ⚠️ À surveiller
Cas d'usage Dérivations pures Effets secondaires Logique complexe
Dépendances circulaires ⚠️ Possible ⚠️ Courant ⚠️ Courant

Astuce d'expert
Pour déboguer les dépendances circulaires, utilisez les callbacks onTrack et onTrigger dans watchEffect pour logger exactement quand et pourquoi les dépendances sont recalculées. Encapsulez les modifications d'état dans des conditions de garde strictes et utilisez des flags de "transition d'état" pour prévenir les boucles. Testez toujours avec les options de débogage activées.

⚠️ Attention critique
Les computed sans dépendances explicites peuvent créer des effets de bord cachés. Les watchers profonds (deep: true) sont extrêmement coûteux sur les gros objets. Les dépendances circulaires non gérées causent des boucles infinies qui freezent l'application. Ne mettez jamais d'opérations longues ou asynchrones directement dans un computed ; utilisez un watcher ou watchEffect avec gestion d'erreurs appropriée.


3. Performance Optimization : Lazy Loading, Code Splitting et Virtual Scrolling

Définition
La performance Vue.js avancée implique de minimiser le bundle initial avec le code splitting, de charger les ressources à la demande (lazy loading), et d'implémenter des techniques comme le virtual scrolling pour les listes géantes. Ces optimisations réduisent le temps de chargement initial et maintiennent une fluidité constante même avec beaucoup de données.

Explication détaillée
Le lazy loading des composants permet de découper le bundle en chunks que le navigateur télécharge à la demande. Cela réduit le JavaScript initial et améliore les Core Web Vitals. Le virtual scrolling (windowing) est une technique qui ne rend dans le DOM que les éléments visibles dans la fenêtre de visualisation, crucial pour les listes de milliers d'éléments.

Vue 3 offre des outils natifs pour le code splitting avec les imports dynamiques et le système de modules. Combiné avec des stratégies de prefetching et preloading intelligentes, on peut créer des applications très performantes. Le profiling est essentiel : utilisez Lighthouse, DevTools Performance, et le Vue Profiler pour identifier les goulots d'étranglement réels.

// 1. Lazy loading de composants
import { defineAsyncComponent } from 'vue'

// Pattern simple
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// Pattern avancé avec loading, error et timeout
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,           // Délai avant affichage du loading
  timeout: 10000,       // Timeout après 10s
  suspensible: true     // Compatible Suspense
})

// 2. Virtual Scrolling avec composant custom
import { ref, computed } from 'vue'

export default {
  name: 'VirtualScroller',
  props: {
    items: Array,
    itemHeight: Number,
    containerHeight: Number
  },
  setup(props) {
    const scrollTop = ref(0)
    
    // Calcul des items visibles
    const visibleRange = computed(() => {
      const startIndex = Math.floor(scrollTop.value / props.itemHeight)
      const endIndex = Math.ceil(
        (scrollTop.value + props.containerHeight) / props.itemHeight
      )
      return {
        start: Math.max(0, startIndex - 5), // Buffer de 5 items
        end: Math.min(props.items.length, endIndex + 5)
      }
    })

    const visibleItems = computed(() =>
      props.items.slice(visibleRange.value.start, visibleRange.value.end)
    )

    const offsetY = computed(() =>
      visibleRange.value.start * props.itemHeight
    )

    const totalHeight = computed(() =>
      props.items.length * props.itemHeight
    )

    const onScroll = (e) => {
      scrollTop.value = e.target.scrollTop
    }

    return {
      visibleItems,
      offsetY,
      totalHeight,
      onScroll,
      visibleRange
    }
  }
}

// Template du virtual scroller
/*
<div
  class="virtual-scroller"
  :style="{ height: containerHeight + 'px' }"
  @scroll="onScroll"
>
  <div :style="{ height: totalHeight + 'px', position: 'relative' }">
    <div
      :style="{
        transform: `translateY(${offsetY}px)`,
        position: 'absolute',
        top: 0,
        left: 0,
        right: 0
      }"
    >
      <div
        v-for="(item, idx) in visibleItems"
        :key="visibleRange.start + idx"
        :style="{ height: itemHeight + 'px' }"
      >
        <slot :item="item" />
      </div>
    </div>
  </div>
</div>
*/

// 3. Code Splitting avec routes
// router.js
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/dashboard',
    component: () => import(
      /* webpackChunkName: "dashboard" */
      './views/Dashboard.vue'
    ),
    meta: { requiresAuth: true }
  },
  {
    path: '/admin',
    component: () => import(
      /* webpackChunkName: "admin" */
      './views/Admin.vue'
    ),
    meta: { requiresRole: 'admin' }
  }
]

// 4. Prefetch/Preload intelligent
// Prefetch les chunks pour les routes probables
router.beforeEach((to, from, next) => {
  const adminChunk = import(
    /* webpackPrefetch: true */
    './views/Admin.vue'
  )
  next()
})

// 5. Optimisation des re-rendus
import { computed, shallowRef, triggerRef } from 'vue'

// shallowRef pour les objets non réactifs profonds
const largeData = shallowRef({
  items: [] // Pas de réactivité imbriquée
})

// Mise à jour manuelle
const updateData = () => {
  largeData.value.items.push(newItem)
  triggerRef(largeData) // Force la mise à jour
}

// 6. Profiling avec Vue DevTools
const logMetrics = () => {
  if (window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
    performance.mark('component-render-start')
    // ... composant
    performance.mark('component-render-end')
    performance.measure(
      'component-render',
      'component-render-start',
      'component-render-end'
    )
  }
}

Tableau comparatif

Technique Impact Complexité Cas d'usage
Lazy Loading Components ⬇️ Initial JS 🟢 Basse Routes, modales
Code Splitting ⬇️ Initial JS 🟡 Moyenne Gros bundles
Virtual Scrolling ⬆️ Runtime perf 🔴 Haute Listes 1000+ items
Suspense Boundaries 🎨 UX 🟡 Moyenne Async components
shallowRef ⬆️ Runtime perf 🟢 Basse Gros objets immuables

Astuce d'expert
Utilisez performance.mark() et performance.measure() pour profiler les sections critiques. Combinez avec Lighthouse CI pour tracker les performances à chaque commit. Pour le virtual scrolling, ajoutez un buffer de 5-10 items avant et après la zone visible pour éviter le "flashing". Utilisez requestAnimationFrame dans les handlers de scroll pour découpler le scroll du rendu.

⚠️ Attention critique
Le lazy loading avec Suspense peut créer des UX confuses si pas bien gérée. Virtual scrolling casse les recherches browser (Ctrl+F) si non implémenté correctement. Le code splitting excessif crée plus de requêtes HTTP que de bénéfice. Ne prefetch pas à l'aveugle : analysez les patterns de navigation réels. Les shallowRef perdent la réactivité imbriquée, ce qui peut créer des bugs subtils difficiles à déboguer.


4. Debugging Avancé : Memory Leaks, Performance Profiling et Error Handling

Définition
Le débogage Vue.js avancé implique l'identification et la correction des fuites mémoire (memory leaks), l'analyse détaillée des performances avec des outils natifs, et la mise en place d'une gestion des erreurs robuste. Les fuites mémoire surviennent quand des références non nettoyées persistent en mémoire, causant une dégradation progressive.

Explication détaillée
Les fuites mémoire en Vue.js viennent souvent de watchers non supprimés lors du démontage, de références de DOM conservées, ou de closures capturant des contextes entiers. Vue 3 facilite le nettoyage automatique avec la Composition API, mais des pièges existent toujours. Le profiling mémoire révèle les allocations inutiles, tandis que le profiling CPU identifie le code coûteux.

Vue DevTools inclut un profiler qui enregistre chaque composant rendu, sa durée, et ses dépendances. Combined avec le profiler navigateur, on peut identifier précisément où l'application traîne. Une gestion d'erreur appropriée capture les exceptions non gérées, prévient les crashes utilisateur, et facilite le débogage en production.

// 1. Détection et prévention des memory leaks
import { ref, watch, onUnmounted, computed } from 'vue'

export default {
  setup() {
    const count = ref(0)
    let unsubscribe = null

    // ❌ MAUVAIS : Watcher sans cleanup
    watch(() => count.value, () => {
      console.log('Count changed')
    })

    // ✅ BON : Watcher avec cleanup explicite
    const cleanupWatch = watch(() => count.value, () => {
      console.log('Count changed')
    })

    // ✅ EXCELLENT : Cleanup automatique avec onUnmounted
    watch(() => count.value, () => {
      console.log('Count changed')
    })

    // ❌ MAUVAIS : Référence non nettoyée
    let timerInterval = null

    // ✅ BON : Nettoyage du timer
    onMounted(() => {
      timerInterval = setInterval(() => {
        count.value++
      }, 1000)
    })

    onUnmounted(() => {
      if (timerInterval) {
        clearInterval(timerInterval)
        timerInterval = null
      }
    })

    // ❌ MAUVAIS : Closure capturant le contexte entier
    const eventListener = (e) => {
      console.log(this) // Reference circulaire potentielle
    }

    // ✅ BON : Nettoyage approprié
    const setupEventListener = () => {
      const listener = (e) => {
        count.value++
      }
      window.addEventListener('resize', listener)

      onUnmounted(() => {
        window.removeEventListener('resize', listener)
      })
    }

    setupEventListener()

    return { count }
  }
}

// 2. Performance Monitoring avec PerformanceObserver
export const setupPerformanceMonitoring = () => {
  // Observer les longues tâches
  if (window.PerformanceObserver) {
    try {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          console.warn('Long task detected:', entry)
          // Envoyer à service de monitoring
          reportMetric({
            type: 'long-task',
            duration: entry.duration,
            name: entry.name
          })
        }
      })

      observer.observe({ entryTypes: ['longtask', 'measure'] })

      return observer
    } catch (e) {
      console.error('PerformanceObserver not supported', e)
    }
  }
}

// 3. Custom Error Handler pour Vue
import { createApp } from 'vue'

const app = createApp(App)

app.config.errorHandler = (err, instance, info) => {
  // Log dans le service de monitoring
  console.error('Vue Error:', err, info)

  logErrorToService({
    error: err.toString(),
    stack: err.stack,
    component: instance.$options.name,
    info: info,
    timestamp: Date.now(),
    userAgent: navigator.userAgent
  })

  // Optionnel : afficher un message utilisateur
  // showErrorNotification(err.message)
}

// Warning handler pour les avertissements Vue
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue Warning:', msg, trace)

  // Log uniquement en développement
  if (process.env.NODE_ENV === 'development') {
    logWarning({
      message: msg,
      component: instance.$options.name,
      trace: trace
    })
  }
}

// Global async error handler
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled Promise Rejection:', event.reason)

  logErrorToService({
    type: 'unhandledRejection',
    error: event.reason?.toString(),
    stack: event.reason?.stack
  })

  // Prévenir le crash
  event.preventDefault()
})

// 4. Memory Profiling with Chrome DevTools
export const analyzeMemoryUsage = () => {
  if (window.performance && window.performance.memory) {
    const { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit } =
      performance.memory

    return {
      usedMB: (usedJSHeapSize / 1048576).toFixed(2),
      totalMB: (totalJSHeapSize / 1048576).toFixed(2),
      limitMB: (jsHeapSizeLimit / 1048576).toFixed(2),
      percentUsed: ((usedJSHeapSize / jsHeapSizeLimit) * 100).toFixed(2)
    }
  }
}

// 5. Component Memory Tracking
const componentLifecycles = {}

export const trackComponentMemory = (name) => {
  return {
    setup() {
      const id = Math.random().toString(36)

      onMounted(() => {
        if (!componentLifecycles[name]) {
          componentLifecycles[name] = []
        }
        componentLifecycles[name].push({
          id,
          mounted: Date.now(),
          memory: performance.memory?.usedJSHeapSize
        })
      })

      onUnmounted(() => {
        const idx = componentLifecycles[name].findIndex(c => c.id === id)
        if (idx > -1) {
          componentLifecycles[name][idx].unmounted = Date.now()
          componentLifecycles[name][idx].finalMemory = performance.memory?.usedJSHeapSize
        }
      })
    }
  }
}

// 6. Sentry Integration pour error tracking en production
import * as Sentry from '@sentry/vue'

Sentry.init({
  dsn: 'https://your-sentry-dsn@sentry.io/project-id',
  integrations: [
    new Sentry.Replay({
      maskAllText: true,
      blockAllMedia: true
    })
  ],
  tracesSampleRate: 0.1,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  environment: process.env.NODE_ENV
})

app.use(Sentry.vueIntegration({
  trackComponents: true,
  componentName: true
}))

Tableau de diagnostic

Symptôme Cause probable Solution
Mémoire croît Watchers/timers non supprimés Audit onUnmounted, utiliser cleanup
Rendu lent Computed/watcher dépendances Profiler avec DevTools, optimiser dépendances
Freezing lors du scroll Virtual scrolling absent Implémenter windowing, shallowRef
Erreurs prod non détectées Pas de error handler Implémenter error handler global + Sentry
Memory spikes périodiques Fuites dans boucles Identifier et libérer références

Astuce d'expert
Utilisez Chrome DevTools "Memory" → "Heap Snapshots" pour comparer avant/après un scénario utilisateur. Prenez deux snapshots séparés par 30 secondes et cherchez les objets non collectés. Pour le profiling CPU, utilisez "Performance" tab, enregistrez une action utilisateur, et analysez la timeline avec la vue "Bottom-Up" groupée par fonction. Intégrez Sentry en production pour capturer les erreurs réelles.

⚠️ Attention critique
Les closures en JavaScript capturent automatiquement le scope entier, créant des références circulaires subtiles. Ne supposez pas que onUnmounted est appelé toujours (navigation SPA rapide peut contourner ce hook). performance.memory n'est disponible que dans Chrome ; testez toujours avec feature detection. Les memory leaks insidieux peuvent prendre des heures/jours à devenir évidents avec des milliers d'utilisateurs. Monitorez en production obligatoirement.


5. Patterns Avancés : Composables Réutilisables, Mixins vs Composition API et Anti-Patterns

Définition
Les composables sont des fonctions réutilisables encapsulant la logique avec état (Composition API). Ils remplacent les mixins obsolètes en offrant une meilleure encapsulation et traçabilité. Les anti-patterns sont des pratiques courantes mais nuisibles à la maintenabilité et performance ; les identifier et les éviter est crucial pour une architecture robuste.

Explication détaillée
Vue 3 privilégie la Composition API sur les mixins pour éviter les collisions de noms et les dépendances implicites. Un composable est simplement une fonction qui utilise les hooks Vue (ref, computed, watch) et retourne un objet réactif. Cette approche rend la logique testable, composable et type-safe (avec TypeScript).

Les anti-patterns courants incluent la mutation directe d'état global sans contrôle, la création de dépendances circulaires intentionnelles, l'absence de gestion d'erreurs, et la création de composants "god components" qui font trop de choses. Reconnaître et refactoriser ces patterns conduit à des bases de code plus maintenables et performantes.

// 1. Composable réutilisable - useAsync
import { ref, computed, unref } from 'vue'

export function useAsync(asyncFn, options = {}) {
  const { immediate = true, resetOnExecute = false, delay = 0 } = options

  const data = ref(null)
  const error = ref(null)
  const isLoading = ref(false)
  const isFinished = ref(false)

  const execute = async (...args) => {
    if (resetOnExecute) {
      data.value = null
      error.value = null
    }

    isLoading.value = true
    isFinished.value = false

    try {
      if (delay) {
        await new Promise(resolve => setTimeout(resolve, delay))
      }

      data.value = await asyncFn(...args)
      error.value = null
    } catch (err) {
      error.value = err
      data.value = null
    } finally {
      isLoading.value = false
      isFinished.value = true
    }

    return { data: data.value, error: error.value }
  }

  const retry = () => execute()

  if (immediate) {
    execute()
  }

  return {
    data,
    error,
    isLoading,
    isFinished,
    execute,
    retry
  }
}

// Usage
export default {
  setup() {
    const { data: posts, isLoading, error, execute } = useAsync(
      async () => {
        const res = await fetch('/api/posts')
        return res.json()
      },
      { immediate: false }
    )

    const loadPosts = () => {
      execute()
    }

    return { posts, isLoading, error, loadPosts }
  }
}

// 2. Composable avec état local - useLocalStorage
import { ref, watch } from 'vue'

export function useLocalStorage(key, initialValue = null) {
  // Initialiser depuis localStorage
  const getValue = () => {
    const stored = window.localStorage.getItem(key)
    if (stored === null) return initialValue
    try {
      return JSON.parse(stored)
    } catch {
      return stored
    }
  }

  const state = ref(getValue())

  // Synchroniser avec localStorage
  const setState = (newValue) => {
    state.value = newValue
    if (newValue === null) {
      window.localStorage.removeItem(key)
    } else {
      window.localStorage.setItem(key, JSON.stringify(newValue))
    }
  }

  // Écouter les changements d'autres onglets
  const handleStorageChange = (event) => {
    if (event.key === key) {
      try {
        state.value = JSON.parse(event.newValue)
      } catch {
        state.value = event.newValue
      }
    }
  }

  window.addEventListener('storage', handleStorageChange)

  onUnmounted(() => {
    window.removeEventListener('storage', handleStorageChange)
  })

  return {
    state,
    setState,
    reset: () => setState(initialValue)
  }
}

// 3. Composable avec injection/provision - usePagination
import { provide,
Accédez à des centaines d'examens QCM — Découvrir les offres Premium