Vue 3 : Guide Complet de l'API de Composition et des Fonctionnalités Clés

Introduction à l'API de Composition

L'API de Composition est une collection de fonctions qui permet d'organiser la logique d'un composant Vue de manière plus flexible et réutilisable. Elle offre une alternative aux Options API traditionneles, particulièrement utile pour les composants complexes ou la réutilisation de logique. L'entrée principale de cette API est la fonction setup, ou plus communément, le bloc <script setup>.

La fonction setup

La fonction setup est exécutée très tôt dans le cycle de vie du composant, spécifiquement avent beforeCreate et created. Elle sert de point d'entrée pour la logique du composant en utilisant l'API de Composition.

  • Dans setup, l'utilisation de this est déconseillée car l'instance du composant n'est pas encore entièrement disponible.
  • Toutes les variables réactives et les fonctions que vous souhaitez exposer au template du composant doivent être retournées par la fonction setup dans un objet.
  • Les props passées à setup sont réactives et sont automatiquement mises à jour. Il est crucial de ne pas déstructurer les props directement, car cela briserait leur réactivité.
  • Le second argument de setup est un objet context qui expose des propriétés essentielles comme attrs, slots, emit (pour émettre des événements) et expose (pour exposer des propriétés/méthodes à l'instance du composant via ref), toutes mises à jour automatiquement.
<template>
   <h1>Bienvenue</h1>
   <p>Âge du visiteur: {{ visitorAge }}</p>
   <button @click="greet">Saluer</button>
</template>
<script>
import { defineComponent, ref } from "vue"

export default defineComponent({
  name: 'PageAccueil',
  props: {
    userName: String
  },
  setup(props, context) {
    const visitorAge = ref(25); // Une variable réactive
    
    function greet() {
      console.log(`Bonjour ${props.userName}, vous avez ${visitorAge.value} ans.`);
      context.emit('user-greeted', props.userName);
    }

    // Retourne les données et fonctions à utiliser dans le template
    return {
      visitorAge,
      greet
    }
  }
})
</script>

Le sucre syntaxique <script setup>

Pour simplifier l'écriture de composants Vue avec l'API de Composition, Vue 3 a introduit <script setup>. Ce bloc de script est transpilé pour exécuter son contenu dans la fonction setup sous le capot. Il élimine le besoin de retourner explicitement les variables et fonctions, et offre plusieurs avantages :

  • Les importations de composants peuvent être utilisées directement dans le template sans enregistrement via components.
  • Le nom du composant est automatiquement déduit du nom de fichier, rendant la propriété name facultative.

Utilisation des props dans <script setup>

La macro defineProps permet de déclarer les props du composant. Elle retourne un objet réactif représentant les valeurs des props.

Définir et Émettre des Événements

La macro defineEmits est utilisée pour déclarer les événements qu'un composant peut émettre. Elle retourne une fonction emit à utiliser pour déclencher ces événements.

Accéder aux slots et attrs

Les fonctions utilitaires useSlots et useAttrs fournissent un accès aux slots et aux attributs non déclarés (attrs) passés au composant, respectivement.

Exposer des données et méthodes via defineExpose

Dans <script setup>, le contenu est par défaut encapsulé et non accessible via une référence (ref) depuis le parent. Pour exposer explicitement des propriétés ou des méthodes à l'instance du composant parente (accessible via template ref), utilisez defineExpose.

<template>
   <!-- Mon composant Enfant, utilisé directement -->
   <ComposantEnfant titre="Mon Titre" @action="handleAction">
      <template #header>
         <h2>En-tête de l'enfant</h2>
      </template>
   </ComposantEnfant>
</template>
<script setup>
import ComposantEnfant from './ComposantEnfant.vue'
import { ref, defineEmits, defineExpose, defineProps, useSlots, useAttrs } from 'vue'

const currentCount = ref(0)

// Déclaration des props
const props = defineProps({
  message: String
})

// Déclaration des événements émis
const emit = defineEmits(['action', 'update:modelValue'])

// Accès aux slots et attrs
const slots = useSlots()
const attrs = useAttrs()

function incrementCount() {
  currentCount.value++
  emit('action', currentCount.value)
}

// Exposer des éléments pour l'accès via template ref
defineExpose({
    currentCount,
    incrementCount
})

const handleAction = (value) => {
    console.log(`Action reçue de l'enfant avec la valeur: ${value}`)
}
</script>

Notez que depuis Vue 2.6.x et exclusivement en Vue 3.x, la directive v-slot est la seule manière d'utiliser les slots nommés ou scoped, unifiant ainsi la syntaxe.

Gestion de la Réactivité : reactive, ref, toRefs et toRef

Vue 3 propose deux fonctions principales pour déclarer des données réactives : ref et reactive.

ref

La fonction ref est utilisée pour créer des références réactives pour des valeurs primitives (chaînes de caractères, nombres, booléens) ou des objets. Lorsqu'une primitive est enveloppée dans ref, elle devient un objet avec une seule propriété .value. Pour accéder ou modifier sa valeur, il faut passer par .value.

Si ref est utilisée avec un objet, Vue 3 convertit automatiquement cet objet en un proxy réactif en utilisant reactive en interne. Dans les templates, Vue déballe automatiquement les références (ref) si elles sont des propriétés de niveau supérieur, vous n'avez donc pas besoin d'ajouter .value.

reactive

La fonction reactive est utilisée pour créer des objets réactifs (y compris les tableaux). Elle transforme l'objet original en un proxy réactif de manière profonde, ce qui signifie que toutes les propriétés imbriquées de l'objet sont également réactives.

Différences entre ref et reactive

  • ref est préférée pour les valeurs primitives. reactive est conçue pour les objets et les tableaux.
  • Les valeurs ref sont accessibles et modifiées via la propriété .value dans le script, mais sont automatiquement déballées dans les templates. Les objets reactive sont manipulés directement.
  • ref utilise Object.defineProperty pour les primitives (avec un objet wrapper), et Proxy pour les objets (en interne via reactive). reactive utilise toujours Proxy.

toRefs et toRef

Ces fonctions sont utiles pour gérer la déstructuration d'objets réactifs tout en conservant leur réactivité.

  • toRefs: Convertit un objet reactive en un objet simple où chaque propriété est une ref. Cela permet de déstructurer l'objet reactive dans le script sans perdre la réactivité des propriétés.
  • toRef: Crée une ref pour une propriété spécifique d'un objet reactive. Cette ref maintient une connexion réactive à la propriété source. Si la ref est modifiée, la propriété originale de l'objet reactive est également mise à jour et vice-versa.

Il est important de noter que dans les templates, les ref créées par toRefs ou toRef sont automatiquement déballées et n'ont pas besoin de .value si elles sont au premier niveau de l'objet.

<template>
  <p>Année: {{ currentYear }}</p>
  <p>Nom d'utilisateur: {{ userProfile.username }}</p>
  <p>Âge (via toRefs): {{ age }}</p>
  <p>Statut de l'utilisateur (via toRef): {{ isActive }}</p>
  <button @click="updateProfile">Mettre à jour le profil</button>
  <hr/>
  <p>Animal de compagnie: {{ petName }}</p>
  <p>Type: {{ petType }}</p>
</template>

<script setup>
import { reactive, ref, toRefs, toRef } from "vue"

const currentYear = ref(2023)

const userProfile = reactive({
  username: "Alice",
  age: 30,
  isActive: true
})

// Déstructuration avec toRefs
const { age } = toRefs(userProfile)
const isActive = toRef(userProfile, 'isActive') // Création d'une ref pour 'isActive'

setTimeout(() => {
  userProfile.username = "Bob"
  age.value = 31 // Met à jour userProfile.age
  isActive.value = false // Met à jour userProfile.isActive
}, 1500)

// Exemple avec ref pour un objet
const pet = ref({
  petName: "Fido",
  petType: "Chien"
})

// Pour déstructurer un objet ref, il faut accéder à .value
const { petName, petType } = toRefs(pet.value)

setTimeout(() => {
  pet.value.petType = "Chat" // Met à jour petType
}, 2000)

function updateProfile() {
    currentYear.value++
    userProfile.username = "Charlie"
}
</script>

Surveillance et Calcul : watch, watchEffect et computed

watch

La fonction watch permet d'observer une source de données réactive et d'exécuter une fonction de rappel lorsque cette source change. Elle est paresseuse par défaut, c'est-à-dire que le rappel n'est déclenché que lorsque la source observée est modifiée.

Signature: watch(source, callback, [options])

  • source: Peut être une ref, un objet reactive, une fonction qui retourne une valeur réactive, ou un tableau de ces types.
  • callback: La fonction à exécuter, recevant les nouvelles et anciennes valeurs.
  • options: Un objet qui peut inclure deep: true (pour observer les changements dans les objets imbriqués), immediate: true (pour déclencher le rappel immédiatement lors du montage du composant), et flush (pour contrôler le moment où le rappel est exécuté).

Lors de l'observation d'un objet reactive directement, l'ancienne valeur passée au rappel peut ne pas être fiable, et la surveillance est implicitement profonde.

<template>
  <p>Nom: {{ person.firstName }} {{ person.lastName }}</p>
  <p>Compteur: {{ counter }}</p>
  <button @click="changeNames">Changer les noms</button>
  <button @click="incrementCounter">Incrémenter le compteur</button>
</template>

<script setup>
import { reactive, ref, watch } from "vue"

const counter = ref(0)
const person = reactive({
  firstName: "Jean",
  lastName: "Dupond",
  address: {
    city: "Paris",
    zip: "75001"
  }
})

// Surveiller une ref simple
watch(counter, (newVal, oldVal) => {
  console.log('Compteur a changé de:', oldVal, 'à:', newVal)
})

// Surveiller une propriété d'un objet reactive
watch(() => person.firstName, (newVal, oldVal) => {
  console.log('Prénom a changé de:', oldVal, 'à:', newVal)
})

// Surveiller un objet imbriqué avec deep: true
watch(() => person.address, (newVal, oldVal) => {
  console.log('Adresse a été modifiée:', newVal, 'ancienne:', oldVal)
}, {
  deep: true, // Nécessaire pour détecter les changements profonds
  immediate: true // Déclenche le watcher au montage
})

// Surveiller plusieurs sources
watch([counter, () => person.lastName], ([newCounter, newLastName], [oldCounter, oldLastName]) => {
  console.log('Multi-surveillance:', { newCounter, oldLastName, newLastName, oldCounter })
})

function changeNames() {
    person.firstName = "Marie"
    person.lastName = "Durand"
    person.address.zip = "75002"
}

function incrementCounter() {
    counter.value++
}

// Arrêter une surveillance
const stopWatchName = watch(() => person.firstName, (newVal) => {
  console.log('Ce watcher pour le prénom s\'arrêtera bientôt:', newVal)
})

setTimeout(() => {
  stopWatchName() // Arrête la surveillance après un délai
  console.log('Surveillance du prénom arrêtée.')
}, 5000)

</script>

watchEffect

watchEffect est une fonction plus simple qui exécute un effet de manière réactive. Elle détecte automatiquement les dépendances et réexécute l'effet chaque fois qu'une dépendance est modifiée. Elle est toujours exécutée au moins une fois, lors du montage du composant.

  • Pas besoin de spécifier les sources à observer; watchEffect les collecte automatiquement.
  • Déclenchée immédiatement au montage (équivalent à immediate: true dans watch).
  • Ne fournit pas l'ancienne valeur, seulement la nouvelle.
<template>
  <h3>Nom Complet: {{ fullName }}</h3>
  <p>Code Postal: {{ location.postalCode }}</p>
</template>
<script setup>
import { watchEffect, reactive, ref, computed } from 'vue';

const employee = reactive({
  firstName: 'Lucas',
  lastName: 'Martin'
})
const location = ref({
  city: 'Lyon',
  postalCode: '69001'
})

const fullName = computed(() => `${employee.firstName} ${employee.lastName}`)

// watchEffect observe automatiquement employee.firstName et employee.lastName
watchEffect(() => {
  console.log(`Le nom complet est maintenant: ${fullName.value}`);
  // watchEffect observe également location.value.postalCode
  console.log(`Le code postal est: ${location.value.postalCode}`);
});

setInterval(() => {
  employee.firstName = 'Olivier' + Math.floor(Math.random() * 10);
  location.value.postalCode = '6900' + Math.floor(Math.random() * 9);
}, 2000);
</script>

computed

La fonction computed permet de créer une propriété réactive dérivée. Elle est mise à jour automatiquement chaque fois que ses dépendances réactives changent, et met en cache sa valeur tant que ses dépendances n'ont pas changé. Elle peut être définie avec une fonction simple (lecture seule) ou un objet avec des fonctions get et set (lecture/écriture).

<template>
 <input type="text" placeholder="Prénom" v-model="contact.givenName" />
 <input type="text" placeholder="Nom de famille" v-model="contact.surname" />
 <h3>Nom complet du contact: {{ contactFullName }}</h3>
 <button @click="changeSurname">Changer le nom de famille</button>
</template>
<script setup>
import { reactive, computed } from 'vue';

const contact = reactive({
  givenName: '',
  surname: ''
})

// Propriété calculée en lecture seule
const contactFullName = computed(() => {
  return `${contact.givenName} ${contact.surname}`.trim()
})

// Propriété calculée avec setter (lecture/écriture)
const changeableFullName = computed({
    get() {
        return `${contact.givenName} ${contact.surname}`.trim()
    },
    set(newValue) {
        const parts = newValue.split(' ')
        contact.givenName = parts[0] || ''
        contact.surname = parts.slice(1).join(' ') || ''
    }
})

function changeSurname() {
    changeableFullName.value = "Jeanne Dupont" // Utilise le setter
}
</script>

Hooks Personnalisés (Custom Hooks)

Les hooks personnalisés dans Vue 3 sont des fonctions réutilisables qui encapsulent de la logique réactive de l'API de Composition. Similaires aux mixins de Vue 2, ils offrent une meilleure organisation, une inférence de type améliorée et évitent les problèmes de collision de noms. Par convention, leurs noms commencent par use.

// hooks/useCounter.js
import { ref, computed } from "vue"

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

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

    const decrement = () => {
        currentCount.value--
    }

    return {
        currentCount,
        doubleCount,
        increment,
        decrement
    }
}

<template>
  <h3>Compteur: {{ currentCount }}</h3>
  <h3>Compteur doublé: {{ doubleCount }}</h3>
  <button @click="increment">Augmenter</button>
  <button @click="decrement">Diminuer</button>
</template>
<script setup>
import { useCounter } from '@/hooks/useCounter'; // Assurez-vous que le chemin est correct

const { currentCount, doubleCount, increment, decrement } = useCounter(50);
</script>

Réactivité : Vue 2.x (Object.defineProperty) vs Vue 3.x (Proxy)

La gestion de la réactivité a subi une refonte majeure entre Vue 2 et Vue 3, passant de Object.defineProperty à Proxy.

  • Object.defineProperty (Vue 2) :
    • Requiert une itération sur chaque propriété d'un objet pour la rendre réactive. Pour les objets imbriqués, une récursion est nécessaire.
    • Ne peut pas détecter l'ajout ou la suppression de propriétés sur un objet existant. C'est pourquoi des méthodes comme Vue.set ou Vue.delete étaient nécessaires pour garantir la réactivité de ces opérations.
    • Ne peut pas détecter les modifications directes d'éléments de tableau par index ou la modification de la longueur d'un tableau.
  • Proxy (Vue 3) :
    • Délègue l'accès à un objet entier plutôt qu'à des propriétés individuelles. Cela signifie que Proxy peut intercepter toutes les opérations sur l'objet (lecture, écriture, ajout, suppression de propriétés, etc.) sans avoir besoin de遍历 (parcourir) les propriétés.
    • Gère nativement l'ajout et la suppression de propriétés, ainsi que les modifications de tableaux par index ou longueur, sans nécessiter de méthodes spéciales comme $set.
    • Offre de meilleures performances pour les objets de grande taille et une plus grande flexibilité pour l'interception des opérations.

Crochets de Cycle de Vie (Lifecycle Hooks)

Avec l'API de Composition, les crochets de cycle de vie sont préfixés par on et sont appelés dans la fonction setup ou <script setup>.

  • onMounted: Exécuté après que le composant a été monté sur le DOM.
  • onUpdated: Exécuté après qu'une mise à jour du DOM du composant a été effectuée.
  • onBeforeUnmount: Exécuté juste avant le démontage du composant.
  • onUnmounted: Exécuté après le démontage du composant du DOM.
  • onActivated: Déclenché lorsqu'un composant caché par <KeepAlive> devient actif. Utile pour recharger des données ou reprendre des opérations suspendues.
  • onDeactivated: Déclenché lorsqu'un composant actif est caché par <KeepAlive>. Utile pour suspendre des opérations coûteuses ou sauvegarder l'état.
  • onRenderTracked: Hook de débogage qui est déclenché chaque fois qu'une propriété réactive ou une dépendance est suivie pendant un rendu. Il fournit des informations détaillées sur les dépendances du rendu.
  • onRenderTriggered: Hook de débogage déclenché chaque fois qu'une dépendance réactive provoque un re-rendu. Il offre des informations sur la raison exacte du déclenchement du re-rendu, y compris la clé de la dépendance, sa nouvelle et ancienne valeur.
  • onErrorCaptured: Capture les erreurs des composants enfants ou descendants. Permet de gérer les erreurs de manière centralisée et d'éviter qu'elles ne fassent planter l'application.

Fragment (Fragments)

Vue 3 introduit la capacité pour un composant d'avoir plusieurs nœuds racines dans son template, une fonctionnalité appelée "Fragments". Auparavant, chaque composant Vue 2 devait avoir un seul élément racine. Les Fragments réduisent la profondeur de l'arbre DOM, ce qui peut améliorer les performances et réduire la consommation de mémoire.

<!-- LayoutComposant.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">
    <p>Contenu principal</p>
  </main>
  <footer>...</footer>
</template>

Teleport

Le composant Teleport permet de rendre le contenu d'un composant à un endroit différent du DOM, en dehors de la hiérarchie de son parent logique. C'est idéal pour les modales, les tooltips, ou les notifications qui doivent apparaître au-dessus de tout le reste, souvent directement dans le body.

La propriété to spécifie l'élément cible du DOM (via un sélecteur CSS, un ID ou une référence d'élément) où le contenu du Teleport sera rendu.

<template>
  <div>
    <button @click="showModal = true">Ouvrir la modale</button>
    <Teleport to="body">
      <div v-if="showModal" class="modal-overlay">
        <div class="modal-content">
          <h2>Titre de la Modale</h2>
          <p>Contenu déplacé vers le body.</p>
          <button @click="showModal = false">Fermer</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>
<script setup>
import { ref } from 'vue';
const showModal = ref(false);
</script>

Suspense

Suspense est un composant spécial conçu pour gérer les composants asynchrones. Il permet d'afficher un contenu de remplacement (fallback) pendant qu'un composant enfant asynchrone (par exemple, un composant chargé paresseusement ou qui effectue une requête de données au montage) est en cours de chargement.

Il possède deux slots: #default pour le contenu principal asynchrone, et #fallback pour le contenu à afficher pendant le chargement.

<template>
  <div>
    <h1>Application Principale</h1>
    <Suspense>
      <!-- Contenu qui peut être asynchrone -->
      <template #default>
        <ComposantAsynchrone />
      </template>
      <!-- Contenu de secours affiché pendant le chargement -->
      <template #fallback>
        <div>Chargement du composant...</div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';

// Définition d'un composant asynchrone (par exemple, un chargement paresseux)
const ComposantAsynchrone = defineAsyncComponent(() =>
  new Promise(resolve => {
    setTimeout(() => {
      resolve({
        template: '<div><h3>Contenu chargé de manière asynchrone!</h3></div>'
      })
    }, 2000) // Simule un délai de chargement de 2 secondes
  })
);
</script>

Étiquettes: Vue.js Composition API Vue 3 Reactivity Hooks

Publié le 30 juillet à 13h59