1. v-for avancé — tableaux, objets, ranges
v-for est la directive de rendu de liste de Vue. Elle supporte plusieurs sources de données.
v-for sur un tableau
<!-- Valeur seule -->
<li v-for="fruit in fruits">{{ fruit }}</li>
<!-- Valeur + index -->
<li v-for="(fruit, index) in fruits">
{{ index + 1 }}. {{ fruit }}
</li>
<!-- Objet dans tableau -->
<li v-for="(produit, i) in produits" :key="produit.id">
{{ i + 1 }}. {{ produit.nom }} — {{ produit.prix }}€
</li>
v-for sur un objet
<!-- Valeur seule -->
<li v-for="valeur in utilisateur">{{ valeur }}</li>
<!-- Clé + valeur -->
<li v-for="(valeur, cle) in utilisateur">
<strong>{{ cle }}:</strong> {{ valeur }}
</li>
<!-- Clé + valeur + index -->
<li v-for="(valeur, cle, index) in utilisateur">
{{ index }}. {{ cle }}: {{ valeur }}
</li>
data() {
return {
utilisateur: {
nom: 'Alice',
email: 'alice@test.fr',
role: 'admin',
age: 28
}
};
}
// Rendu : nom: Alice / email: alice@test.fr / role: admin / age: 28
v-for avec un range (n in N)
<!-- Affiche les chiffres 1 à 5 -->
<span v-for="n in 5" :key="n">{{ n }}</span>
<!-- → 1 2 3 4 5 -->
<!-- Étoiles de notation -->
<span v-for="i in 5" :key="i">
{{ i <= note ? '★' : '☆' }}
</span>
<!-- Grille de placeholder -->
<div v-for="n in nombreColonnes" :key="n" class="skeleton"></div>
v-for est déclaratif et réactif : quand le tableau change, le DOM se met à jour automatiquement. Pas besoin de manipuler le DOM manuellement.
2. :key — l'importance d'une clé stable
La prop :key aide Vue à identifier chaque élément de liste. Elle est obligatoire et doit être unique et stable.
Pourquoi :key est crucial
<!-- ❌ Sans :key : Vue réutilise les nœuds dans le mauvais ordre -->
<li v-for="item in items">{{ item.nom }}</li>
<!-- ❌ :key="index" : instable si on ajoute/supprime au milieu -->
<li v-for="(item, i) in items" :key="i">{{ item.nom }}</li>
<!-- ✅ :key="id" stable : Vue peut optimiser les mises à jour -->
<li v-for="item in items" :key="item.id">{{ item.nom }}</li>
Problème avec l'index comme clé
// État initial : [A, B, C] avec keys [0, 1, 2]
// Si on supprime B → [A, C] avec keys [0, 1]
// Vue pense que c'est B qui a changé, pas qu'il a été supprimé
// → Animations incorrectes, état de composants perdus
// Avec :key="item.id", Vue sait exactement quel élément est supprimé
// → Comportement correct, animation précise
Quand utiliser l'index ?
<!-- ✅ OK si la liste est statique (jamais réordonnée/supprimée) -->
<div v-for="(tab, i) in onglets" :key="i">{{ tab }}</div>
<!-- ✅ OK pour les ranges (1 to N) -->
<div v-for="n in 5" :key="n">{{ n }}</div>
<!-- ❌ JAMAIS pour des listes dynamiques avec CRUD -->
:key pour les listes dynamiques. Si vos données n'ont pas d'id, générez-en un (Date.now() + Math.random() à la création).
3. Mutations vs remplacement de tableau
Vue observe les méthodes de mutation de tableau (qui modifient le tableau en place) et déclenche les mises à jour du DOM automatiquement.
Méthodes mutantes (réactives)
// Ces méthodes MODIFIENT le tableau original — Vue les détecte
this.items.push(nouvelItem) // Ajouter à la fin
this.items.pop() // Supprimer le dernier
this.items.shift() // Supprimer le premier
this.items.unshift(item) // Ajouter au début
this.items.splice(index, 1) // Supprimer à un index
this.items.splice(index, 0, item)// Insérer à un index
this.items.sort((a, b) => ...) // Trier en place
this.items.reverse() // Inverser en place
Méthodes non-mutantes (retournent un nouveau tableau)
// Ces méthodes retournent un NOUVEAU tableau — réaffecter pour déclencher la réactivité
this.items = this.items.filter(i => i.actif)
this.items = this.items.map(i => ({ ...i, lu: true }))
this.items = this.items.concat(autresItems)
this.items = [...this.items, nouvelItem] // spread
this.items[0] = nouvelItem) — ce qui n'était pas le cas dans Vue 2 !
Patterns CRUD courants
methods: {
// Ajouter
ajouterItem(item) {
this.items.push({ id: Date.now(), ...item });
},
// Supprimer
supprimerItem(id) {
this.items = this.items.filter(i => i.id !== id);
// Ou : this.items.splice(this.items.findIndex(i => i.id === id), 1);
},
// Modifier
modifierItem(id, changes) {
const item = this.items.find(i => i.id === id);
if (item) Object.assign(item, changes);
// Ou : this.items = this.items.map(i => i.id === id ? {...i, ...changes} : i);
},
// Basculer un booléen
toggleFavori(id) {
const item = this.items.find(i => i.id === id);
if (item) item.favori = !item.favori;
}
}
4. v-if + v-for — priorité et <template>
En Vue 3, v-if a une priorité plus haute que v-for. Cela signifie que v-if s'évalue en premier — ce qui peut causer des bugs si vous les combinez sur le même élément.
Ne pas combiner sur le même élément
<!-- ❌ Mauvais : v-if sur le même élément que v-for -->
<li
v-for="item in items"
v-if="item.actif" <!-- v-if est évalué en PREMIER -->
:key="item.id"
>
<!-- En Vue 3 : item est accessible car v-if sur même nœud -->
<!-- MAIS : selon la version ça peut planter si item n'existe pas -->
</li>
Solution : wrapper <template>
<!-- ✅ Bon : v-for sur <template> (wrapper sans rendu) -->
<template v-for="item in items" :key="item.id">
<li v-if="item.actif">
{{ item.nom }}
</li>
</template>
<!-- ✅ Encore mieux : utiliser computed pour filtrer -->
<li v-for="item in itemsActifs" :key="item.id">
{{ item.nom }}
</li>
computed: {
// Filtrage dans computed : plus propre et plus performant
itemsActifs() {
return this.items.filter(i => i.actif);
}
}
computed pour filtrer. Le template reste lisible, et le filtrage est mis en cache automatiquement.
v-if conditionnel sur les sections
<!-- Section entière conditionnelle -->
<template v-if="items.length > 0">
<h3>{{ items.length }} résultats</h3>
<ul>
<li v-for="item in items" :key="item.id">{{ item.nom }}</li>
</ul>
</template>
<template v-else>
<p class="empty-state">Aucun résultat trouvé.</p>
</template>
5. Tri et filtrage réactifs avec computed
Utilisez des propriétés computed pour trier et filtrer les listes. C'est plus propre que de modifier le tableau source et les résultats sont mis en cache.
Filtre par recherche
data() {
return {
contacts: [
{ id: 1, nom: 'Alice Martin', email: 'alice@test.fr' },
{ id: 2, nom: 'Bob Dupont', email: 'bob@test.fr' },
{ id: 3, nom: 'Alice Wang', email: 'awang@test.fr' }
],
recherche: ''
};
},
computed: {
contactsFiltres() {
const q = this.recherche.toLowerCase().trim();
if (!q) return this.contacts;
return this.contacts.filter(c =>
c.nom.toLowerCase().includes(q) ||
c.email.toLowerCase().includes(q)
);
}
}
<input v-model="recherche" placeholder="Rechercher..." />
<p>{{ contactsFiltres.length }} / {{ contacts.length }} contacts</p>
<li v-for="c in contactsFiltres" :key="c.id">{{ c.nom }}</li>
Tri dynamique
data() {
return {
triChamp: 'nom',
triOrdre: 'asc' // 'asc' ou 'desc'
};
},
computed: {
contactsTries() {
return [...this.contactsFiltres].sort((a, b) => {
const va = a[this.triChamp];
const vb = b[this.triChamp];
const compare = String(va).localeCompare(String(vb));
return this.triOrdre === 'asc' ? compare : -compare;
});
}
},
methods: {
changerTri(champ) {
if (this.triChamp === champ) {
this.triOrdre = this.triOrdre === 'asc' ? 'desc' : 'asc';
} else {
this.triChamp = champ;
this.triOrdre = 'asc';
}
}
}
Pagination
data() {
return {
pageCourante: 1,
itemsParPage: 5
};
},
computed: {
totalPages() {
return Math.ceil(this.contactsTries.length / this.itemsParPage);
},
contactsPage() {
const debut = (this.pageCourante - 1) * this.itemsParPage;
return this.contactsTries.slice(debut, debut + this.itemsParPage);
}
},
watch: {
// Revenir à la page 1 si la recherche change
recherche() { this.pageCourante = 1; }
}
6. Listes virtualisées — aperçu pour les grandes listes
Quand une liste contient des milliers d'éléments, le rendu de tous les nœuds DOM en même temps est coûteux. La virtualisation résout ce problème en ne rendant que les éléments visibles à l'écran.
Principe de la virtualisation
// Sans virtualisation : 10 000 items = 10 000 nœuds DOM
// Avec virtualisation : 10 000 items = seulement ~20 nœuds DOM visibles
// Le composant calcule quels items sont dans la fenêtre visible
// et ne crée les nœuds que pour ceux-là
Implémentation simple (sans librairie)
data() {
return {
items: Array.from({ length: 10000 }, (_, i) => ({
id: i,
nom: `Item ${i + 1}`
})),
defilement: 0,
hauteurItem: 40,
hauteurFenetre: 400,
margeHaut: 5 // Nombre d'items "buffer" au-dessus
};
},
computed: {
indexDebut() {
return Math.max(0, Math.floor(this.defilement / this.hauteurItem) - this.margeHaut);
},
indexFin() {
const visible = Math.ceil(this.hauteurFenetre / this.hauteurItem);
return Math.min(this.items.length, this.indexDebut + visible + this.margeHaut * 2);
},
itemsVisibles() {
return this.items.slice(this.indexDebut, this.indexFin);
},
hauteurTotale() {
return this.items.length * this.hauteurItem;
},
offsetHaut() {
return this.indexDebut * this.hauteurItem;
}
}
Librairies pour Vue 3
// Option 1 : @vueuse/virtual-list (VueUse)
import { useVirtualList } from '@vueuse/core';
const { list, containerProps, wrapperProps } = useVirtualList(
myItems,
{ itemHeight: 40, overscan: 5 }
);
// Option 2 : vue-virtual-scroller
// import { RecycleScroller } from 'vue-virtual-scroller'