Vue.js

Les bases de Vue 3

Comprendre la réactivité, les directives et le cycle de vie des composants Vue 3

Dans ce chapitre, on construit notre Show Watchlist — une application de gestion de séries à voir et déjà vus. Chaque concept est illustré avec un extrait du composant final présenté en fin de chapitre.

Réactivité

La réactivité est au cœur de Vue 3 : quand vos données changent, l'interface se met à jour automatiquement.

ref()

ref() crée une valeur réactive. Elle s'utilise pour les types simples (nombre, chaîne, booléen) mais aussi pour les tableaux et objets.


<template>
  <p>Séries dans la watchlist : {{ shows.length }}</p>
</template>

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

  interface Show {
    id: number
    title: string
    genre: string
    year: number
    seen: boolean
  }

  const search = ref<string>('')
  const shows = ref<Show[]>([
    {id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true},
    {id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false},
    {id: 3, title: 'Interstellar', genre: 'Sci-Fi', year: 2014, seen: false},
  ])
</script>

Remarque : Dans le <template>, Vue déroule automatiquement le .value. Dans le <script>, il faut toujours passer par shows.value pour lire ou modifier le tableau.

Les fonctions

En Vue, vous pouvez déclarer des fonctions classiques pour calculer ou transformer des données. Mais getSeenCount() est appelée à chaque re-rendu du composant, même si shows n'a pas changé. Ajoutez un console.log et un bouton qui force un re-rendu sans toucher à shows pour le constater :


<template>
  <p>{{ getSeenCount() }} série(s) vue(s) sur {{ shows.length }}</p>
  <button @click="shows[0].seen = !shows[0].seen">Basculer Inception</button>
  <button @click="count++">Re-rendre ({{ count }})</button>
</template>

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

  const count = ref(0)
  const shows = ref([
    {title: 'Inception', seen: true},
    {title: 'Dune', seen: false},
    {title: 'Interstellar', seen: false},
  ])

  function getSeenCount() {
    console.log('getSeenCount appelée !') // s'affiche à chaque clic, même sur "Re-rendre"
    return shows.value.filter((s) => s.seen).length
  }
</script>

Ouvrez la console : getSeenCount appelée ! apparaît à chaque clic, y compris sur "Re-rendre" qui ne touche pas à shows.

C'est là qu'intervient computed().

computed()

computed() crée une valeur dérivée, recalculée automatiquement uniquement quand ses dépendances changent.


<template>
  <p>{{ seenCount }} série(s) vue(s) sur {{ shows.length }}</p>
  <button @click="shows[0].seen = !shows[0].seen">Basculer Inception</button>
  <button @click="count++">Re-rendre ({{ count }})</button>
</template>

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

  const count = ref(0)
  const shows = ref([
    {title: 'Inception', seen: true},
    {title: 'Dune', seen: false},
    {title: 'Interstellar', seen: false},
  ])

  const seenCount = computed(() => {
    console.log('seenCount recalculé !') // s'affiche UNIQUEMENT quand shows change
    return shows.value.filter((s) => s.seen).length
  })
</script>

Cliquez sur "Re-rendre" : le log n'apparaît plus. Vue réutilise la valeur mise en cache. Le calcul ne se relance que quand shows change réellement.

watch()

watch() permet d'exécuter du code en réaction au changement d'une valeur réactive. Utile pour déclencher des effets secondaires (appel API, log, etc.).


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

  const search = ref<string>('')

  watch(search, (newVal) => {
    console.log('Recherche :', newVal)
  })
</script>

Cycle de vie des composants

Vue appelle des hooks à des moments précis de la vie d'un composant. Le plus utilisé est onMounted, appelé une fois que le composant est inséré dans le DOM — idéal pour déclencher un appel API initial. onUpdated lui, est appelé après chaque mise à jour du DOM — ce qui correspond exactement aux moments où seenCount est recalculé :


<template>
  <input v-model="search" placeholder="Rechercher une série..."/>
  <p>{{ seenCount }} série(s) vue(s) sur {{ shows.length }}</p>
  <button @click="shows[0].seen = !shows[0].seen">Basculer Inception</button>
  <button @click="count++">Re-rendre ({{ count }})</button>
</template>

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

  const count = ref(0)
  const search = ref('')
  const shows = ref([
    {title: 'Inception', seen: true},
    {title: 'Dune', seen: false},
    {title: 'Interstellar', seen: false},
  ])

  const seenCount = computed(() => shows.value.filter((s) => s.seen).length)

  watch(search, (newVal) => {
    console.log('Recherche :', newVal)
  })

  onMounted(() => {
    console.log('Application prête, séries vues :', seenCount.value)
  })

  onUpdated(() => {
    console.log('DOM mis à jour, séries vues :', seenCount.value)
  })
</script>

Les principaux hooks :

HookMoment d'appel
onMountedAprès l'insertion du composant dans le DOM
onUpdatedAprès une mise à jour du DOM
onUnmountedAvant la destruction du composant
onBeforeMountJuste avant le montage

Documentation : La liste complète des hooks est disponible sur fr.vuejs.org/guide/essentials/lifecycle.

Directives Vue

Vue fournit des directives pour manipuler le DOM de manière déclarative directement dans le template.

v-if / v-else / v-else-if

Affiche ou masque un élément selon une condition. L'élément est créé ou détruit dans le DOM.


<template>
  <p v-if="seenShows.length === 0">Aucune série vue pour l'instant.</p>
  <ul>
    <li v-for="show in shows" :key="show.id">
      <span v-if="show.seen">✓ Vu</span>
      <span v-else>À voir</span>
    </li>
  </ul>
</template>

v-for

Génère une liste d'éléments dynamiquement. Toujours utiliser :key avec un identifiant unique.


<template>
  <ul>
    <li v-for="show in shows" :key="show.id">
      {{ show.title }} ({{ show.year }}) — {{ show.genre }}
    </li>
  </ul>
</template>

v-bind

Lie dynamiquement une valeur à un attribut HTML. Le raccourci est :.


<template>
  <!-- Ajoute la classe CSS "seen" si la série a été vue -->
  <li v-for="show in shows" :key="show.id" :class="{ seen: show.seen }">
    {{ show.title }}
  </li>
</template>

v-model

v-model synchronise automatiquement un champ de formulaire avec une variable réactive dans les deux sens :

  • Quand la variable change dans le script → le champ se met à jour
  • Quand l'utilisateur tape dans le champ → la variable se met à jour

C'est ce qu'on appelle une liaison bidirectionnelle.

Sans v-model, il faudrait gérer ces deux directions manuellement :


<template>
  <!-- Sans v-model : gestion manuelle -->
  <input :value="search" @input="search = $event.target.value"/>

  <!-- Avec v-model : équivalent, mais en une directive -->
  <input v-model="search" placeholder="Rechercher une série..."/>
</template>

v-on

Écoute des événements DOM. Le raccourci est @.


<template>
  <!-- Forme longue -->
  <button v-on:click="show.seen = !show.seen">Basculer</button>

  <!-- Raccourci @ -->
  <button @click="show.seen = !show.seen">Basculer</button>
</template>

Différence v-bind vs v-model

  • v-bind (:) : liaison unidirectionnelle (données → DOM). Pour les attributs comme src, href, class.
  • v-model : liaison bidirectionnelle (données ↔ DOM). Pour les inputs, selects, textareas.

Exemple complet

Voici le composant Show Watchlist qui combine tous les concepts du chapitre :


<template>
  <div>
    <input v-model="search" placeholder="Rechercher une série..."/>
    <p v-if="seenShows.length === 0">Aucune série vue pour l'instant.</p>
    <ul>
      <li v-for="show in shows" :key="show.id" :class="{ seen: show.seen }">
        {{ show.title }} ({{ show.year }}) — {{ show.genre }}
        <span v-if="show.seen">✓ Vu</span>
        <span v-else>À voir</span>
        <button @click="show.seen = !show.seen">Basculer</button>
      </li>
    </ul>
    <p>{{ seenShows.length }} série(s) vue(s) sur {{ shows.length }}</p>
  </div>
</template>

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

  interface Show {
    id: number
    title: string
    genre: string
    year: number
    seen: boolean
  }

  const search = ref<string>('')

  const shows = ref<Show[]>([
    {id: 1, title: 'Inception', genre: 'Sci-Fi', year: 2010, seen: true},
    {id: 2, title: 'The Dark Knight', genre: 'Action', year: 2008, seen: false},
    {id: 3, title: 'Interstellar', genre: 'Sci-Fi', year: 2014, seen: false},
    {id: 4, title: 'Parasite', genre: 'Thriller', year: 2019, seen: true},
    {id: 5, title: 'Dune', genre: 'Sci-Fi', year: 2021, seen: false},
    {id: 6, title: 'Everything Everywhere All at Once', genre: 'Action', year: 2022, seen: true},
  ])

  const seenShows = computed(() =>
      shows.value.filter((m) => m.seen)
  )

  watch(search, (newVal) => {
    console.log('Recherche :', newVal)
  })

  onMounted(() => {
    console.log('Application prête')
  })

  onUpdated(() => {
    console.log('DOM mis à jour, séries vues :', seenShows.value.length)
  })
</script>