Skip to content

Instantly share code, notes, and snippets.

@stephdl
Last active February 22, 2026 06:17
Show Gist options
  • Select an option

  • Save stephdl/fc72dcae7fdfb0fe5fe2e32111817200 to your computer and use it in GitHub Desktop.

Select an option

Save stephdl/fc72dcae7fdfb0fe5fe2e32111817200 to your computer and use it in GitHub Desktop.
Vue.js 2 & 3 - Guide Complet avec Explications

Vue.js 2 & 3 - Guide Complet avec Explications

Documentation approfondie avec explications claires et code commenté

Dernière mise à jour : Février 2026


📑 Table des matières

  1. Introduction & Mise en place
  2. Anatomie d'un composant
  3. Réactivité
  4. Propriétés calculées (Computed)
  5. Watchers
  6. Méthodes (Methods)
  7. Lifecycle Hooks
  8. Props & Events
  9. Composables vs Mixins
  10. Gestion d'état (Store)
  11. Routing (Vue Router)
  12. Organisation du projet
  13. Helpers & Utilities
  14. Directives personnalisées
  15. Plugins & Librairies
  16. Performance & Optimisation
  17. Testing
  18. Best Practices & Patterns
  19. Slots & Content Distribution
  20. Teleport & Suspense
  21. Render Functions & JSX
  22. Transitions & Animations
  23. Formulaires avancés
  24. Internationalisation (i18n)
  25. SSR & SSG
  26. API & Gestion des requêtes
  27. State Management avancé
  28. TypeScript avancé
  29. Development Tools
  30. Sécurité
  31. Accessibilité (a11y)
  32. Mobile & Progressive Web Apps
  33. Debugging & Troubleshooting
  34. Ecosystem & Intégrations
  35. Patterns de design
  36. Real-world Examples
  37. Annexes & Migration

1. Introduction & Mise en place

Vue.js - C'est quoi ?

Vue = Framework JavaScript pour créer des interfaces utilisateur réactives.

Réactif = Quand les données changent, l'interface se met à jour automatiquement.

🔄 Vue 2 vs Vue 3

Aspect Vue 2 Vue 3 Explication
Performance Baseline 55% plus rapide Meilleur algorithme de rendu
Taille 22kb 16kb Code plus léger
Réactivité Object.defineProperty Proxy Détecte tous les changements
API Options API Options + Composition Plus flexible
TypeScript Basique Excellent Meilleure intégration
Store recommandé Vuex Pinia Pinia plus simple et typé
Création app new Vue() createApp() Plus modulaire en Vue 3

Installation

Vue 3 avec Vite (recommandé)

# Créer un projet Vue 3
npm create vite@latest mon-projet -- --template vue-ts
cd mon-projet
npm install
npm run dev

Vue 2 avec Vue CLI (legacy)

# Créer un projet Vue 2
npm install -g @vue/cli
vue create mon-projet
cd mon-projet
npm run serve

Configuration de base (Vue 3)

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)) // @ = raccourci vers src/
    }
  },
  server: {
    port: 3000,
    open: true // Ouvre le navigateur auto
  }
})

Point d'entrée

// Vue 2 - main.js
import Vue from 'vue'
import App from './App.vue'

new Vue({
  render: h => h(App)
}).$mount('#app')

// Vue 3 - main.ts
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

2. Anatomie d'un composant

Composant = Bloc réutilisable avec HTML + JS + CSS

Options API (Vue 2 & 3)

<template>
  <div class="counter">
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
export default {
  name: 'Counter',
  
  // Props = données du parent
  props: {
    initialCount: {
      type: Number,
      default: 0
    }
  },
  
  // Data = état local (doit être une fonction !)
  data() {
    return {
      title: 'My Counter',
      count: this.initialCount
    }
  },
  
  // Computed = valeurs calculées avec cache
  computed: {
    doubleCount() {
      return this.count * 2 // Recalculé seulement si count change
    }
  },
  
  // Watch = réagir aux changements
  watch: {
    count(newVal, oldVal) {
      console.log(`Count: ${oldVal}${newVal}`)
    }
  },
  
  // Methods = fonctions
  methods: {
    increment() {
      this.count++
      this.$emit('update', this.count) // Envoyer event au parent
    }
  },
  
  // Lifecycle = hooks du cycle de vie
  mounted() {
    console.log('Composant monté dans le DOM')
  }
}
</script>

<style scoped>
/* scoped = styles seulement pour ce composant */
.counter {
  padding: 20px;
}
</style>

Composition API avec setup() (Vue 3)

<template>
  <div class="counter">
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
import { ref, computed, watch, onMounted } from 'vue'

export default {
  props: {
    initialCount: { type: Number, default: 0 }
  },
  
  setup(props, { emit }) {
    // ref() = donnée réactive (besoin de .value)
    const title = ref('My Counter')
    const count = ref(props.initialCount)
    
    // Computed
    const doubleCount = computed(() => count.value * 2)
    
    // Watch
    watch(count, (newVal, oldVal) => {
      console.log(`${oldVal}${newVal}`)
    })
    
    // Fonction
    const increment = () => {
      count.value++
      emit('update', count.value)
    }
    
    // Lifecycle
    onMounted(() => console.log('Mounted'))
    
    // Tout retourner pour le template
    return { title, count, doubleCount, increment }
  }
}
</script>

Script Setup (Vue 3 - Recommandé)

<template>
  <div class="counter">
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue'

// Props
const props = defineProps({
  initialCount: { type: Number, default: 0 }
})

// Emits
const emit = defineEmits(['update'])

// État (pas besoin de return !)
const title = ref('My Counter')
const count = ref(props.initialCount)

// Computed
const doubleCount = computed(() => count.value * 2)

// Watch
watch(count, (n, o) => console.log(`${o}${n}`))

// Fonction
const increment = () => {
  count.value++
  emit('update', count.value)
}

// Lifecycle
onMounted(() => console.log('Mounted'))
</script>

3. Réactivité

Réactivité = Les données changent → L'interface se met à jour automatiquement

Vue 2 - Limitations (Object.defineProperty)

Vue 2 utilise Object.defineProperty qui ne peut intercepter que les propriétés déjà déclarées au moment de l'initialisation.

// Vue 2
export default {
  data() {
    return {
      user: { name: 'John' },
      items: [1, 2, 3]
    }
  },
  methods: {
    update() {
      // ❌ Non réactif en Vue 2 - propriété inexistante au départ
      this.user.email = 'new@email.com'
      
      // ❌ Non réactif en Vue 2 - modification par index
      this.items[0] = 99
      
      // ❌ Non réactif en Vue 2 - suppression
      delete this.user.name
      
      // ✅ Solutions Vue 2
      this.$set(this.user, 'email', 'new@email.com') // Ajouter propriété réactive
      this.$set(this.items, 0, 99)                   // Modifier index réactif
      this.$delete(this.user, 'name')                // Supprimer propriété réactive
      this.items.push(4)                             // Méthodes de tableau OK
      this.items = [...this.items, 4]                // Remplacement OK
    }
  }
}

Vue 3 - Tout réactif (Proxy)

Vue 3 utilise Proxy qui intercepte toutes les opérations, y compris l'ajout et la suppression de propriétés.

// Vue 3
import { reactive, ref } from 'vue'

const user = reactive({ name: 'John' })
const items = ref([1, 2, 3])

// ✅ Tout fonctionne nativement
user.email = 'new@email.com' // Ajouter propriété → réactif
items.value[0] = 99          // Modifier index → réactif
delete user.name             // Supprimer → réactif
// Plus besoin de $set / $delete !

ref() vs reactive()

import { ref, reactive } from 'vue'

// ref() = Pour primitives ET objets
const count = ref(0)
const user = ref({ name: 'John' })

count.value++              // Besoin de .value dans <script>
user.value.name = 'Jane'   // Accès à l'objet via .value
user.value = { name: 'New' } // Remplacement de l'objet OK

// reactive() = Pour objets seulement
const state = reactive({
  count: 0,
  user: { name: 'John' }
})

state.count++            // Pas de .value dans <script>
state.user.name = 'Jane'
// ❌ Ne JAMAIS réassigner un reactive
// state = { count: 1 } → perd la réactivité !

Règle simple :

  • Primitives (number, string, boolean) → ref()
  • Objets que vous ne remplacerez pas entièrement → reactive()
  • Objets que vous pourriez remplacer → ref()

⚠️ Vue 2 : pas de ref() / reactive() natifs, tout passe par data().


4. Propriétés calculées (Computed)

Computed = Valeur calculée qui se met en cache. Recalculée seulement si les dépendances changent.

Différence avec method :

  • Computed : Cache le résultat ✅
  • Method : Recalculé à chaque rendu ❌

Vue 2 - Options API

export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe',
      cart: [
        { name: 'Item 1', price: 10, qty: 2 },
        { name: 'Item 2', price: 20, qty: 1 }
      ]
    }
  },
  computed: {
    // Simple
    fullName() {
      return `${this.firstName} ${this.lastName}`
    },
    // Avec logique
    totalPrice() {
      return this.cart.reduce((sum, item) => sum + item.price * item.qty, 0)
    },
    // Getter + Setter
    fullNameEditable: {
      get() {
        return `${this.firstName} ${this.lastName}`
      },
      set(value) {
        const [first, last] = value.split(' ')
        this.firstName = first
        this.lastName = last
      }
    }
  }
}

Vue 3 - Composition API

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')
const cart = ref([
  { name: 'Item 1', price: 10, qty: 2 },
  { name: 'Item 2', price: 20, qty: 1 }
])

// Simple
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// Avec logique
const totalPrice = computed(() => {
  return cart.value.reduce((sum, item) => sum + item.price * item.qty, 0)
})

// Getter + Setter
const fullNameEditable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value) => {
    const [first, last] = value.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

// Lecture
console.log(fullName.value) // 'John Doe'

// Écriture (appelle le setter)
fullNameEditable.value = 'Jane Smith'
// firstName = 'Jane', lastName = 'Smith'

5. Watchers

Watcher = Surveille une donnée et exécute du code quand elle change.

Différence avec computed :

  • Computed : Retourne une valeur calculée
  • Watcher : Exécute du code avec effets de bord (side effects)

Vue 2 - Options API

export default {
  data() {
    return {
      searchQuery: '',
      user: { name: 'John', age: 30 }
    }
  },
  watch: {
    // Watcher simple
    searchQuery(newVal, oldVal) {
      console.log(`Search: ${oldVal}${newVal}`)
      this.fetchResults(newVal)
    },
    // Avec options
    searchQuery: {
      handler(newVal) {
        this.fetchResults(newVal)
      },
      immediate: true // Exécuter au montage
    },
    // Watch profond (deep)
    user: {
      handler(newUser) {
        this.saveUser(newUser)
      },
      deep: true // Observer les changements profonds
    },
    // Watch d'un chemin précis
    'user.name'(newName) {
      console.log('Nom changé:', newName)
    }
  }
}

Vue 3 - Composition API

import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const user = ref({ name: 'John', age: 30 })

// watch() - Dépendances explicites
watch(searchQuery, (newVal, oldVal) => {
  console.log(`Search: ${oldVal}${newVal}`)
  fetchResults(newVal)
})

// Watch avec options
watch(searchQuery, (newVal) => {
  console.log('Query:', newVal)
}, {
  immediate: true, // Exécuter immédiatement
  deep: false
})

// Watch multiple sources
watch([firstName, lastName], ([newFirst, newLast]) => {
  console.log(`Name: ${newFirst} ${newLast}`)
})

// Deep watch
watch(user, (newUser) => {
  saveUser(newUser)
}, { deep: true })

// watchEffect() - Tracking automatique (Vue 3 seulement)
watchEffect(() => {
  // Tous les ref utilisés ici sont automatiquement observés
  console.log(`Query: ${searchQuery.value}`)
  console.log(`User: ${user.value.name}`)
})

// Avec cleanup (nettoyage avant prochain appel)
watch(searchQuery, async (newVal, oldVal, onCleanup) => {
  const controller = new AbortController()
  
  onCleanup(() => {
    controller.abort() // Annuler requête précédente
  })
  
  await fetch(`/api/search?q=${newVal}`, { signal: controller.signal })
})

⚠️ watchEffect est exclusif à Vue 3. En Vue 2, utilisez watch avec immediate: true et accédez aux propriétés manuellement.


6. Méthodes (Methods)

Methods = Fonctions réutilisables dans le composant.

Vue 2 - Options API

export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    },
    incrementBy(amount) {
      this.count += amount
    },
    async fetchData() {
      const res = await fetch('/api/data')
      return res.json()
    }
  }
}

Vue 3 - Composition API

import { ref } from 'vue'

const count = ref(0)

const increment = () => {
  count.value++
}

const incrementBy = (amount) => {
  count.value += amount
}

const fetchData = async () => {
  const res = await fetch('/api/data')
  return res.json()
}

Event Handlers (commun Vue 2 & 3)

<template>
  <!-- Appel sans paramètre -->
  <button @click="increment">+1</button>
  
  <!-- Avec paramètre -->
  <button @click="incrementBy(5)">+5</button>
  
  <!-- Accès à l'event natif -->
  <button @click="handleClick">Click</button>
  
  <!-- Event + paramètre -->
  <button @click="handleClick($event, 'extra')">Click</button>
  
  <!-- Expression inline -->
  <button @click="count++">+1</button>
  
  <!-- Modificateurs d'événement -->
  <form @submit.prevent="handleSubmit">...</form>
  <div @click.stop="handleClick">...</div>
  
  <!-- Touches clavier -->
  <input @keyup.enter="submit">
  <input @keyup.esc="cancel">
</template>

7. Lifecycle Hooks

Lifecycle = Moments clés du cycle de vie d'un composant.

Correspondance Vue 2 ↔ Vue 3

Vue 2 (Options API) Vue 3 (Options API) Vue 3 (Composition API) Quand
beforeCreate beforeCreate (top-level du setup) Avant init
created created (top-level du setup) Après init
beforeMount beforeMount onBeforeMount() Avant montage DOM
mounted mounted onMounted() Après montage DOM
beforeUpdate beforeUpdate onBeforeUpdate() Avant update
updated updated onUpdated() Après update
beforeDestroy beforeUnmount onBeforeUnmount() Avant destruction
destroyed unmounted onUnmounted() Après destruction
errorCaptured errorCaptured onErrorCaptured() Erreur enfant

⚠️ Attention à la migration : beforeDestroybeforeUnmount et destroyedunmounted. C'est une source fréquente de bug lors de la migration Vue 2 → Vue 3.

Vue 2 - Options API

export default {
  beforeCreate() {
    // this.$data et this.$props ne sont pas encore dispo
    console.log('Before create')
  },
  created() {
    // Données initialisées, pas de DOM
    console.log('Created - bon endroit pour fetch initial')
    this.fetchData()
  },
  mounted() {
    // DOM disponible
    console.log('Mounted - accès au DOM OK')
    this.$refs.myInput.focus()
  },
  beforeDestroy() {
    // Nettoyage
    clearInterval(this.timer)
  },
  destroyed() {
    console.log('Destroyed')
  }
}

Vue 3 - Composition API

import {
  onBeforeMount, onMounted,
  onBeforeUpdate, onUpdated,
  onBeforeUnmount, onUnmounted
} from 'vue'

// Code au top-level = équivalent de created()
console.log('Created')
fetchData()

onBeforeMount(() => console.log('Before mount'))

onMounted(() => {
  console.log('Mounted - DOM ready')
  // Setup event listeners
  // Fetch initial data
  // Init librairies tierces
})

onBeforeUpdate(() => console.log('Before update'))
onUpdated(() => console.log('Updated'))

onBeforeUnmount(() => {
  console.log('Before unmount - cleanup')
  clearInterval(timer)
})

onUnmounted(() => console.log('Unmounted'))

8. Props & Events

Communication Parent → Enfant → Parent

Vue utilise un flux de données unidirectionnel :

  • Parent → Enfant : Props (données descendantes)
  • Enfant → Parent : Events (événements montants)

Props (Parent → Enfant)

<!-- Parent.vue - Vue 2 & 3 -->
<template>
  <UserCard 
    :user="currentUser"
    :loading="isLoading"
    title="Mon Profil"
  />
</template>
<!-- UserCard.vue (Enfant) - Vue 2 -->
<script>
export default {
  props: {
    user: {
      type: Object,
      required: true
    },
    loading: {
      type: Boolean,
      default: false
    },
    title: {
      type: String,
      default: 'Utilisateur'
    }
  },
  created() {
    console.log(this.user.name)
  }
}
</script>
<!-- UserCard.vue (Enfant) - Vue 3 Script Setup -->
<script setup>
const props = defineProps({
  user: { type: Object, required: true },
  loading: { type: Boolean, default: false },
  title: { type: String, default: 'Utilisateur' }
})

console.log(props.user.name)
</script>

⚠️ Règle commune Vue 2 & Vue 3 : les props sont READ-ONLY

// ❌ MAUVAIS - Modifier directement la prop (Vue 2 & 3)
// this.initialCount++ (Vue 2) / props.initialCount++ (Vue 3)

// ✅ BON - Créer une copie locale
// Vue 2
data() {
  return { count: this.initialCount }
}

// Vue 3
const count = ref(props.initialCount)

TypeScript avec Props (Vue 3) :

<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
  role?: 'admin' | 'user'
}

interface Props {
  user: User
  loading?: boolean
  title?: string
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  title: 'Utilisateur'
})
</script>

Events (Enfant → Parent)

<!-- UserCard.vue - Vue 2 -->
<script>
export default {
  methods: {
    editUser() {
      this.$emit('update', { ...this.user, name: 'Nouveau nom' })
    },
    deleteUser() {
      this.$emit('delete', this.user.id)
    }
  }
}
</script>
<!-- UserCard.vue - Vue 3 Script Setup -->
<script setup>
const props = defineProps({ user: { type: Object, required: true } })
const emit = defineEmits(['update', 'delete'])

const editUser = () => emit('update', { ...props.user, name: 'Nouveau nom' })
const deleteUser = () => emit('delete', props.user.id)
</script>
<!-- Parent.vue - Vue 2 & 3 (syntaxe identique) -->
<template>
  <UserCard
    :user="currentUser"
    @update="handleUpdate"
    @delete="handleDelete"
  />
</template>

TypeScript avec Events (Vue 3) :

<script setup lang="ts">
interface Emits {
  (e: 'update', user: User): void
  (e: 'delete', id: number): void
  (e: 'save'): void
}

const emit = defineEmits<Emits>()
emit('update', user)  // ✅ TypeScript vérifie les types
// emit('delete', 'abc') // ❌ Erreur TypeScript
</script>

v-model (Binding bidirectionnel)

v-model Vue 2 : prop value + event input

<!-- Vue 2 - Composant compatible v-model -->
<script>
export default {
  model: {
    prop: 'value',    // Prop reçue
    event: 'input'    // Event émis
  },
  props: {
    value: String
  },
  methods: {
    update(e) {
      this.$emit('input', e.target.value)
    }
  }
}
</script>
<template>
  <input :value="value" @input="update">
</template>

<!-- Usage Vue 2 -->
<CustomInput v-model="message" />
<!-- Équivalent à : <CustomInput :value="message" @input="message = $event" /> -->

v-model Vue 3 : prop modelValue + event update:modelValue

<!-- Vue 3 - Composant compatible v-model -->
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])

const updateValue = (e) => emit('update:modelValue', e.target.value)
</script>
<template>
  <input :value="modelValue" @input="updateValue">
</template>

<!-- Usage Vue 3 -->
<CustomInput v-model="message" />
<!-- Équivalent à : <CustomInput :model-value="message" @update:model-value="message = $event" /> -->

Multiple v-model (Vue 3 uniquement)

<!-- Parent -->
<UserForm
  v-model:first-name="firstName"
  v-model:last-name="lastName"
/>

<!-- UserForm.vue - Vue 3 -->
<script setup>
const props = defineProps({ firstName: String, lastName: String })
const emit = defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input :value="firstName" @input="emit('update:firstName', $event.target.value)">
  <input :value="lastName" @input="emit('update:lastName', $event.target.value)">
</template>

⚠️ Le multiple v-model n'existe pas nativement en Vue 2 (il faut simuler avec des props + events séparés).

Provide / Inject (Éviter Props Drilling)

Problème : Props Drilling

<!-- ❌ Props Drilling -->
<GrandParent>
  <Parent :user="user">         ← Doit passer user
    <Child :user="user">         ← Doit passer user
      <GrandChild :user="user"/> ← Utilise finalement user
    </Child>
  </Parent>
</GrandParent>

Vue 2 - Provide/Inject

// GrandParent.vue - Vue 2
export default {
  provide() {
    return {
      // ⚠️ En Vue 2, les valeurs fournies NE SONT PAS réactives par défaut
      user: this.user,
      // Pour rendre réactif, passer l'instance Vue ou utiliser Vue.observable
      getUser: () => this.user // Workaround pour réactivité
    }
  },
  data() {
    return { user: { name: 'John' } }
  }
}

// GrandChild.vue - Vue 2
export default {
  inject: ['user', 'getUser']
}

Vue 3 - Provide/Inject réactif

// GrandParent.vue - Vue 3
import { provide, ref } from 'vue'

const user = ref({ name: 'John', role: 'admin' })
const theme = ref('dark')

// ✅ En Vue 3, les ref sont réactives via provide/inject
provide('user', user)
provide('theme', theme)

// GrandChild.vue - Vue 3
import { inject } from 'vue'

const user = inject('user')          // Ref réactive
const theme = inject('theme')        // Ref réactive
const config = inject('config', { apiUrl: '/api' }) // Valeur par défaut

⚠️ Différence clé : En Vue 2, les valeurs fournies via provide ne sont pas réactives automatiquement. En Vue 3, si vous fournissez un ref ou reactive, les changements se propagent automatiquement aux descendants.

Quand utiliser Provide/Inject :

  • ✅ Données de configuration (theme, langue, user...)
  • ✅ Quand les données traversent 3+ niveaux
  • ❌ Pour la communication directe parent-enfant (utiliser props)

9. Composables vs Mixins

La réutilisation de logique est l'un des points où Vue 2 et Vue 3 divergent le plus. Vue 2 utilisait les Mixins, Vue 3 introduit les Composables via la Composition API.

Mixins (Vue 2 - Legacy)

Un mixin est un objet qui contient des options Vue (data, methods, lifecycle hooks...) qui sont fusionnées dans le composant qui l'utilise.

Problèmes des Mixins :

  • Source des données opaque : on ne sait pas d'où vient this.loading
  • Conflits de noms : deux mixins avec data.loading → collision silencieuse
  • Couplage implicite : les mixins peuvent se référencer entre eux
// mixins/formMixin.js - Vue 2
export const formMixin = {
  data() {
    return {
      loading: false,
      errors: {}
    }
  },
  methods: {
    async submit(fn) {
      this.loading = true
      this.errors = {}
      try {
        await fn()
      } catch (e) {
        this.errors = e.errors || {}
      } finally {
        this.loading = false
      }
    }
  }
}

// MyForm.vue - Vue 2
import { formMixin } from '@/mixins/formMixin'

export default {
  mixins: [formMixin], // Fusion des options
  methods: {
    handleSubmit() {
      // this.loading vient du mixin — mais c'est pas évident à la lecture !
      this.submit(() => this.$api.save(this.formData))
    }
  }
}

Composables (Vue 3 - Recommandé)

Un composable est une fonction qui encapsule de la logique réactive. La source des données est explicite car le composable est importé et destructuré nommément.

Avantages des Composables :

  • Source explicite : on voit clairement d'où vient loading
  • Pas de conflits de noms : vous choisissez les noms à la destructuration
  • Composition claire : les composables peuvent s'utiliser entre eux proprement
// composables/useForm.js - Vue 3
import { ref } from 'vue'

export function useForm() {
  const loading = ref(false)
  const errors = ref({})

  const submit = async (fn) => {
    loading.value = true
    errors.value = {}
    try {
      await fn()
    } catch (e) {
      errors.value = e.errors || {}
    } finally {
      loading.value = false
    }
  }

  return { loading, errors, submit }
}

// MyForm.vue - Vue 3
import { useForm } from '@/composables/useForm'

const { loading, errors, submit } = useForm()
// loading vient clairement de useForm — explicite !

const handleSubmit = () => {
  submit(() => api.save(formData.value))
}

Comparaison côte à côte

Aspect Mixin (Vue 2) Composable (Vue 3)
Source des données Opaque (this.loading — d'où ?) Explicite (const { loading } = useForm())
Conflits de noms Risque élevé Aucun (vous renommez à la destructuration)
Réutilisabilité Limitée Excellente
TypeScript Difficile Natif
Debugging Compliqué Simple
// Renommer pour éviter les conflits - Vue 3
const { loading: formLoading, submit } = useForm()
const { loading: authLoading } = useAuth()
// Aucun conflit possible !

Composables utiles

// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const double = computed(() => count.value * 2)

  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => { count.value = initialValue }

  return { count, double, increment, decrement, reset }
}

// Usage
const { count, double, increment, reset } = useCounter(10)
// composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  const fetchData = async () => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  fetchData()

  return { data, error, loading, refetch: fetchData }
}

// Usage
const { data, error, loading } = useFetch('/api/users')
// composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const value = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })

  return value
}

// Usage
const settings = useLocalStorage('app-settings', { theme: 'dark', lang: 'fr' })
settings.value.theme = 'light' // Auto-sauvegardé !

⚠️ Les mixins restent supportés en Vue 3 pour la compatibilité, mais sont officiellement déconseillés. Préférez toujours les composables dans un projet Vue 3.


10. Gestion d'état (Store)

Pourquoi un Store ?

Quand plusieurs composants distants partagent les mêmes données (panier, utilisateur connecté, notifications...), les props et events deviennent insuffisants. Le store centralise l'état global.

Vuex vs Pinia

Aspect Vuex (Vue 2 & 3) Pinia (Vue 3 - recommandé)
Boilerplate Élevé (mutations, actions séparées) Minimal
TypeScript Difficile Excellent
Modules Complexes Stores séparés
DevTools Oui Oui
API commit pour mutations Modification directe
Taille Plus lourd Plus léger

Vuex (Vue 2 - et Vue 3 legacy)

// store/index.js - Vuex (Vue 2)
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // State = données globales
  state: {
    user: null,
    cart: []
  },
  
  // Getters = propriétés calculées du store
  getters: {
    isAuthenticated: (state) => !!state.user,
    cartTotal: (state) => state.cart.reduce((s, i) => s + i.price * i.qty, 0)
  },
  
  // Mutations = SEULE façon de modifier le state (synchrone)
  mutations: {
    SET_USER(state, user) {
      state.user = user
    },
    ADD_TO_CART(state, product) {
      const item = state.cart.find(i => i.id === product.id)
      if (item) {
        item.qty++
      } else {
        state.cart.push({ ...product, qty: 1 })
      }
    },
    CLEAR_CART(state) {
      state.cart = []
    }
  },
  
  // Actions = logique async, appelle des mutations
  actions: {
    async login({ commit }, { email, password }) {
      const res = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      })
      const data = await res.json()
      commit('SET_USER', data.user)
    },
    logout({ commit }) {
      commit('SET_USER', null)
      commit('CLEAR_CART')
    }
  }
})

// main.js - Vue 2
import store from './store'
new Vue({ store, render: h => h(App) }).$mount('#app')
// Usage dans composant - Vue 2
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['user', 'cart']),
    ...mapGetters(['isAuthenticated', 'cartTotal'])
  },
  methods: {
    ...mapActions(['login', 'logout']),
    addToCart(product) {
      this.$store.commit('ADD_TO_CART', product) // commit direct
    }
  }
}

Pinia (Vue 3 - Recommandé)

npm install pinia
// main.ts - Vue 3
import { createApp } from 'vue'
import { createPinia } from 'pinia'

const pinia = createPinia()
createApp(App).use(pinia).mount('#app')
// stores/cart.ts - Pinia
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // STATE
  const items = ref([])

  // GETTERS (computed)
  const totalItems = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )
  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  // ACTIONS (pas de mutations séparées !)
  const addItem = (product) => {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  const removeItem = (productId) => {
    const index = items.value.findIndex(i => i.id === productId)
    if (index > -1) items.value.splice(index, 1)
  }

  const clearCart = () => { items.value = [] }

  return { items, totalItems, totalPrice, addItem, removeItem, clearCart }
})
// stores/user.ts - Pinia
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))
  const loading = ref(false)

  const isAuthenticated = computed(() => !!user.value)
  const isAdmin = computed(() => user.value?.role === 'admin')
  const userName = computed(() => user.value?.name || 'Invité')

  const login = async (email, password) => {
    loading.value = true
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      })
      const data = await response.json()
      user.value = data.user
      token.value = data.token
      localStorage.setItem('token', data.token)
    } finally {
      loading.value = false
    }
  }

  const logout = () => {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return { user, token, loading, isAuthenticated, isAdmin, userName, login, logout }
})
<!-- Usage Pinia dans composant - Vue 3 -->
<script setup>
import { useCartStore } from '@/stores/cart'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const cartStore = useCartStore()
const userStore = useUserStore()

// ✅ storeToRefs préserve la réactivité
const { items, totalPrice } = storeToRefs(cartStore)
const { isAuthenticated, userName } = storeToRefs(userStore)

// ✅ Les actions n'ont pas besoin de storeToRefs
const { addItem, clearCart } = cartStore
const { login, logout } = userStore
</script>

<template>
  <div v-if="isAuthenticated">
    Bienvenue {{ userName }} — {{ totalPrice }} €
    <button @click="logout">Déconnexion</button>
  </div>
</template>

Communication entre Stores (Pinia)

// stores/orders.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useCartStore } from './cart'

export const useOrdersStore = defineStore('orders', () => {
  const orders = ref([])

  const createOrder = async () => {
    const userStore = useUserStore()
    const cartStore = useCartStore()

    if (!userStore.isAuthenticated) throw new Error('Vous devez être connecté')

    const order = {
      userId: userStore.user.id,
      items: cartStore.items,
      total: cartStore.totalPrice,
      date: new Date()
    }

    const response = await fetch('/api/orders', {
      method: 'POST',
      body: JSON.stringify(order)
    })
    const newOrder = await response.json()
    orders.value.push(newOrder)

    cartStore.clearCart() // Vider le panier après commande

    return newOrder
  }

  return { orders, createOrder }
})

Store vs Props/Events vs Provide/Inject

Méthode Cas d'usage Exemple
Props/Events Communication directe parent-enfant Parent passe nom à enfant
Provide/Inject Données à travers plusieurs niveaux Theme, config, user context
Store État global partagé par TOUTE l'app Panier, auth, notifications

11. Routing (Vue Router)

npm install vue-router@4  # Vue 3
npm install vue-router@3  # Vue 2

Configuration

Vue 2 avec Vue Router 3

// router/index.js - Vue 2
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter) // ← Obligatoire en Vue 2

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/users/:id',
    name: 'user',
    component: () => import('@/views/User.vue'),
    props: true
  },
  {
    path: '/admin',
    component: () => import('@/layouts/Admin.vue'),
    children: [
      { path: '', component: () => import('@/views/Dashboard.vue') },
      { path: 'users', component: () => import('@/views/AdminUsers.vue') }
    ],
    meta: { requiresAuth: true }
  },
  {
    path: '*', // ← Vue Router 3 (Vue 2)
    component: () => import('@/views/NotFound.vue')
  }
]

const router = new VueRouter({ // ← new VueRouter() en Vue 2
  mode: 'history',             // ← mode: 'history' en Vue 2
  routes
})

// Navigation guard - Vue 2
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !store.getters.isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

export default router

// main.js - Vue 2
new Vue({ router, render: h => h(App) }).$mount('#app')

Vue 3 avec Vue Router 4

// router/index.ts - Vue 3
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/users/:id',
    name: 'user',
    component: () => import('@/views/User.vue'),
    props: true
  },
  {
    path: '/admin',
    component: () => import('@/layouts/Admin.vue'),
    children: [
      { path: '', component: () => import('@/views/Dashboard.vue') },
      { path: 'users', component: () => import('@/views/AdminUsers.vue') }
    ],
    meta: { requiresAuth: true }
  },
  {
    path: '/:pathMatch(.*)*', // ← Vue Router 4 (Vue 3) - syntaxe différente !
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({ // ← createRouter() en Vue 3
  history: createWebHistory(), // ← history: createWebHistory() en Vue 3
  routes
})

// Navigation guard - Vue 3 (même API, mais useStore → Pinia)
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

export default router

// main.ts - Vue 3
createApp(App).use(router).mount('#app')

Tableau des différences Vue Router 3 vs 4

Aspect Vue Router 3 (Vue 2) Vue Router 4 (Vue 3)
Création new VueRouter({}) createRouter({})
Historique mode: 'history' history: createWebHistory()
Route 404 path: '*' path: '/:pathMatch(.*)*'
Installation Vue.use(VueRouter) .use(router) dans createApp
this.$router ✅ Options API ✅ Options API seulement
useRouter() ❌ Non disponible ✅ Composition API
useRoute() ❌ Non disponible ✅ Composition API

Navigation

<template>
  <!-- Identique Vue 2 & 3 -->
  <router-link to="/">Home</router-link>
  <router-link :to="{ name: 'user', params: { id: 123 } }">User 123</router-link>
  <router-view />
</template>

<script>
// Vue 2 - Options API
export default {
  methods: {
    goToUser(id) {
      this.$router.push({ name: 'user', params: { id } })
    },
    goBack() {
      this.$router.back()
    }
  },
  computed: {
    userId() {
      return this.$route.params.id
    },
    searchQuery() {
      return this.$route.query.search
    }
  }
}
</script>
<script setup>
// Vue 3 - Composition API
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

const goToUser = (id) => {
  router.push({ name: 'user', params: { id } })
}

const goBack = () => router.back()

// Accès aux params/query
console.log(route.params.id)     // Paramètre dynamique
console.log(route.query.search)  // Query string
</script>

⚠️ useRouter() et useRoute() sont exclusifs à Vue 3. En Vue 2, utilisez toujours this.$router et this.$route depuis les Options API.


12. Organisation du projet

Structure complète et professionnelle

mon-projet/
├── public/
│   ├── favicon.ico
│   ├── robots.txt
│   └── manifest.json
│
├── src/
│   ├── api/                         # Appels API organisés par domaine
│   │   ├── index.ts                # Configuration axios/fetch
│   │   ├── interceptors.ts
│   │   ├── auth.api.ts
│   │   ├── users.api.ts
│   │   └── products.api.ts
│   │
│   ├── assets/
│   │   ├── images/
│   │   ├── styles/
│   │   │   ├── main.css
│   │   │   ├── variables.css
│   │   │   ├── reset.css
│   │   │   └── utilities.css
│   │   └── fonts/
│   │
│   ├── components/
│   │   ├── base/                   # Design System (BaseButton, BaseInput...)
│   │   ├── layout/                 # AppHeader, AppFooter, AppSidebar...
│   │   ├── forms/                  # FormInput, FormSelect...
│   │   └── features/               # Composants métier (user/, product/, cart/)
│   │
│   ├── composables/                 # Vue 3 uniquement
│   │   ├── core/                   # useToggle, useCounter, usePagination
│   │   ├── dom/                    # useClickOutside, useScroll, useBreakpoints
│   │   ├── data/                   # useFetch, useLocalStorage, useDebounce
│   │   └── business/               # useAuth, useCart, useNotifications
│   │
│   ├── config/
│   │   ├── constants.ts
│   │   └── env.ts
│   │
│   ├── directives/
│   ├── layouts/                     # DefaultLayout, AuthLayout, DashboardLayout
│   ├── locales/                     # en/, fr/
│   ├── middleware/                  # auth.ts, guest.ts, admin.ts
│   ├── plugins/                     # i18n, pinia/vuex, router, axios
│   ├── router/
│   │   ├── index.ts
│   │   ├── routes/
│   │   └── guards/
│   │
│   ├── stores/                      # Pinia (Vue 3) ou Vuex (Vue 2)
│   ├── types/                       # TypeScript (Vue 3)
│   │   ├── models/
│   │   └── api/
│   │
│   ├── utils/
│   │   ├── formatters/
│   │   ├── validators/
│   │   └── helpers/
│   │
│   ├── views/                       # Pages
│   │   ├── auth/
│   │   ├── dashboard/
│   │   ├── users/
│   │   └── products/
│   │
│   ├── App.vue
│   └── main.ts
│
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
│
├── .env
├── .env.development
├── .env.production
├── .eslintrc.js
├── .prettierrc.js
├── package.json
├── tsconfig.json
└── vite.config.ts

Bonnes pratiques de nommage

  • Composants : PascalCase.vue (UserCard.vue)
  • Composables : camelCase.ts avec préfixe use (useAuth.ts) — Vue 3 uniquement
  • Utilitaires : camelCase.ts (formatDate.ts)
  • Types : PascalCase.ts (User.ts)

Index.ts pour exports propres

// composables/index.ts
export { useAuth } from './business/useAuth'
export { useFetch } from './data/useFetch'
export { useToggle } from './core/useToggle'

// Import simplifié
import { useAuth, useFetch } from '@/composables'

13. Helpers & Utilities

Formatters

// utils/formatters.ts

export const formatDate = (date: Date) =>
  new Intl.DateTimeFormat('fr-FR').format(date)

export const formatCurrency = (value: number) =>
  new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value)

export const formatNumber = (value: number) =>
  new Intl.NumberFormat('fr-FR').format(value)

export const truncate = (str: string, length: number) =>
  str.length > length ? str.slice(0, length) + '...' : str

Validators

// utils/validators.ts

export const isEmail = (email: string) =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)

export const isPhone = (phone: string) =>
  /^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/.test(phone)

export const isStrongPassword = (password: string) =>
  /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/.test(password)

Debounce

// utils/debounce.ts

export function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: ReturnType<typeof setTimeout>
  return function(...args: Parameters<T>) {
    clearTimeout(timeout)
    timeout = setTimeout(() => func(...args), wait)
  }
}

// Usage
const debouncedSearch = debounce((query: string) => {
  fetchResults(query)
}, 300)

14. Directives personnalisées

Directive = Attribut spécial qui modifie le comportement d'un élément DOM.

⚠️ Différence majeure : les noms des hooks ont changé

Hook Vue 2 Hook Vue 3 Quand
bind created Avant que le composant soit monté
inserted mounted Après montage dans le DOM
update updated Après mise à jour du composant
componentUpdated (fusionné dans updated)
unbind unmounted Après démontage

v-focus

// Vue 2
export const focus = {
  inserted(el) {
    el.focus()
  }
}

// Vue 3
import { Directive } from 'vue'

export const vFocus: Directive = {
  mounted(el) {
    el.focus()
  }
}

// Usage : <input v-focus>

v-click-outside

// Vue 2
export const clickOutside = {
  bind(el, binding) {
    el.clickOutsideEvent = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unbind(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  }
}

// Vue 3
export const vClickOutside: Directive = {
  mounted(el, binding) {
    el.clickOutsideEvent = (event: Event) => {
      if (!(el === event.target || el.contains(event.target as Node))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el.clickOutsideEvent)
  },
  unmounted(el) {
    document.removeEventListener('click', el.clickOutsideEvent)
  }
}

// Usage : <div v-click-outside="closeModal">...</div>

Enregistrement

// Vue 2 - main.js
import { focus, clickOutside } from './directives'
Vue.directive('focus', focus)
Vue.directive('click-outside', clickOutside)

// Vue 3 - main.ts
import { vFocus } from './directives/v-focus'
import { vClickOutside } from './directives/v-click-outside'
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)

15. Plugins & Librairies

Element Plus (Vue 3) / Element UI (Vue 2)

// Vue 3 - Element Plus
npm install element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)

// Vue 2 - Element UI
npm install element-ui
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)

VeeValidate (Form Validation)

npm install vee-validate yup
<script setup>
import { useForm, useField } from 'vee-validate'
import * as yup from 'yup'

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8)
})

const { handleSubmit, errors } = useForm({ validationSchema: schema })
const { value: email } = useField('email')
const { value: password } = useField('password')

const onSubmit = handleSubmit((values) => console.log('Submitted:', values))
</script>

<template>
  <form @submit="onSubmit">
    <input v-model="email" type="email">
    <span class="error">{{ errors.email }}</span>
    <input v-model="password" type="password">
    <span class="error">{{ errors.password }}</span>
    <button type="submit">Submit</button>
  </form>
</template>

16. Performance & Optimisation

Lazy Loading

// Composants - Vue 3
const HeavyComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)

// Composants - Vue 2
const HeavyComponent = () => import('./components/HeavyComponent.vue')

// Routes (identique Vue 2 & 3)
{
  path: '/admin',
  component: () => import('@/views/Admin.vue')
}

v-memo (Vue 3 uniquement)

v-memo est une directive exclusive à Vue 3 (Vue 2 n'a pas d'équivalent natif). Elle mémorise le rendu d'un sous-arbre et le re-rend seulement si les dépendances listées changent.

<template>
  <!-- Re-render SEULEMENT si item.selected change -->
  <div v-for="item in items" :key="item.id" v-memo="[item.selected]">
    {{ item.name }}
  </div>
</template>

⚠️ En Vue 2, optimiser les listes coûteuses revient à utiliser Object.freeze() ou à fragmenter les composants.

KeepAlive (Vue 2 & 3)

KeepAlive = Cache les composants inactifs (garde l'état).

<!-- Vue 2 & 3 - Syntaxe identique -->
<template>
  <KeepAlive :max="10">
    <component :is="currentView" />
  </KeepAlive>
</template>

shallowRef / shallowReactive (Vue 3)

// Vue 3 uniquement
import { shallowRef, shallowReactive } from 'vue'

// shallowRef = réactivité de surface (le .value est réactif, pas les props internes)
const bigObject = shallowRef({ deeply: { nested: { data: 'value' } } })

// shallowReactive = réactivité de surface (les props de 1er niveau seulement)
const state = shallowReactive({ count: 0, user: { name: 'John' } })
// state.count++ → réactif ✅
// state.user.name = '...' → NON réactif ❌

17. Testing

npm install -D vitest @vue/test-utils jsdom
// Counter.spec.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from '@/components/Counter.vue'

describe('Counter', () => {
  it('renders count', () => {
    const wrapper = mount(Counter)
    expect(wrapper.text()).toContain('0')
  })

  it('increments on click', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('1')
  })

  it('emits update event', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted()).toHaveProperty('update')
  })
})

Tester un composable (Vue 3)

// composables/useCounter.spec.ts
import { useCounter } from '@/composables/useCounter'

describe('useCounter', () => {
  it('increments', () => {
    const { count, increment } = useCounter(0)
    increment()
    expect(count.value).toBe(1)
  })
})

18. Best Practices & Patterns

Conventions de nommage

  • Composants : PascalCase (UserProfile.vue)
  • Composables : camelCase + use (useAuth.ts) — Vue 3 uniquement
  • Props : camelCase dans le script, kebab-case dans le template
  • Events : kebab-case (@update-user)

Patterns recommandés

// ✅ Bon : Composables plutôt que mixins (Vue 3)
export function useForm() { /* logique réutilisable */ }

// ❌ Éviter : Mixins (Vue 2 legacy, déconseillé en Vue 3)
const formMixin = { data() { /* ... */ } }

// ✅ Bon : Provide/Inject pour données profondes
provide('theme', theme)

// ❌ Éviter : Props drilling
// <A :theme> → <B :theme> → <C :theme> → <D :theme>

19. Slots & Content Distribution

Slots = Passer du contenu HTML au composant enfant.

⚠️ Différence de syntaxe Vue 2 vs Vue 3

La syntaxe des slots a été unifiée en Vue 3. Vue 2 avait deux syntaxes (ancienne et nouvelle). Vue 3 n'accepte que la nouvelle.

Syntaxe Vue 2 Vue 3
Slot nommé (template) slot="header" ou v-slot:header v-slot:header ou #header
Slot scopé slot-scope="props" ou v-slot:default="props" v-slot:default="props" ou #default="props"

Slots nommés

<!-- BaseCard.vue - Identique Vue 2 & 3 (nouvelle syntaxe) -->
<template>
  <div class="card">
    <header><slot name="header">Default Header</slot></header>
    <main><slot>Default Content</slot></main>
    <footer><slot name="footer"></slot></footer>
  </div>
</template>

<!-- Usage - Syntaxe ancienne Vue 2 (déconseillée) -->
<BaseCard>
  <template slot="header"><h1>Custom Header</h1></template>
  <p>Main content</p>
  <template slot="footer"><button>Action</button></template>
</BaseCard>

<!-- Usage - Syntaxe moderne (Vue 2.6+ et Vue 3) -->
<BaseCard>
  <template #header><h1>Custom Header</h1></template>
  <p>Main content</p>
  <template #footer><button>Action</button></template>
</BaseCard>

Scoped Slots

<!-- TodoList.vue - Identique Vue 2 & 3 (nouvelle syntaxe) -->
<template>
  <ul>
    <li v-for="(todo, index) in todos" :key="todo.id">
      <slot :todo="todo" :index="index">
        {{ todo.text }}
      </slot>
    </li>
  </ul>
</template>

<!-- Usage - Syntaxe ancienne Vue 2 (déconseillée) -->
<TodoList :todos="todos">
  <template slot-scope="{ todo, index }">
    <span>{{ index + 1 }}. {{ todo.text }}</span>
  </template>
</TodoList>

<!-- Usage - Syntaxe moderne (Vue 2.6+ et Vue 3) -->
<TodoList :todos="todos">
  <template #default="{ todo, index }">
    <span>{{ index + 1 }}. {{ todo.text }}</span>
    <button @click="deleteTodo(todo.id)">Delete</button>
  </template>
</TodoList>

⚠️ Si vous lisez du code Vue 2 avec slot="..." et slot-scope="...", c'est l'ancienne syntaxe. Elle n'existe plus en Vue 3. Préférez toujours v-slot / #.


20. Teleport & Suspense

Teleport (Vue 3 uniquement)

Teleport = Rendre du contenu ailleurs dans le DOM (ex: modales, tooltips).

⚠️ Pas d'équivalent natif en Vue 2. En Vue 2, il faut utiliser la librairie tierce portal-vue.

# Vue 2 - Librairie tierce nécessaire
npm install portal-vue

# Vue 2 - main.js
import PortalVue from 'portal-vue'
Vue.use(PortalVue)
<!-- Vue 2 avec portal-vue -->
<portal to="body-end">
  <div v-if="showModal" class="modal">
    <button @click="showModal = false">Close</button>
  </div>
</portal>

<!-- Dans App.vue Vue 2 -->
<portal-target name="body-end" />
<!-- Vue 3 - Teleport natif -->
<template>
  <button @click="showModal = true">Open Modal</button>

  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <h2>Modal Title</h2>
      <button @click="showModal = false">Close</button>
    </div>
  </Teleport>
</template>

Suspense (Vue 3 uniquement)

Suspense = Gérer le chargement de composants asynchrones avec un état de fallback.

⚠️ Pas d'équivalent natif en Vue 2. En Vue 2, gérez le loading manuellement avec v-if/v-else.

<!-- Vue 2 - Gestion manuelle du loading -->
<template>
  <div v-if="loading"><LoadingSpinner /></div>
  <AsyncComponent v-else />
</template>

<script>
export default {
  components: {
    AsyncComponent: () => import('./AsyncComponent.vue')
  },
  data() { return { loading: true } },
  async created() {
    await this.loadData()
    this.loading = false
  }
}
</script>
<!-- Vue 3 - Suspense natif -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

<!-- AsyncComponent.vue - Vue 3 -->
<script setup>
// await au top level autorisé grâce à Suspense
const data = await fetch('/api/data').then(r => r.json())
</script>

21. Render Functions & JSX

Render Functions = Créer des templates en JavaScript pur. Utile pour des composants très dynamiques.

⚠️ Signature de h() différente entre Vue 2 et Vue 3

// Vue 2 - h reçu en paramètre, options imbriquées
export default {
  render(h) {                        // ← h en paramètre
    return h('div',
      {
        class: 'container',
        attrs: { id: 'app' },        // ← attrs séparé
        on: { click: this.onClick }, // ← on séparé
        domProps: { innerHTML: '...' }
      },
      [
        h('h1', 'Hello'),
        h('p', 'World')
      ]
    )
  }
}

// Vue 3 - h importé, options aplaties
import { h } from 'vue'             // ← h importé

export default {
  render() {
    return h('div',
      {
        class: 'container',
        id: 'app',                   // ← pas de attrs: {}
        onClick: this.onClick        // ← camelCase directement
      },
      [
        h('h1', 'Hello'),
        h('p', 'World')
      ]
    )
  }
}

JSX

<!-- Vue 2 - JSX (avec plugin babel) -->
<script>
export default {
  render() {
    return (
      <div class="container">
        <h1>Hello</h1>
        <p onClick={this.handleClick}>World</p>
      </div>
    )
  }
}
</script>

<!-- Vue 3 - JSX (avec @vitejs/plugin-vue-jsx) -->
<script setup lang="tsx">
const handleClick = () => console.log('clicked')

const MyComponent = () => (
  <div class="container">
    <h1>Hello</h1>
    <p onClick={handleClick}>World</p>
  </div>
)
</script>

22. Transitions & Animations

⚠️ Changement de nom de classes CSS entre Vue 2 et Vue 3

C'est un piège fréquent lors de la migration. Les classes d'entrée ont été renommées pour plus de clarté.

Phase Vue 2 Vue 3
Début de l'entrée v-enter v-enter-from ← renommé
Pendant l'entrée v-enter-active v-enter-active
Fin de l'entrée v-enter-to v-enter-to
Début de la sortie v-leave v-leave-from ← renommé
Pendant la sortie v-leave-active v-leave-active
Fin de la sortie v-leave-to v-leave-to
<!-- Vue 2 - CSS de transition -->
<style>
.fade-enter,        /* ← Vue 2 */
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
</style>

<!-- Vue 3 - CSS de transition -->
<style>
.fade-enter-from,   /* ← Vue 3 : renommé de v-enter */
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
</style>
<!-- Template - Identique Vue 2 & 3 -->
<template>
  <Transition name="fade">
    <div v-if="show">Content</div>
  </Transition>

  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </TransitionGroup>
</template>

⚠️ Si vous migrez de Vue 2 à Vue 3 et que vos transitions ne fonctionnent plus, vérifiez en premier lieu le renommage .v-enter.v-enter-from.


23. Formulaires avancés

<!-- Vue 2 -->
<script>
export default {
  data() {
    return {
      form: {
        name: '',
        email: '',
        age: null,
        country: '',
        newsletter: false
      }
    }
  },
  computed: {
    isValid() {
      return this.form.name && this.form.email && this.form.country
    }
  },
  methods: {
    handleSubmit() {
      if (this.isValid) console.log('Submitted:', this.form)
    }
  }
}
</script>
<!-- Vue 3 -->
<script setup lang="ts">
import { reactive, computed } from 'vue'

const form = reactive({
  name: '',
  email: '',
  age: null as number | null,
  country: '',
  newsletter: false
})

const isValid = computed(() => form.name && form.email && form.country)

const handleSubmit = () => {
  if (isValid.value) console.log('Submitted:', form)
}
</script>
<!-- Template - Identique Vue 2 & 3 -->
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.name" required>
    <input v-model="form.email" type="email" required>
    <input v-model.number="form.age" type="number">
    <select v-model="form.country" required>
      <option value="">Select...</option>
      <option value="FR">France</option>
      <option value="US">USA</option>
    </select>
    <input type="checkbox" v-model="form.newsletter">
    <button type="submit" :disabled="!isValid">Submit</button>
  </form>
</template>

24. Internationalisation (i18n)

npm install vue-i18n@9  # Vue 3
npm install vue-i18n@8  # Vue 2
// plugins/i18n.ts - Vue 3 (vue-i18n v9)
import { createI18n } from 'vue-i18n'

export default createI18n({
  locale: 'fr',
  fallbackLocale: 'en',
  messages: {
    en: { welcome: 'Welcome', hello: 'Hello {name}' },
    fr: { welcome: 'Bienvenue', hello: 'Bonjour {name}' }
  }
})

// main.ts - Vue 3
import i18n from './plugins/i18n'
createApp(App).use(i18n).mount('#app')
// plugins/i18n.js - Vue 2 (vue-i18n v8)
import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

export default new VueI18n({
  locale: 'fr',
  fallbackLocale: 'en',
  messages: {
    en: { welcome: 'Welcome', hello: 'Hello {name}' },
    fr: { welcome: 'Bienvenue', hello: 'Bonjour {name}' }
  }
})
<!-- Usage Vue 3 -->
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
</script>

<template>
  <p>{{ $t('welcome') }}</p>
  <p>{{ $t('hello', { name: 'John' }) }}</p>
  <button @click="locale = 'en'">EN</button>
  <button @click="locale = 'fr'">FR</button>
</template>

<!-- Usage Vue 2 - $t disponible globalement via this -->
<template>
  <p>{{ $t('welcome') }}</p>
  <button @click="$i18n.locale = 'en'">EN</button>
</template>

25. SSR & SSG

Nuxt.js (Framework SSR/SSG)

# Nuxt 3 (pour Vue 3)
npx nuxi init my-nuxt-app

# Nuxt 2 (pour Vue 2, legacy)
npx create-nuxt-app my-nuxt-app

Structure Nuxt 3

my-nuxt-app/
├── pages/              # Routes auto-générées
│   ├── index.vue      # → /
│   └── about.vue      # → /about
├── components/
├── composables/
├── layouts/
└── nuxt.config.ts

26. API & Gestion des requêtes

// services/api.ts - Vue 2 & 3
import axios from 'axios'

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL, // Vite (Vue 3)
  // baseURL: process.env.VUE_APP_API_URL, // Vue CLI (Vue 2)
  timeout: 10000
})

// Request interceptor (ajouter token)
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// Response interceptor (gérer erreurs)
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default apiClient
// services/user.service.ts
import api from './api'

export const userService = {
  getAll: () => api.get('/users'),
  getById: (id: number) => api.get(`/users/${id}`),
  create: (data: any) => api.post('/users', data),
  update: (id: number, data: any) => api.put(`/users/${id}`, data),
  delete: (id: number) => api.delete(`/users/${id}`)
}

27. State Management avancé

Vuex vs Pinia - Tableau comparatif complet

Feature Vuex 4 (Vue 2 & 3) Pinia (Vue 3)
Mutations séparées ✅ Obligatoires ❌ Pas nécessaires
Modification directe ❌ Interdit ✅ Autorisé
Modules Complexes (namespaced) Stores séparés
Composition API Partiel Natif
TypeScript Difficile Excellent
Devtools
$patch

Pinia Plugins

// Plugin logger
export function piniaLogger({ store }) {
  store.$subscribe((mutation, state) => {
    console.log(`[${store.$id}]`, mutation.type, state)
  })
}

// Plugin persistence
export function piniaPersistence({ store }) {
  const stored = localStorage.getItem(store.$id)
  if (stored) store.$patch(JSON.parse(stored))

  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

// main.ts
const pinia = createPinia()
pinia.use(piniaLogger)
pinia.use(piniaPersistence)

Vuex Modules (Vue 2 - structure courante)

// store/modules/cart.js - Vuex module
const cartModule = {
  namespaced: true,
  state: () => ({ items: [] }),
  getters: {
    total: (state) => state.items.reduce((s, i) => s + i.price, 0)
  },
  mutations: {
    ADD_ITEM(state, item) { state.items.push(item) }
  },
  actions: {
    addItem({ commit }, item) { commit('ADD_ITEM', item) }
  }
}

// store/index.js
export default new Vuex.Store({
  modules: { cart: cartModule, user: userModule }
})

// Usage dans composant Vue 2
this.$store.dispatch('cart/addItem', product)
this.$store.getters['cart/total']

28. TypeScript avancé

type vs interface — Quelle différence ?

Les deux permettent de décrire la forme d'un objet. La différence principale :

  • interface est extensible (on peut la redéclarer pour l'augmenter)
  • type est plus expressif (unions, intersections, types conditionnels)
// interface — recommandé pour les modèles de données
interface User {
  id: number
  name: string
}

// Extensible : on peut ajouter des propriétés ailleurs
interface User {
  email: string // Fusionne avec la déclaration précédente ✅
}

// type — recommandé pour les unions et alias complexes
type Status = 'pending' | 'active' | 'banned'
type ID = string | number
type UserOrAdmin = User | Admin

// ❌ type ne peut pas être redéclaré
// type User = { email: string } // Erreur : duplicate identifier

Règle pratique :

  • Modèles de données, props, objets → interface
  • Unions (A | B), intersections (A & B), alias de primitives → type

Typer ref() et reactive()

import { ref, reactive } from 'vue'

// ref() — inféré automatiquement si valeur initiale connue
const count = ref(0)          // Ref<number>
const name = ref('John')      // Ref<string>
const active = ref(false)     // Ref<boolean>

// ref() — type explicite nécessaire si valeur initiale null/undefined
const user = ref<User | null>(null)   // ✅ Ref<User | null>
const items = ref<string[]>([])       // ✅ Ref<string[]>
const error = ref<Error | null>(null) // ✅ Ref<Error | null>

// Accès
user.value?.name   // string | undefined (safe navigation)
items.value.push('hello')

// reactive() — inféré automatiquement
const state = reactive({
  count: 0,           // number
  user: null as User | null,  // User | null
  tags: [] as string[]        // string[]
})

// reactive() — type explicite avec interface
interface AppState {
  count: number
  user: User | null
  tags: string[]
}

const state = reactive<AppState>({
  count: 0,
  user: null,
  tags: []
})

Typer computed()

import { ref, computed } from 'vue'
import type { ComputedRef } from 'vue'

const price = ref(100)
const quantity = ref(3)

// Inféré automatiquement — TypeScript déduit le type de retour
const total = computed(() => price.value * quantity.value) // ComputedRef<number>

// Type explicite — utile si le type de retour est complexe
const fullName = computed<string>(() => `${firstName.value} ${lastName.value}`)

// Computed avec getter + setter — typage des deux
const doubled = computed<number>({
  get: () => price.value * 2,
  set: (val: number) => { price.value = val / 2 }
})

// Type exportable si besoin
const cartSummary: ComputedRef<{ total: number; count: number }> = computed(() => ({
  total: items.value.reduce((s, i) => s + i.price, 0),
  count: items.value.length
}))

Typer watch() et watchEffect()

import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const user = ref<User | null>(null)

// watch simple — types inférés des arguments
watch(count, (newVal: number, oldVal: number) => {
  console.log(newVal, oldVal)
})

// watch d'une valeur nullable — newVal peut être null
watch(user, (newUser: User | null) => {
  if (newUser) {
    console.log(newUser.name) // TypeScript sait que newUser est User ici
  }
})

// watch multiple — tableau de tuples
watch([count, user], ([newCount, newUser]: [number, User | null]) => {
  console.log(newCount, newUser)
})

// watchEffect — pas de typage particulier, tout est inféré
watchEffect(() => {
  // TypeScript suit les types des ref utilisées automatiquement
  if (user.value) {
    console.log(user.value.name) // string
  }
})

Interfaces et types de modèles

// types/models.ts

// Modèle de base
export interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
  createdAt: Date
}

// Enums — pour des valeurs fixes et nommées
export enum UserRole {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest'
}

export enum HttpStatus {
  OK = 200,
  Created = 201,
  NotFound = 404,
  ServerError = 500
}

// Union de littéraux — alternative légère aux enums
export type Theme = 'light' | 'dark' | 'system'
export type SortOrder = 'asc' | 'desc'
export type ID = string | number

Utility Types — les plus utiles

TypeScript fournit des types utilitaires intégrés pour transformer vos interfaces.

interface User {
  id: number
  name: string
  email: string
  password: string
  role: 'admin' | 'user'
}

// Partial<T> — toutes les propriétés deviennent optionnelles
type UpdateUserDTO = Partial<User>
// { id?: number, name?: string, email?: string, ... }

// Required<T> — toutes les propriétés deviennent obligatoires
type StrictUser = Required<Partial<User>>

// Omit<T, K> — exclure des propriétés
type CreateUserDTO = Omit<User, 'id'>                  // Sans id
type PublicUser = Omit<User, 'password' | 'role'>      // Sans password ni role

// Pick<T, K> — ne garder que certaines propriétés
type UserCredentials = Pick<User, 'email' | 'password'>
type UserPreview = Pick<User, 'id' | 'name'>

// Record<K, V> — objet avec clés et valeurs typées
type UserMap = Record<number, User>          // { [id: number]: User }
type Translations = Record<string, string>   // { [key: string]: string }

// Readonly<T> — toutes les propriétés en lecture seule
type ImmutableUser = Readonly<User>

// ReturnType<T> — type de retour d'une fonction
const getUser = () => ({ id: 1, name: 'John' })
type UserReturn = ReturnType<typeof getUser> // { id: number, name: string }

// Array<T> — équivalent à T[]
type Users = Array<User>     // User[]
type IDs = Array<number>     // number[]

// Nullable<T> — pattern courant (pas natif, à définir)
type Nullable<T> = T | null
type MaybeUser = Nullable<User>  // User | null

Typer les réponses API avec des Generics

// types/api.ts

// Réponse API générique
export interface ApiResponse<T> {
  data: T
  message: string
  status: number
  success: boolean
}

// Liste paginée
export interface PaginatedResponse<T> {
  data: T[]
  pagination: {
    page: number
    perPage: number
    total: number
    totalPages: number
  }
}

// Erreur API
export interface ApiError {
  message: string
  errors?: Record<string, string[]>
  code?: string
}

// Usage dans un service
import type { ApiResponse, PaginatedResponse, ApiError } from '@/types/api'
import type { User } from '@/types/models'

export const userService = {
  getById: async (id: number): Promise<ApiResponse<User>> => {
    const res = await api.get(`/users/${id}`)
    return res.data
  },

  getAll: async (page = 1): Promise<PaginatedResponse<User>> => {
    const res = await api.get('/users', { params: { page } })
    return res.data
  }
}

// Usage dans un composable
const fetchUser = async (id: number) => {
  try {
    const response = await userService.getById(id)
    user.value = response.data       // TypeScript sait que c'est un User
    console.log(response.status)     // number
  } catch (e) {
    const err = e as ApiError
    console.log(err.message)         // string
  }
}

Typer les Props et Emits (rappel complet)

<script setup lang="ts">
import type { User } from '@/types/models'

// Props avec interface
interface Props {
  user: User
  loading?: boolean
  title?: string
  items?: string[]
  onSelect?: (id: number) => void // Callback typé
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  title: 'Utilisateur',
  items: () => [] // ⚠️ Valeurs par défaut pour tableaux/objets : utiliser une factory
})

// Emits avec interface
interface Emits {
  (e: 'update', user: User): void
  (e: 'delete', id: number): void
  (e: 'save'): void
  (e: 'error', message: string): void
}

const emit = defineEmits<Emits>()

// TypeScript vérifie les appels
emit('update', props.user)   // ✅
emit('delete', 42)           // ✅
// emit('delete', 'abc')     // ❌ Erreur : string n'est pas number
// emit('unknown')           // ❌ Erreur : event inconnu
</script>

Typer provide / inject

// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'
import type { User, Theme } from './models'

// Définir des clés d'injection typées
export const userKey: InjectionKey<Ref<User | null>> = Symbol('user')
export const themeKey: InjectionKey<Ref<Theme>> = Symbol('theme')

// Parent — provide typé
import { provide, ref } from 'vue'
import { userKey, themeKey } from '@/types/injection-keys'

const user = ref<User | null>(null)
const theme = ref<Theme>('dark')

provide(userKey, user)   // TypeScript vérifie que c'est bien Ref<User | null>
provide(themeKey, theme)

// Enfant — inject typé
import { inject } from 'vue'
import { userKey, themeKey } from '@/types/injection-keys'

const user = inject(userKey)           // Ref<User | null> | undefined
const theme = inject(themeKey, ref<Theme>('light')) // Ref<Theme> (avec défaut)

// user est potentiellement undefined si provide absent
if (user?.value) {
  console.log(user.value.name) // TypeScript sait que c'est User
}

Typer defineExpose()

defineExpose permet d'exposer des propriétés/méthodes au parent via ref. TypeScript peut typer ce que le composant expose.

// ChildComponent.vue
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const inputRef = ref<HTMLInputElement | null>(null)

const reset = () => { count.value = 0 }
const focus = () => inputRef.value?.focus()

// Exposer explicitement ce que le parent peut utiliser
defineExpose({
  count,
  reset,
  focus
})
</script>

// Parent.vue
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

// Typer la ref du composant enfant
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)

const handleReset = () => {
  childRef.value?.reset()              // ✅ TypeScript connaît reset()
  console.log(childRef.value?.count)   // ✅ TypeScript connaît count
  // childRef.value?.unknown()         // ❌ Erreur : méthode inconnue
}
</script>

<template>
  <ChildComponent ref="childRef" />
  <button @click="handleReset">Reset enfant</button>
</template>

Typer les refs DOM

<script setup lang="ts">
import { ref, onMounted } from 'vue'

// Ref vers un élément HTML natif
const inputRef = ref<HTMLInputElement | null>(null)
const divRef = ref<HTMLDivElement | null>(null)
const buttonRef = ref<HTMLButtonElement | null>(null)

// Ref vers un composant Vue
import MyComponent from './MyComponent.vue'
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)

onMounted(() => {
  inputRef.value?.focus()              // HTMLInputElement
  inputRef.value?.select()            // HTMLInputElement
  console.log(divRef.value?.offsetHeight) // number | undefined
})
</script>

<template>
  <input ref="inputRef" type="text">
  <div ref="divRef">...</div>
  <MyComponent ref="componentRef" />
</template>

Types conditionnels et mapped types (avancé)

// Type conditionnel — T extends ... ? A : B
type IsString<T> = T extends string ? true : false
type IsArray<T> = T extends any[] ? true : false

type A = IsString<string>  // true
type B = IsString<number>  // false
type C = IsArray<string[]> // true

// NonNullable — retirer null et undefined
type MaybeUser = User | null | undefined
type DefiniteUser = NonNullable<MaybeUser> // User

// Extraire le type d'un tableau
type UserArray = User[]
type SingleUser = UserArray[number] // User

// Mapped type — transformer toutes les propriétés
type Optional<T> = {
  [K in keyof T]?: T[K]
}
// Équivalent à Partial<T>

// Rendre toutes les propriétés en string
type Stringified<T> = {
  [K in keyof T]: string
}

type StringifiedUser = Stringified<User>
// { id: string, name: string, email: string, role: string, ... }

29. Development Tools

Vue Devtools

Extension browser pour débugger Vue. Compatible Vue 2 et Vue 3 (versions différentes de l'extension).

Features :

  • Inspection des composants et de leur état
  • Historique Pinia/Vuex (time-travel debugging)
  • Performance profiling

Vite Plugins (Vue 3)

// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({ imports: ['vue', 'vue-router', 'pinia'] }),
    Components({ dirs: ['src/components'] })
  ]
})

30. Sécurité

XSS Prevention

<template>
  <!-- ❌ Dangereux - Vue 2 & 3 -->
  <div v-html="userInput"></div>

  <!-- ✅ Sécurisé - Vue 2 & 3 -->
  <div>{{ userInput }}</div>

  <!-- ✅ Avec sanitization si v-html indispensable -->
  <div v-html="sanitize(userInput)"></div>
</template>

<script setup>
import DOMPurify from 'dompurify'
const sanitize = (html: string) => DOMPurify.sanitize(html)
</script>

31. Accessibilité (a11y)

<!-- Vue 2 & 3 - identique -->
<template>
  <button :aria-label="label" :aria-pressed="isPressed" @click="handleClick">
    {{ text }}
  </button>

  <nav aria-label="Main navigation">
    <ul role="list">
      <li><a href="/" aria-current="page">Home</a></li>
    </ul>
  </nav>

  <label for="email">Email</label>
  <input
    id="email"
    v-model="email"
    aria-required="true"
    :aria-invalid="!isEmailValid"
  >
</template>

32. Mobile & Progressive Web Apps

PWA Configuration (Vue 3 + Vite)

// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    VitePWA({
      manifest: {
        name: 'My Vue App',
        short_name: 'VueApp',
        theme_color: '#42b983',
        icons: [{ src: '/icon-192x192.png', sizes: '192x192', type: 'image/png' }]
      }
    })
  ]
})

33. Debugging & Troubleshooting

Erreurs communes

// Error: Cannot read property 'value' of undefined
// ✅ Vérifier l'initialisation
const data = ref(null)
if (data.value) console.log(data.value.property)

// Error: Maximum call stack size exceeded
// ✅ Éviter boucles infinies dans computed/watch
// Ne pas modifier une valeur observée dans son propre watcher sans condition de sortie

// Vue 2 - Error: [Vue warn] Avoid mutating a prop
// ✅ Créer une copie locale de la prop
data() { return { localValue: this.propValue } }

// Vue 3 - Réactivité perdue après destructuration d'un reactive
const state = reactive({ count: 0 })
const { count } = state        // ❌ count n'est plus réactif !
const { count } = toRefs(state) // ✅ count est un ref réactif

34. Ecosystem & Intégrations

Vue + GraphQL

npm install @apollo/client graphql
import { ApolloClient, InMemoryCache } from '@apollo/client'

export const apolloClient = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache()
})

35. Patterns de design

Container/Presentational

<!-- Container (Smart) - Vue 3 -->
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
const { users } = storeToRefs(userStore)
</script>

<template>
  <UserListPresentation :users="users" />
</template>

<!-- Presentation (Dumb) - Vue 2 & 3 -->
<script setup>
defineProps<{ users: User[] }>()
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

36. Real-world Examples

Dashboard

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

const stats = ref({ users: 0, revenue: 0, orders: 0 })

const formattedRevenue = computed(() =>
  new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.value.revenue)
)

const fetchStats = async () => {
  const res = await fetch('/api/stats')
  stats.value = await res.json()
}

onMounted(() => fetchStats())
</script>

<template>
  <div class="dashboard">
    <h1>Dashboard</h1>
    <div class="stats">
      <div class="stat-card">
        <h3>Users</h3>
        <p>{{ stats.users }}</p>
      </div>
      <div class="stat-card">
        <h3>Revenue</h3>
        <p>{{ formattedRevenue }}</p>
      </div>
      <div class="stat-card">
        <h3>Orders</h3>
        <p>{{ stats.orders }}</p>
      </div>
    </div>
  </div>
</template>

37. Annexes & Migration

Migration Vue 2 → Vue 3 : Récapitulatif complet

Vue 2 Vue 3 Notes
new Vue() createApp() Nouvelle API d'initialisation
new VueRouter() createRouter() API modulaire
new Vuex.Store() createPinia() (recommandé) Pinia remplace Vuex
beforeDestroy beforeUnmount Hook renommé
destroyed unmounted Hook renommé
$on / $off / $once Retirés Utiliser mitt ou tiny-emitter
Filters (`{{ val filter }}`) Retirés
$set / $delete Plus nécessaires Proxy détecte tout
Vue.directive() app.directive() Scope applicatif
Vue.component() app.component() Scope applicatif
slot="name" v-slot:name / #name Ancienne syntaxe retirée
slot-scope v-slot Ancienne syntaxe retirée
.v-enter .v-enter-from Classe CSS renommée
.v-leave .v-leave-from Classe CSS renommée
Mixins Composables (use...) Pattern recommandé
this.$router useRouter() Composition API
this.$route useRoute() Composition API
mode: 'history' history: createWebHistory() Configuration router
path: '*' path: '/:pathMatch(.*)*' Route 404
v-model = :value + @input v-model = :modelValue + @update:modelValue Composants custom
Multiple v-model impossible v-model:prop natif Vue 3 uniquement

Ressources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment