Passer au contenu

Fondamentaux de la réactivité

Préférence d'API

Cette page et de nombreux autres chapitres plus loin dans le guide contiennent un contenu différent pour l'Options API et la Composition API. Actuellement, votre préférence est l'Options APIla Composition API. Vous pouvez passer d'un style d'API à l'autre à l'aide des boutons "Préférence d'API" situés en haut de la barre latérale gauche.

Déclarer un état réactif

Avec l'Options API, nous utilisons l'option data pour déclarer l'état réactif d'un composant. La valeur de l'option doit être une fonction qui renvoie un objet. Vue appellera la fonction lors de la création d'une nouvelle instance de composant, et enveloppera l'objet retourné dans son système de réactivité. Toutes les propriétés de premier niveau de cet objet sont transmises à l'instance du composant (this dans les méthodes et dans les hooks du cycle de vie) :

js
export default {
  data() {
    return {
      count: 1
    }
  },

  // `mounted` est un hook du cycle de vie que nous aborderons plus tard
  mounted() {
    // `this` fait référence à l'instance du composant.
    console.log(this.count) // => 1

    // les données peuvent également être mutées
    this.count = 2
  }
}

Essayer en ligne

Ces propriétés d'instance ne sont ajoutées que lors de la première création de l'instance, vous devez donc vous assurer qu'elles sont toutes présentes dans l'objet retourné par la fonction data. Si besoin, utilisez null, undefined ou une autre valeur temporaire pour les propriétés pour lesquelles la valeur souhaitée n'est pas encore disponible.

Vous pouvez ajouter une nouvelle propriété directement à this sans l'inclure dans data. Cependant, les propriétés ajoutées de cette manière ne pourront pas déclencher de mises à jour réactives.

Vue utilise un préfixe $ lorsqu'il expose ses propres API natives via l'instance du composant. Il réserve également le préfixe _ pour les propriétés internes. Il faut éviter d'utiliser des noms qui commencent par l'un de ces caractères pour les propriétés data à sa racine.

Proxy réactif vs. original

Dans Vue 3, les données sont rendues dynamiques en tirant parti des proxys JavaScript. Les utilisateurs venant de Vue 2 doivent être conscients du cas limite suivant :

js
export default {
  data() {
    return {
      someObject: {}
    }
  },
  mounted() {
    const newObject = {}
    this.someObject = newObject

    console.log(newObject === this.someObject) // faux
  }
}

Lorsque vous accédez à this.someObject après lui avoir assigné une valeur, la valeur est un proxy réactif de l'objet d'origine newObject. Contrairement à Vue 2, le newObject d'origine reste intact et ne sera pas rendu réactif : assurez-vous de toujours accéder à l'état réactif comme une propriété de this.

Déclarer un état réactif

ref()

Avec la Composition API, la méthode recommandée pour déclarer l'état réactif consiste à utiliser la fonction ref() :

js
import { ref } from 'vue'

const count = ref(0)

ref() prend l'argument et le renvoie enveloppé dans un objet ref avec une propriété .value :

js
const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

Voir aussi : Typer les variables réactives

Pour utiliser un état réactif dans le template d'un composant, déclarez et renvoyez-le depuis la fonction setup() du composant :

js
import { ref } from 'vue'

export default {
  // `setup` est un hook spécial dédié à la Composition API.
  setup() {
    const count = ref(0)

    // expose l'état au template
    return {
      count
    }
  }
}
template
<div>{{ count }}</div>

Notez que nous n'avons pas besoin d'ajouter .value lors de l'utilisation de la ref dans le template. Pour plus de commodité, les refs sont automatiquement déballées lorsqu'elles sont utilisées dans des templates (avec quelques pièges).

Vous pouvez également muter une ref directement dans les gestionnaires d'événements :

template
<button @click="count++">
  {{ count }}
</button>

Pour une logique plus complexe, nous pouvons déclarer des fonctions qui modifient les ref dans la même portée et les exposer en tant que méthodes à côté de l'état :

js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      // .value est nécessaire en JavaScript
      count.value++
    }

    // n'oubliez pas d'également exposer la fonction.
    return {
      count,
      increment
    }
  }
}

Les méthodes exposées sont généralement utilisées comme écouteurs d'événements :

template
<button @click="increment">
  {{ count }}
</button>

Voici l'exemple sur Codepen, sans utiliser d'outils de build.

<script setup>

Exposer manuellement l'état et les méthodes via setup() peut être verbeux. Heureusement, cela peut être évité avec l'utilisation de composants monofichiers (SFC). Nous pouvons simplifier l'utilisation avec <script setup> :

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>

Essayer en ligne

Les importations de premier niveau et les variables déclarées dans <script setup> sont automatiquement utilisables dans le template du même composant. Considérez le template comme une fonction JavaScript déclarée dans la même portée - il a naturellement accès à tout ce qui est déclaré à ses côtés.

TIP

Pour le reste du guide, nous utiliserons principalement la syntaxe monofichier + <script setup> pour les exemples de code de la Composition API, car c'est l'utilisation la plus courante pour les développeurs Vue.

Si vous n'utilisez pas SFC, vous pouvez toujours utiliser la Composition API avec l'option setup().

Why Refs?

You might be wondering why we need refs with the .value instead of plain variables. To explain that, we will need to briefly discuss how Vue's reactivity system works.

When you use a ref in the template, and changes the ref's value later, Vue automatically detects the change and updates the DOM accordingly. This is made possible with a dependency-tracking based reactivity system. When a component is rendered for the first time, Vue tracks every ref that was used during the render. Later on, when a ref is mutated, it will trigger re-render for components that are tracking it.

In standard JavaScript, there is no way to detect the access or mutation of plain variables. But we can intercept a property's get and set operations.

The .value property gives Vue the opportunity to detect when a ref has been accessed or mutated. Under the hood, Vue performs the tracking in its getter, and performs triggering in its setter. Conceptually, you can think of a ref as an object that looks like this:

js
// pseudo code, not actual implementation
const myRef = {
  _value: 0,
  get value() {
    track()
    return this._value
  },
  set value(newValue) {
    this._value = newValue
    trigger()
  }
}

Another nice trait of refs is that unlike plain variables, you can pass refs into functions while retaining access to the latest value and the reactivity connection. This is particularly useful when refactoring complex logic into reusable code.

The reactivity system is discussed in more details in the Reactivity in Depth section.

Déclarer des méthodes

Pour ajouter des méthodes à une instance d'un composant, nous utilisons l'option methods. Il doit s'agir d'un objet contenant les méthodes souhaitées :

js
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // les méthodes peuvent être appellées dans les hooks du cycle de vie, ou dans d'autres méthodes !
    this.increment()
  }
}

Vue lie automatiquement la valeur this pour methods afin qu'elle se réfère toujours à l'instance du composant. Cela garantit qu'une méthode conserve la bonne valeur this si elle est utilisée comme un écouteur d'événement ou comme une fonction de rappel. Vous devez éviter d'utiliser des fonctions fléchées lorsque vous définissez des méthodes, car cela empêche Vue de lier la valeur this appropriée :

js
export default {
  methods: {
    increment: () => {
      // À éviter: pas d'accès à `this` ici!
    }
  }
}

Comme toutes les autres propriétés de l'instance du composant, les méthodes sont accessibles depuis le template du composant. Dans un template, elles sont le plus souvent utilisées comme des écouteurs d'événements :

template
<button @click="increment">{{ count }}</button>

Essayer en ligne

Dans l'exemple ci-dessus, la méthode increment sera appelée lorsque l'on clique sur le <button>.

Réactivité profonde

Dans Vue, l'état est profondément réactif par défaut. Cela signifie que vous pouvez vous attendre à ce que les changements soient détectés même lorsque vous modifiez des objets ou des tableaux imbriqués :

js
export default {
  data() {
    return {
      obj: {
        nested: { count: 0 },
        arr: ['foo', 'bar']
      }
    }
  },
  methods: {
    mutateDeeply() {
      // cela va fonctionner comme prévu.
      this.obj.nested.count++
      this.obj.arr.push('baz')
    }
  }
}

Refs can hold any value type, including deeply nested objects, arrays, or JavaScript built-in data structures like Map.

A ref will make its value deeply reactive. This means you can expect changes to be detected even when you mutate nested objects or arrays:

js
import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // cela va fonctionner comme prévu.
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

Non-primitive values are turned into reactive proxies via reactive(), which is discussed below.

It is also possible to opt-out of deep reactivity with shallow refs. For shallow refs, only .value access is tracked for reactivity. Shallow refs can be used for optimizing performance by avoiding the observation cost of large objects, or in cases where the inner state is managed by an external library.

Further reading:

Timing de mise à jour du DOM

Lorsque vous modifiez un état réactif, le DOM est automatiquement mis à jour. Toutefois, il convient de noter que les mises à jour du DOM ne sont pas appliquées de manière synchrone. En effet, Vue les met en mémoire tampon jusqu'au prochain "tick" du cycle de mises à jour pour s'assurer que chaque composant ne soit mis à jour qu'une seule fois, quel que soit le nombre de modifications d'état que vous avez effectuées.

Pour attendre que la mise à jour du DOM soit terminée après un changement d'état, vous pouvez utiliser l'API globale nextTick() :

js
import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // Maintenant le DOM est mis à jour
}
js
import { nextTick } from 'vue'

export default {
  methods: {
    async increment() {
      this.count++
      await nextTick()
      // Maintenant le DOM est mis à jour
    }
  }
}

reactive()

There is another way to declare reactive state, with the reactive() API. Unlike a ref which wraps the inner value in a special object, reactive() makes an object itself reactive:

js
import { reactive } from 'vue'

const state = reactive({ count: 0 })

See also: Typing Reactive

Usage in template:

template
<button @click="state.count++">
  {{ state.count }}
</button>

Les objets réactifs sont des proxys JavaScript et se comportent comme des objets classiques. La différence est que Vue est capable de traquer l'accès aux propriétés et les mutations d'un objet réactif.

reactive() converts the object deeply: nested objects are also wrapped with reactive() when accessed. It is also called by ref() internally when the ref value is an object. Similar to shallow refs, there is also the shallowReactive() API for opting-out of deep reactivity.

Proxy réactif vs. original

Il est important de noter que la valeur retournée par reactive() est un proxy de l'objet original, qui n'est pas égal à l'objet original :

js
const raw = {}
const proxy = reactive(raw)

// le proxy n'est PAS égal à l'original.
console.log(proxy === raw) // faux

Seul le proxy est réactif - muter l'objet original ne déclenchera pas de mises à jour. Par conséquent, la meilleure pratique pour travailler avec le système de réactivité de Vue est d'utiliser exclusivement les versions proxifiées de votre état.

Pour assurer un accès cohérent au proxy, appeler reactive() sur le même objet retournera toujours le même proxy, et appeler reactive() sur un proxy existant retournera également ce même proxy :

js
// appeler reactive() sur le même objet retourne le même proxy
console.log(reactive(raw) === proxy) // true

// appeler reactive() sur un proxy se retourne lui-même
console.log(reactive(proxy) === proxy) // vrai

Cette règle s'applique tout aussi bien aux objets imbriqués. En raison de la réactivité profonde, les objets imbriqués à l'intérieur d'un objet réactif sont également des proxys :

js
const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

Limitations de reactive()

L'API reactive() a deux limitations :

  1. Types limités : Elle ne fonctionne que pour les types d'objets (objets, tableaux et objets de type collections tels que Map et Set). Elle ne peut pas contenir les types primitifs tels que string, number ou boolean.

  2. Remplacement impossible de l'intégralité de l'objet : Comme le suivi de la réactivité de Vue fonctionne sur l'accès aux propriétés, nous devons toujours conserver la même référence à l'objet réactif. Cela signifie que nous ne pouvons pas facilement "remplacer" un objet réactif car la connexion de réactivité à la première référence serait perdue :

    js
    let state = reactive({ count: 0 })
    
    // la référence précédente ({ count: 0 }) n'est plus suivie 
    // (la connexion de réactivité est perdue !)
    state = reactive({ count: 1 })
  3. Ne fonctionne pas avec la destructuration : lorsque nous déstructurons la propriété d'un objet réactif en variables locales, ou lorsque nous passons cette propriété dans une fonction, nous perdons la connexion à la réactivité :

    js
    const state = reactive({ count: 0 })
    
    // count est déconnecté de state.count lorsqu'il est déstructuré.
    let { count } = state
    // n'affecte pas l'état original
    count++
    
    // la fonction reçoit un simple nombre et
    // ne sera pas capable de traquer les changements de state.count
    // nous devons passer l'objet entier pour conserver la réactivité
    callSomeFunction(state.count)

À cause de ces limites, nous recommandons l'usage de ref() en tant qu'API principale pour déclarer un état réactif.

As Reactive Object Property

A ref is automatically unwrapped when accessed or mutated as a property of a reactive object. In other words, it behaves like a normal property :

js
const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

If a new ref is assigned to a property linked to an existing ref, it will replace the old ref:

js
const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// original ref is now disconnected from state.count
console.log(count.value) // 1

Ref unwrapping only happens when nested inside a deep reactive object. It does not apply when it is accessed as a property of a shallow reactive object.

Pièges lors de déballage de tableaux et collections

Unlike reactive objects, there is no unwrapping performed when the ref is accessed as an element of a reactive array or a native collection type like Map:

js
const books = reactive([ref('Vue 3 Guide')])
// need .value here
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// need .value here
console.log(map.get('count').value)

Ref unwrapping in templates only applies if the ref is a top-level property in the template render context.

In the example below, count and object are top-level properties, but object.id is not:

js
const count = ref(0)
const object = { id: ref(0) }

Therefore, this expression works as expected:

template
{{ count + 1 }}

...while this one does NOT:

template
{{ object.id + 1 }}

The rendered result will be [object Object]1 because object.id is not unwrapped when evaluating the expression and remains a ref object. To fix this, we can destructure id into a top-level property:

js
const { id } = object
template
{{ id + 1 }}

Désormais le résultat rendu sera 2.

Another thing to note is that a ref does get unwrapped if it is the final evaluated value of a text interpolation (i.e. a {{ }} tag), so the following will render 1:

template
{{ object.id }}

This is just a convenience feature of text interpolation and is equivalent to {{ object.id.value }}.

Méthodes avec état

Dans certains cas, nous pouvons avoir besoin de créer dynamiquement une méthode, par exemple en créant un gestionnaire d'événements debounced :

js
import { debounce } from 'lodash-es'

export default {
  methods: {
    // Debounce avec Lodash
    click: debounce(function () {
      // ... répond au clic ...
    }, 500)
  }
}

Toutefois, cette approche est problématique pour les composants réutilisés, car une fonction debounced possède un état : elle maintient un état interne sur le temps écoulé. Si plusieurs instances de composants partagent la même fonction debounced, elles interféreront les unes avec les autres.

Pour que la fonction debounced de chaque instance de composant soit indépendante des autres, nous pouvons créer la version debounced dans le hook de cycle de vie created :

js
export default {
  created() {
    // chaque instance a maintenant sa propre copie du gestionnaire debounced
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // c'est aussi une bonne idée d'annuler le minuteur
    // lorsque le composant est supprimé
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... répond au clic ...
    }
  }
}
Fondamentaux de la réactivitéa chargé