Vue 3 Composition API : Guide complet pour maîtriser la réactivité

Découvrez Vue 3 Composition API avec ce guide pratique. Apprenez ref, reactive, computed, watch et les composables pour créer des applications Vue performantes.

Illustration de Vue 3 Composition API avec des blocs de code réactifs interconnectés

La Composition API de Vue 3 représente une évolution majeure dans la façon de structurer les composants Vue. Cette approche permet d'organiser le code par fonctionnalité plutôt que par option, facilitant la réutilisation et la maintenance des applications complexes.

Prérequis

Ce guide suppose une connaissance de base de Vue.js. Les exemples utilisent la syntaxe <script setup> introduite dans Vue 3.2, qui est désormais la méthode recommandée.

Comprendre la réactivité avec ref et reactive

La réactivité est au cœur de Vue 3. Deux primitives principales permettent de créer des données réactives : ref pour les valeurs primitives et reactive pour les objets complexes.

La fonction ref crée une référence réactive qui encapsule une valeur. Pour accéder ou modifier cette valeur dans le script, il faut utiliser la propriété .value. Dans le template, Vue décompresse automatiquement cette propriété.

Counter.vuejavascript
<script setup>
import { ref } from 'vue'

// Création d'une ref avec valeur initiale 0
const count = ref(0)

// Fonction qui incrémente le compteur
// Note: .value est nécessaire dans le script
const increment = () => {
  count.value++
}

// Fonction de réinitialisation
const reset = () => {
  count.value = 0
}
</script>

<template>
  <!-- Dans le template, pas besoin de .value -->
  <div class="counter">
    <p>Compteur : {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="reset">Réinitialiser</button>
  </div>
</template>

Pour les objets avec plusieurs propriétés liées, reactive offre une syntaxe plus naturelle sans avoir besoin de .value.

UserProfile.vuejavascript
<script setup>
import { reactive } from 'vue'

// reactive pour les objets complexes
// Toutes les propriétés sont automatiquement réactives
const user = reactive({
  name: 'Marie Dupont',
  email: 'marie@example.com',
  preferences: {
    theme: 'dark',
    notifications: true
  }
})

// Modification directe des propriétés (pas de .value)
const updateTheme = (newTheme) => {
  user.preferences.theme = newTheme
}

// Attention: ne pas réassigner l'objet entier
// user = { ... } casserait la réactivité
</script>

<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
    <p>Thème : {{ user.preferences.theme }}</p>
  </div>
</template>

La règle générale : utiliser ref pour les valeurs primitives (string, number, boolean) et reactive pour les objets structurés.

Propriétés calculées avec computed

Les propriétés calculées permettent de dériver des valeurs à partir de l'état réactif. Elles sont mises en cache et ne se recalculent que lorsque leurs dépendances changent, ce qui les rend très performantes.

ProductList.vuejavascript
<script setup>
import { ref, computed } from 'vue'

// Liste de produits
const products = ref([
  { id: 1, name: 'Laptop', price: 999, inStock: true },
  { id: 2, name: 'Souris', price: 29, inStock: true },
  { id: 3, name: 'Clavier', price: 79, inStock: false },
  { id: 4, name: 'Écran', price: 299, inStock: true }
])

// Filtre actif
const showOnlyInStock = ref(false)

// computed: filtrage automatique selon le toggle
// Se recalcule uniquement si products ou showOnlyInStock change
const filteredProducts = computed(() => {
  if (showOnlyInStock.value) {
    return products.value.filter(p => p.inStock)
  }
  return products.value
})

// computed: calcul du total avec mise en cache
const totalValue = computed(() => {
  return filteredProducts.value.reduce((sum, p) => sum + p.price, 0)
})

// computed: nombre d'articles affichés
const productCount = computed(() => filteredProducts.value.length)
</script>

<template>
  <div>
    <label>
      <input type="checkbox" v-model="showOnlyInStock" />
      Afficher uniquement les produits en stock
    </label>

    <p>{{ productCount }} produits - Total : {{ totalValue }}</p>

    <ul>
      <li v-for="product in filteredProducts" :key="product.id">
        {{ product.name }} - {{ product.price }}      </li>
    </ul>
  </div>
</template>

Les computed peuvent également être en écriture avec un getter et un setter, utile pour les transformations bidirectionnelles.

FullName.vuejavascript
<script setup>
import { ref, computed } from 'vue'

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

// computed avec getter ET setter
const fullName = computed({
  // Lecture: combine prénom et nom
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  // Écriture: sépare la chaîne en prénom/nom
  set(newValue) {
    const parts = newValue.split(' ')
    firstName.value = parts[0] || ''
    lastName.value = parts.slice(1).join(' ') || ''
  }
})
</script>

<template>
  <!-- La modification de fullName met à jour firstName et lastName -->
  <input v-model="fullName" placeholder="Nom complet" />
  <p>Prénom: {{ firstName }}</p>
  <p>Nom: {{ lastName }}</p>
</template>

Observateurs avec watch et watchEffect

Les watchers permettent d'exécuter des effets secondaires en réponse aux changements de données. Vue 3 propose deux approches : watch pour un contrôle précis et watchEffect pour le tracking automatique.

Quand utiliser watch vs watchEffect ?

watch offre un contrôle précis sur les dépendances et fournit l'ancienne et la nouvelle valeur. watchEffect est plus simple quand toutes les dépendances réactives utilisées doivent déclencher l'effet.

SearchComponent.vuejavascript
<script setup>
import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)

// watch: observe une source spécifique
// Fournit oldValue et newValue, permet le debounce
watch(searchQuery, async (newQuery, oldQuery) => {
  console.log(`Recherche changée de "${oldQuery}" vers "${newQuery}"`)

  // Ne pas rechercher si moins de 3 caractères
  if (newQuery.length < 3) {
    searchResults.value = []
    return
  }

  isLoading.value = true

  try {
    // Appel API simulé
    const response = await fetch(`/api/search?q=${newQuery}`)
    searchResults.value = await response.json()
  } finally {
    isLoading.value = false
  }
}, {
  // Options du watcher
  debounce: 300, // Attendre 300ms après la dernière frappe
  immediate: false // Ne pas exécuter immédiatement
})

// watchEffect: tracking automatique des dépendances
// S'exécute immédiatement et à chaque changement
watchEffect(() => {
  // Toutes les refs utilisées ici sont automatiquement trackées
  console.log(`État actuel: ${searchResults.value.length} résultats`)
  console.log(`Loading: ${isLoading.value}`)
})
</script>

<template>
  <div>
    <input v-model="searchQuery" placeholder="Rechercher..." />
    <p v-if="isLoading">Chargement...</p>
    <ul v-else>
      <li v-for="result in searchResults" :key="result.id">
        {{ result.title }}
      </li>
    </ul>
  </div>
</template>

Pour observer des objets imbriqués ou des tableaux, l'option deep est nécessaire avec watch.

DeepWatch.vuejavascript
<script setup>
import { reactive, watch } from 'vue'

const settings = reactive({
  display: {
    theme: 'light',
    fontSize: 14
  },
  notifications: {
    email: true,
    push: false
  }
})

// deep: true pour observer les changements imbriqués
watch(
  () => settings.display,
  (newDisplay) => {
    console.log('Paramètres display modifiés:', newDisplay)
    // Sauvegarder en localStorage par exemple
    localStorage.setItem('display', JSON.stringify(newDisplay))
  },
  { deep: true }
)
</script>

Prêt à réussir tes entretiens Vue.js / Nuxt.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Créer des composables réutilisables

Les composables sont des fonctions qui encapsulent de la logique réactive réutilisable. Cette approche est l'un des atouts majeurs de la Composition API pour partager du code entre composants.

composables/useFetch.jsjavascript
import { ref, watchEffect, toValue } from 'vue'

// Composable pour les requêtes HTTP
// Gère automatiquement le loading, les erreurs et le refetch
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const isLoading = ref(false)

  // Fonction de fetch réutilisable
  async function fetchData() {
    isLoading.value = true
    error.value = null

    try {
      // toValue() permet d'accepter une ref ou une valeur
      const response = await fetch(toValue(url))

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }

      data.value = await response.json()
    } catch (e) {
      error.value = e.message
    } finally {
      isLoading.value = false
    }
  }

  // watchEffect pour refetch automatique si url est une ref
  watchEffect(() => {
    fetchData()
  })

  // Exposer l'état et les méthodes
  return {
    data,
    error,
    isLoading,
    refetch: fetchData
  }
}

Ce composable s'utilise ensuite dans n'importe quel composant de manière déclarative.

UserList.vuejavascript
<script setup>
import { useFetch } from '@/composables/useFetch'

// Utilisation simple du composable
const { data: users, isLoading, error, refetch } = useFetch('/api/users')
</script>

<template>
  <div>
    <button @click="refetch" :disabled="isLoading">
      Actualiser
    </button>

    <p v-if="isLoading">Chargement des utilisateurs...</p>
    <p v-else-if="error" class="error">Erreur : {{ error }}</p>

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

Voici un autre exemple de composable pour gérer l'état d'un formulaire avec validation.

composables/useForm.jsjavascript
import { reactive, computed } from 'vue'

// Composable de gestion de formulaire
export function useForm(initialValues, validationRules) {
  // État du formulaire
  const form = reactive({
    values: { ...initialValues },
    errors: {},
    touched: {}
  })

  // Valider un champ spécifique
  const validateField = (field) => {
    const rules = validationRules[field]
    if (!rules) return true

    for (const rule of rules) {
      const result = rule(form.values[field])
      if (result !== true) {
        form.errors[field] = result
        return false
      }
    }

    form.errors[field] = null
    return true
  }

  // Valider tout le formulaire
  const validate = () => {
    let isValid = true
    for (const field in validationRules) {
      if (!validateField(field)) {
        isValid = false
      }
    }
    return isValid
  }

  // computed: formulaire valide si aucune erreur
  const isValid = computed(() => {
    return Object.values(form.errors).every(e => !e)
  })

  // Marquer un champ comme touché (pour afficher les erreurs)
  const touch = (field) => {
    form.touched[field] = true
    validateField(field)
  }

  // Réinitialiser le formulaire
  const reset = () => {
    form.values = { ...initialValues }
    form.errors = {}
    form.touched = {}
  }

  return {
    form,
    isValid,
    validate,
    validateField,
    touch,
    reset
  }
}

Gestion du cycle de vie

La Composition API propose des hooks de cycle de vie sous forme de fonctions à appeler dans le setup. Ces hooks permettent d'exécuter du code à des moments précis de la vie du composant.

Nettoyage important

Toujours nettoyer les effets secondaires (timers, event listeners, subscriptions) dans onUnmounted pour éviter les fuites mémoire.

LifecycleDemo.vuejavascript
<script setup>
import {
  ref,
  onMounted,
  onUnmounted,
  onBeforeUpdate,
  onUpdated
} from 'vue'

const windowWidth = ref(window.innerWidth)
const updateCount = ref(0)

// Handler pour le resize
const handleResize = () => {
  windowWidth.value = window.innerWidth
}

// onMounted: composant inséré dans le DOM
// Idéal pour les appels API initiaux et les event listeners
onMounted(() => {
  console.log('Composant monté')
  window.addEventListener('resize', handleResize)

  // Exemple: initialiser une librairie tierce
  // chart = new Chart(chartRef.value, config)
})

// onUnmounted: composant retiré du DOM
// CRITIQUE: nettoyer tous les effets secondaires
onUnmounted(() => {
  console.log('Composant démonté')
  window.removeEventListener('resize', handleResize)

  // Nettoyer les subscriptions, timers, etc.
  // chart?.destroy()
})

// onBeforeUpdate: avant la mise à jour du DOM
onBeforeUpdate(() => {
  console.log('Avant mise à jour')
})

// onUpdated: après la mise à jour du DOM
onUpdated(() => {
  updateCount.value++
  console.log(`DOM mis à jour (${updateCount.value} fois)`)
})
</script>

<template>
  <div>
    <p>Largeur de fenêtre : {{ windowWidth }}px</p>
    <p>Mises à jour du DOM : {{ updateCount }}</p>
  </div>
</template>

Refs de template et accès au DOM

Les refs de template permettent d'accéder directement aux éléments DOM ou aux instances de composants enfants. Cela reste utile pour les cas où la manipulation directe est nécessaire.

InputFocus.vuejavascript
<script setup>
import { ref, onMounted } from 'vue'

// ref pour l'élément DOM
// Même nom que l'attribut ref dans le template
const inputRef = ref(null)

// Fonction pour focus l'input
const focusInput = () => {
  // Accès à l'élément DOM natif
  inputRef.value?.focus()
}

// Focus automatique au montage
onMounted(() => {
  focusInput()
})

// Exposer la méthode au parent si nécessaire
defineExpose({
  focus: focusInput
})
</script>

<template>
  <div>
    <!-- L'attribut ref lie l'élément à inputRef -->
    <input ref="inputRef" type="text" placeholder="Focus automatique" />
    <button @click="focusInput">Redonner le focus</button>
  </div>
</template>

Communication parent-enfant avec props et emits

La Composition API modernise la déclaration des props et events avec defineProps et defineEmits, offrant une meilleure intégration TypeScript.

ChildComponent.vuejavascript
<script setup>
// defineProps: déclare les props attendues
// Avec valeurs par défaut via withDefaults
const props = withDefaults(defineProps<{
  title: string
  count?: number
  items?: string[]
}>(), {
  count: 0,
  items: () => []
})

// defineEmits: déclare les événements émis
// Typage précis des payloads
const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'submit', data: { title: string; items: string[] }): void
}>()

// Fonction qui émet un événement
const handleSubmit = () => {
  emit('submit', {
    title: props.title,
    items: props.items
  })
}

const increment = () => {
  emit('update', props.count + 1)
}
</script>

<template>
  <div class="card">
    <h3>{{ title }}</h3>
    <p>Count: {{ count }}</p>
    <button @click="increment">Incrémenter</button>
    <button @click="handleSubmit">Soumettre</button>
  </div>
</template>
ParentComponent.vuejavascript
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const currentCount = ref(0)
const items = ref(['Item 1', 'Item 2'])

// Handler pour l'événement update
const onUpdate = (newValue) => {
  currentCount.value = newValue
}

// Handler pour l'événement submit
const onSubmit = (data) => {
  console.log('Données soumises:', data)
}
</script>

<template>
  <ChildComponent
    title="Mon composant"
    :count="currentCount"
    :items="items"
    @update="onUpdate"
    @submit="onSubmit"
  />
</template>

Prêt à réussir tes entretiens Vue.js / Nuxt.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Provide et Inject pour l'injection de dépendances

Pour partager des données entre composants distants sans prop drilling, Vue 3 propose provide et inject.

App.vue (ou un composant ancêtre)javascript
<script setup>
import { provide, ref, readonly } from 'vue'

// État global du thème
const theme = ref('light')

// Fonction pour changer le thème
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// provide: rend disponible aux descendants
// readonly empêche la modification directe par les enfants
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>

<template>
  <div :class="theme">
    <slot />
  </div>
</template>
DeepNestedComponent.vue (n'importe où dans l'arbre)javascript
<script setup>
import { inject } from 'vue'

// inject: récupère la valeur fournie par un ancêtre
// Valeur par défaut si non trouvé
const theme = inject('theme', 'light')
const toggleTheme = inject('toggleTheme', () => {})
</script>

<template>
  <div>
    <p>Thème actuel : {{ theme }}</p>
    <button @click="toggleTheme">
      Basculer vers {{ theme === 'light' ? 'sombre' : 'clair' }}
    </button>
  </div>
</template>

Conclusion

La Composition API de Vue 3 offre une approche puissante et flexible pour organiser le code des applications Vue. Les concepts clés à retenir :

  • ref pour les valeurs primitives, reactive pour les objets complexes
  • computed pour les valeurs dérivées avec mise en cache automatique
  • watch et watchEffect pour les effets secondaires réactifs
  • Composables pour extraire et réutiliser la logique entre composants
  • Hooks de cycle de vie fonctionnels (onMounted, onUnmounted, etc.)
  • provide/inject pour l'injection de dépendances sans prop drilling

Cette approche facilite la création d'applications maintenables et testables, tout en offrant une excellente intégration TypeScript. La prochaine étape consiste à explorer les patterns avancés comme les composables asynchrones et l'intégration avec Pinia pour la gestion d'état globale.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#vue 3
#composition api
#javascript
#frontend
#réactivité

Partager

Articles similaires