Vue 3
Framework Progressif
Single File Components, Composition API, ref/reactive, computed, watch, directives, slots, Vue Router, Pinia — Vue 3 avec <script setup>.
Qu'est-ce que Vue ?
Vue est un framework progressif créé par Evan You en 2014. "Progressif" signifie qu'on peut l'adopter graduellement — d'un simple script dans une page HTML jusqu'à une SPA complète. Vue 3 (2020) introduit la Composition API, TypeScript natif et des performances améliorées via le compilateur.
Accessible — syntaxe HTML familière
Performant — Virtual DOM optimisé,
réactivité fine
Flexible — library ou framework complet
Vue occupe le "juste milieu" :
React Vue Angular
Bibliothèque Framework Framework
progressif complet
Liberté max Équilibre Conventions
liberté/ strictes
structure
Forces distinctives :
→ Templates HTML lisibles (pas de JSX)
→ Réactivité automatique (pas de setState)
→ Single File Component = tout en 1 fichier
→ Courbe d'apprentissage douce
→ Très utilisé en Asie, Europe, startups
<!-- Intégration directe sans npm -->
<script src="https://unpkg.com/vue@3"></script>
<div id="app">
<p>{{ message }}</p>
<button @click="count++">{{ count }}</button>
</div>
<script>
Vue.createApp({
data() {
return { message: 'Bonjour Vue !', count: 0 };
}
}).mount('#app');
</script>
// C'est tout ! Vue réagit automatiquement
// aux changements de data → DOM mis à jour
// Pour une vraie app → utiliser Vite + Vue :
npm create vite@latest mon-app -- --template vue-ts
cd mon-app && npm install && npm run dev
Installation & CLI
# Avec Vite (recommandé)
npm create vite@latest mon-app -- --template vue-ts
cd mon-app && npm install && npm run dev
# Avec create-vue (officiel)
npm create vue@latest
# → TypeScript ? Yes
# → JSX ? No
# → Vue Router ? Yes
# → Pinia ? Yes
# → Vitest ? Yes
# → ESLint ? Yes
# Structure d'un projet Vue + Vite :
src/
├── assets/ ← images, fonts
├── components/ ← composants réutilisables
│ └── Button.vue ← Single File Component
├── composables/ ← logique réutilisable (=hooks)
├── pages/ (ou views/)← pages/routes
├── router/
│ └── index.ts
├── stores/ ← Pinia stores
├── App.vue ← composant racine
└── main.ts ← point d'entrée
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import router from './router';
import App from './App.vue';
import './assets/main.css';
const app = createApp(App);
app.use(createPinia()); // état global
app.use(router); // routing
app.mount('#app');
// App.vue — racine
<template>
<RouterView /> <!-- page courante -->
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router';
</script>
Single File Components (.vue)
<template>
<div class="card">
<h2>{{ titre }}</h2>
<p>Prix : {{ prix }} €</p>
<button @click="acheter">
Acheter
</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
titre: string;
prix: number;
}>();
const emit = defineEmits<{
achat: [qte: number];
}>();
function acheter() {
emit('achat', 1);
}
</script>
<style scoped>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
/* scoped = appliqué à CE composant seulement
→ .card[data-v-xxxxxxxx] en prod */
</style>
<!-- ParentPage.vue -->
<template>
<ProductCard
titre="Clavier mécanique"
:prix="89"
@achat="onAchat"
/>
</template>
<script setup lang="ts">
import ProductCard from './ProductCard.vue';
function onAchat(qte: number) {
console.log(`Acheté : ${qte}`);
}
</script>
// Avantages du SFC :
// ✅ Tout en un seul fichier
// ✅ CSS scopé automatiquement
// ✅ Autocomplétion IDE parfaite (Volar)
// ✅ Compilation optimisée
// ✅ Hot Module Replacement précis
// ✅ Pas de conflit de classes CSS
Installer l'extension Vue - Official (Volar) dans VS Code pour avoir l'autocomplétion, le typage et le formatting dans les fichiers .vue.
Options API vs Composition API
Recommandation : utiliser la Composition API avec <script setup> pour tout nouveau projet Vue 3. C'est plus concis, mieux typé, et permet de créer des composables réutilisables. L'Options API reste supportée mais n'est plus l'approche recommandée.
ref & reactive — l'état réactif
import { ref } from 'vue';
// ref() enveloppe la valeur dans un objet .value
const count = ref(0);
const nom = ref('Alice');
const actif = ref(true);
const items = ref<string[]>([]);
// Lire → .value dans <script>
console.log(count.value); // 0
console.log(nom.value); // 'Alice'
// Modifier → .value dans <script>
count.value++;
count.value = 42;
nom.value = 'Bob';
items.value.push('nouveau');
// Dans le <template> → .value automatique
// <p>{{ count }}</p> ← pas de .value !
// <p>{{ nom }}</p>
// ref avec objet
const user = ref({ nom: 'Alice', age: 28 });
user.value.nom = 'Bob'; // ✅ réactif
user.value = { nom: 'Bob', age: 30 }; // ✅ aussi réactif
import { reactive, toRefs } from 'vue';
// reactive() — objet réactif sans .value
const state = reactive({
count: 0,
nom: 'Alice',
items: [] as string[],
});
// Lire / modifier directement
console.log(state.count);
state.count++;
state.nom = 'Bob';
state.items.push('nouveau');
// ⚠ Pièges de reactive :
// 1. Pas de remplacement de l'objet entier
state = { count: 1 }; // ✗ perd la réactivité
Object.assign(state, { count: 1 }); // ✅
// 2. Destructuring perd la réactivité
const { count } = state; // ✗ count n'est plus réactif
// ✅ Utiliser toRefs pour destructurer
const { count, nom } = toRefs(state);
// count et nom sont maintenant des refs
// Conseil général :
// ref() → valeurs primitives ou si besoin
// de remplacer l'objet entier
// reactive() → formulaires, état complexe
computed & watch
import { ref, computed } from 'vue';
const items = ref([
{ nom: 'Clavier', prix: 45, actif: true },
{ nom: 'Souris', prix: 25, actif: false },
{ nom: 'Écran', prix: 320, actif: true },
]);
const filtre = ref('');
// Recalculé seulement si items ou filtre change
const itemsFiltres = computed(() =>
items.value.filter(i =>
i.nom.toLowerCase().includes(filtre.value.toLowerCase())
)
);
const total = computed(() =>
items.value.reduce((s, i) => s + i.prix, 0)
);
// computed accessible en lecture seule
// <p>Total : {{ total }} €</p>
// computed en lecture/écriture (getter + setter)
const fullName = computed({
get: () => `${prenom.value} ${nom.value}`,
set: (val: string) => {
[prenom.value, nom.value] = val.split(' ');
}
});
import { ref, watch, watchEffect } from 'vue';
// watch — surveiller une source précise
const query = ref('');
watch(query, (newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`);
fetchResults(newVal);
});
// Options
watch(query, handler, {
immediate: true, // appeler immédiatement au montage
deep: true, // surveiller en profondeur les objets
once: true, // une seule fois
});
// Surveiller plusieurs sources
watch([prenom, nom], ([np, nn]) => {
console.log(np, nn);
});
// Surveiller une propriété d'objet
watch(() => user.value.age, (age) => {
console.log('Âge changé :', age);
});
// watchEffect — surveille automatiquement les dépendances
watchEffect(() => {
// Tout ce qui est lu ici est auto-suivi
console.log(query.value, filtre.value);
// Relancé si query OU filtre change
});
// Arrêter un watcher
const stop = watchEffect(() => { ... });
stop(); // arrêter
Cycle de vie
| Hook | Quand | Usage typique |
|---|---|---|
onBeforeMount | Avant le premier rendu DOM | Rarement utile |
onMounted | Après insertion dans le DOM | Fetch initial, bibliothèques DOM, focus |
onBeforeUpdate | Avant re-rendu | Lire le DOM avant update |
onUpdated | Après re-rendu | Opérations DOM post-update |
onBeforeUnmount | Avant destruction | Cleanup (timers, listeners) |
onUnmounted | Après destruction | Cleanup final |
onErrorCaptured | Erreur dans un enfant | Error boundary |
import { ref, onMounted, onUnmounted } from 'vue';
const users = ref([]);
const loading = ref(true);
let timer: number;
onMounted(async () => {
// Fetch initial
users.value = await fetch('/api/users').then(r => r.json());
loading.value = false;
// Ajouter un event listener
window.addEventListener('resize', handleResize);
// Timer
timer = setInterval(refresh, 30000);
});
onUnmounted(() => {
// Toujours nettoyer !
window.removeEventListener('resize', handleResize);
clearInterval(timer);
});
// Accéder au DOM avec templateRef
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputRef.value?.focus(); // disponible ici seulement
});
// <input ref="inputRef" />
Composables — logique réutilisable
// composables/useFetch.ts
import { ref, watchEffect } from 'vue';
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const loading = ref(true);
const error = ref<string | null>(null);
watchEffect(async () => {
loading.value = true;
error.value = null;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (e) {
error.value = (e as Error).message;
} finally {
loading.value = false;
}
});
return { data, loading, error };
}
// Utilisation dans n'importe quel composant :
const { data: users, loading, error } =
useFetch<User[]>('/api/users');
// composables/useLocalStorage.ts
export function useLocalStorage<T>(key: string, initialValue: T) {
const stored = localStorage.getItem(key);
const value = ref<T>(stored ? JSON.parse(stored) : initialValue);
watch(value, (val) => {
localStorage.setItem(key, JSON.stringify(val));
}, { deep: true });
return value;
}
// useMouse — position de la souris
export function useMouse() {
const x = ref(0);
const y = ref(0);
const update = (e: MouseEvent) => {
x.value = e.clientX;
y.value = e.clientY;
};
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
// Dans un composant :
const { x, y } = useMouse();
const theme = useLocalStorage('theme', 'dark');
// <p>Souris : {{ x }}, {{ y }}</p>
Directives
<!-- v-if / v-else-if / v-else -->
<p v-if="isLoggedIn">Bonjour !</p>
<p v-else-if="isLoading">Chargement…</p>
<p v-else>Veuillez vous connecter</p>
<!-- v-show — cache avec display:none (DOM présent) -->
<div v-show="isVisible">Je suis caché/visible</div>
<!-- v-for — itérer (toujours avec :key) -->
<li v-for="item in items" :key="item.id">
{{ item.nom }}
</li>
<!-- v-for avec index -->
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.nom }}
</li>
<!-- v-for sur un objet -->
<li v-for="(valeur, clé) in objetConfig" :key="clé">
{{ clé }} : {{ valeur }}
</li>
<!-- v-model — two-way binding -->
<input v-model="searchTerm" />
<textarea v-model="description"></textarea>
<input type="checkbox" v-model="accepte" />
<select v-model="pays">...</select>
<!-- v-bind (:) — lier une prop -->
<img :src="imageUrl" :alt="imageAlt" />
<button :disabled="!isValid">Envoyer</button>
<div :class="{ active: isActive, error: hasError }">
<div :class="[baseClass, conditionalClass]">
<div :style="{ color: textColor, fontSize: size + 'px' }">
<!-- v-on (@) — écouter un événement -->
<button @click="onSave">Sauvegarder</button>
<form @submit.prevent="onSubmit">
<input @keyup.enter="search" />
<!-- v-once — rendu une seule fois -->
<h1 v-once>{{ titre }}</h1>
// Directive personnalisée (v-focus)
const vFocus = {
mounted: (el: HTMLElement) => el.focus()
};
// Usage : <input v-focus />
// Directive globale
app.directive('tooltip', {
mounted(el, binding) {
el.title = binding.value;
}
});
// <span v-tooltip="'Aide ici'">
Data binding
<!-- 1. Interpolation {{ }} — texte uniquement -->
<p>{{ message }}</p>
<p>{{ count * 2 }}</p>
<p>{{ user?.nom ?? 'Inconnu' }}</p>
<!-- 2. v-bind (:) — props et attributs HTML -->
<a :href="url">Lien</a>
<img :src="imgUrl" />
<MonComposant :data="items" :loading="isLoading" />
<!-- Bind tous les attributs d'un objet -->
const attrs = { id: 'main', role: 'main', tabindex: 0 };
<div v-bind="attrs">
<!-- 3. v-model — two-way -->
<input v-model="searchTerm" />
<!-- Équivalent à :
<input :value="searchTerm"
@input="searchTerm = $event.target.value" /> -->
<!-- Modificateurs v-model -->
<input v-model.trim="nom" /> <!-- trim auto -->
<input v-model.number="age" /> <!-- convertir en number -->
<input v-model.lazy="texte" /> <!-- màj sur change, pas input -->
// Vue 3 — v-model sur composant
// Parent : <SearchInput v-model="query" />
// Équivalent : <SearchInput :modelValue="query"
// @update:modelValue="query = $event" />
// SearchInput.vue
const props = defineProps<{
modelValue: string;
}>();
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
// Template
// <input :value="props.modelValue"
// @input="emit('update:modelValue', $event.target.value)" />
// Multiple v-model (Vue 3)
// <Form v-model:prenom="fn" v-model:nom="nn" />
defineProps<{
prenom: string;
nom: string;
}>();
defineEmits<{
'update:prenom': [value: string];
'update:nom': [value: string];
}>();
Événements & modificateurs
<!-- Modificateurs d'événement -->
<form @submit.prevent="onSubmit"> <!-- preventDefault -->
<a @click.stop="handler"> <!-- stopPropagation -->
<div @click.self="handler"> <!-- que sur cet élément -->
<button @click.once="handler"> <!-- une seule fois -->
<div @scroll.passive="onScroll"> <!-- passive = perf scroll -->
<!-- Modificateurs de touches clavier -->
<input @keyup.enter="submit" /> <!-- Entrée -->
<input @keyup.esc="cancel" /> <!-- Échap -->
<input @keydown.ctrl.s="save" /> <!-- Ctrl+S -->
<input @keyup.alt.enter="go" /> <!-- Alt+Entrée -->
<!-- Modificateurs de souris -->
<button @click.right="ctxMenu"> <!-- clic droit -->
<div @click.middle="openTab"> <!-- clic molette -->
<!-- Combinaisons -->
<form @submit.prevent.stop="onSubmit">
// Déclarer les événements que le composant émet
const emit = defineEmits<{
click: []; // sans payload
change: [value: string]; // avec payload string
submit: [data: FormData];
deleted: [id: number];
updated: [id: number, data: User];
}>();
// Émettre
emit('click');
emit('change', 'nouvelle valeur');
emit('deleted', user.id);
// Parent — écouter
// <UserCard
// @deleted="onDeleted"
// @updated="onUpdated"
// />
function onDeleted(id: number) { ... }
function onUpdated(id: number, data: User) { ... }
// $event — récupérer le payload inline
// <UserCard @deleted="userId = $event" />
Slots
<!-- Card.vue -->
<template>
<div class="card">
<!-- Slot header nommé -->
<header>
<slot name="header">
Titre par défaut <!-- fallback -->
</slot>
</header>
<!-- Slot par défaut -->
<main>
<slot />
</main>
<!-- Slot footer nommé -->
<footer>
<slot name="footer" />
</footer>
</div>
</template>
<!-- Utilisation -->
<Card>
<template #header>
<h2>Mon titre</h2>
</template>
<p>Contenu principal</p> <!-- slot default -->
<template #footer>
<button>Fermer</button>
</template>
</Card>
<!-- Liste.vue — expose item au slot -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index">
{{ item.nom }} <!-- fallback -->
</slot>
</li>
</ul>
</template>
<!-- Parent — reçoit item via v-slot -->
<Liste :items="produits">
<template v-slot:default="{ item, index }">
<span>{{ index + 1 }}. {{ item.nom }}</span>
<strong>{{ item.prix }} €</strong>
</template>
</Liste>
<!-- Syntaxe courte #default -->
<Liste :items="produits">
<template #default="{ item }">
{{ item.nom }}
</template>
</Liste>
Props & emits
// Syntaxe TypeScript (recommandée)
const props = defineProps<{
titre: string;
description?: string; // optionnel
prix: number;
items: string[];
variant?: 'primary' | 'danger';
user?: User;
}>();
// Valeurs par défaut avec withDefaults
const props = withDefaults(
defineProps<{
count?: number;
label?: string;
variant?: 'primary' | 'secondary';
}>(),
{
count: 0,
label: 'Bouton',
variant: 'primary',
}
);
// Accéder aux props
console.log(props.titre);
console.log(props.count); // ou la valeur par défaut
// ✅ Ne jamais muter les props directement
// props.titre = 'test'; ← ✗ interdit
// Par défaut, <script setup> est fermé
// Le parent ne peut rien accéder directement
// Exposer des méthodes ou propriétés via ref parent
// MonInput.vue
const inputRef = ref<HTMLInputElement>();
const value = ref('');
function focus() {
inputRef.value?.focus();
}
function clear() {
value.value = '';
}
defineExpose({ focus, clear, value });
// Parent — utiliser via ref de template
const inputComp = ref<InstanceType<typeof MonInput>>();
function resetForm() {
inputComp.value?.clear();
inputComp.value?.focus();
}
// <MonInput ref="inputComp" />
// useAttrs() — attributs non-déclarés en prop
import { useAttrs } from 'vue';
const attrs = useAttrs();
// <input v-bind="attrs" /> — passer les attrs au DOM
provide / inject
import { provide, inject, ref, InjectionKey } from 'vue';
// Clé typée (recommandé)
const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme');
// Composant parent / App.vue
const theme = ref('dark');
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
};
// Fournir la valeur à tous les descendants
provide(ThemeKey, theme);
provide('toggleTheme', toggleTheme);
// N'importe quel composant descendant
const theme = inject(ThemeKey); // Ref<string> | undefined
const theme = inject(ThemeKey, ref('light')); // avec défaut
const toggle = inject<() => void>('toggleTheme');
// Utiliser dans le template
// <div :class="theme">...</div>
// <button @click="toggle">Changer thème</button>
// composables/useTheme.ts
const ThemeKey = Symbol() as InjectionKey<{
theme: Ref<string>;
toggle: () => void;
}>;
export function provideTheme() {
const theme = ref('dark');
const toggle = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
};
provide(ThemeKey, { theme, toggle });
return { theme, toggle };
}
export function useTheme() {
const ctx = inject(ThemeKey);
if (!ctx) throw new Error('useTheme() hors ThemeProvider');
return ctx;
}
// App.vue
provideTheme();
// N'importe quel composant :
const { theme, toggle } = useTheme();
Teleport & Suspense
<!-- Rend le contenu ailleurs dans le DOM
Utile pour : modals, tooltips, notifications
qui doivent être dans <body> pour le z-index -->
<template>
<button @click="showModal = true">
Ouvrir
</button>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<slot />
<button @click="showModal = false">
Fermer
</button>
</div>
</div>
</Teleport>
</template>
<!-- to="#notifications" → vers un div#notifications
to=".modals-container" → vers un élément CSS -->
<!-- KeepAlive — préserver l'état des composants inactifs -->
<KeepAlive :max="5">
<RouterView />
</KeepAlive>
<!-- L'état du composant est préservé entre les navigations -->
<!-- Suspense — afficher un fallback pendant
le chargement d'un composant async -->
<Suspense>
<template #default>
<DashboardAsync />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
// DashboardAsync.vue — composant async
const data = await fetch('/api/dashboard').then(r => r.json());
// Le await dans <script setup> est supporté !
// Vue attend que la promesse soit résolue
// avant d'afficher le composant
// defineAsyncComponent — lazy loading
import { defineAsyncComponent } from 'vue';
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: Spinner,
errorComponent: ErrorMsg,
delay: 200,
timeout: 5000,
});
Vue Router
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
// Paramètre
{ path: '/users/:id', component: UserDetail },
// Lazy loading — chunk séparé
{
path: '/admin',
component: () => import('./pages/Admin.vue'),
meta: { requiresAuth: true },
},
// Routes imbriquées
{
path: '/dashboard',
component: DashLayout,
children: [
{ path: '', component: DashHome },
{ path: 'stats', component: DashStats },
]
},
{ path: '/:pathMatch(.*)*', component: NotFound },
]
});
// Guard global
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
return { path: '/login' };
}
});
export default router;
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
// Lire les paramètres
console.log(route.params.id); // /users/42 → '42'
console.log(route.query.q); // ?q=vue → 'vue'
console.log(route.meta.requiresAuth);
// Navigation programmée
router.push('/home');
router.push({ name: 'user', params: { id: '42' } });
router.push({ path: '/search', query: { q: 'vue' } });
router.replace('/login'); // pas d'historique
router.back();
router.go(-2);
<!-- Template -->
<RouterLink to="/">Accueil</RouterLink>
<RouterLink :to="{ name: 'user', params: { id } }">
Profil
</RouterLink>
<!-- RouterLink active class -->
<RouterLink
to="/dashboard"
active-class="active"
exact-active-class="exact-active"
>
Dashboard
</RouterLink>
<!-- Afficher la route courante -->
<RouterView />
Pinia — état global
// stores/cart.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
// Style Composition (recommandé)
export const useCartStore = defineStore('cart', () => {
// État
const items = ref<CartItem[]>([]);
// Getters (computed)
const total = computed(() =>
items.value.reduce((s, i) => s + i.prix * i.qte, 0)
);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qte, 0)
);
// Actions
function addItem(item: CartItem) {
const existing = items.value.find(i => i.id === item.id);
if (existing) {
existing.qte++;
} else {
items.value.push({ ...item, qte: 1 });
}
}
function removeItem(id: number) {
items.value = items.value.filter(i => i.id !== id);
}
function clear() { items.value = []; }
return { items, total, count, addItem, removeItem, clear };
});
import { useCartStore } from '@/stores/cart';
import { storeToRefs } from 'pinia';
const cart = useCartStore();
// ✅ storeToRefs — destructurer en gardant la réactivité
const { items, total, count } = storeToRefs(cart);
// Les actions peuvent être destructurées directement
const { addItem, removeItem, clear } = cart;
// Template
// <p>{{ count }} articles — {{ total }} €</p>
// <button @click="addItem(product)">Ajouter</button>
// <button @click="clear">Vider</button>
// Store avec persist (persister dans localStorage)
// npm install pinia-plugin-persistedstate
export const useAuthStore = defineStore('auth', () => {
const token = ref('');
const user = ref<User | null>(null);
return { token, user };
}, {
persist: true // sauvegardé dans localStorage
});
HTTP & fetch
// npm install axios
// services/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
// Intercepteur — ajouter le token JWT
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Intercepteur — gérer les 401
api.interceptors.response.use(
res => res,
err => {
if (err.response?.status === 401) {
router.push('/login');
}
return Promise.reject(err);
}
);
// Service user
export const userService = {
getAll: () => api.get<User[]>('/users').then(r => r.data),
getById: (id: number) => api.get<User>(`/users/${id}`).then(r => r.data),
create: (u: Partial<User>) => api.post('/users', u).then(r => r.data),
update: (id: number, u: Partial<User>) => api.patch(`/users/${id}`, u).then(r => r.data),
delete: (id: number) => api.delete(`/users/${id}`),
};
// composables/useUsers.ts
import { ref, onMounted } from 'vue';
import { userService } from '@/services/api';
export function useUsers() {
const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
users.value = await userService.getAll();
} catch (e) {
error.value = (e as Error).message;
} finally {
loading.value = false;
}
}
async function deleteUser(id: number) {
await userService.delete(id);
users.value = users.value.filter(u => u.id !== id);
}
onMounted(fetchUsers);
return { users, loading, error, fetchUsers, deleteUser };
}
// Dans un composant :
const { users, loading, error, deleteUser } = useUsers();
Performance
// v-once — rendu statique une seule fois
<h1 v-once>{{ titreStatique }}</h1>
// v-memo — mémoïser une sous-arborescence
<div v-memo="[item.id, item.selected]">
<ExpensiveChild :item="item" />
</div>
// Ne re-render que si item.id ou item.selected change
// :key — forcer le re-montage
<UserProfile :key="userId" />
// Quand userId change → composant recréé from scratch
// shallowRef / shallowReactive — réactivité de surface
import { shallowRef } from 'vue';
// Pour les gros objets non mutés en profondeur
const bigData = shallowRef({ ... });
// Seul bigData.value (pas les propriétés) est suivi
// Virtualisation pour les longues listes
// npm install vue-virtual-scroller
<RecycleScroller
:items="largeListe"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<UserRow :user="item" />
</RecycleScroller>
// Routes lazy (déjà vu) — le plus impactant
{
path: '/admin',
component: () => import('./pages/Admin.vue')
}
// defineAsyncComponent — composant lazy
const HeavyChart = defineAsyncComponent(
() => import('./HeavyChart.vue')
);
// Nuxt — SSR / SSG avec Vue
// npm install nuxt
// Ajoute : pages/ auto-routées, SSR, SSG,
// useFetch() amélioré, modules Nuxt
// Vue DevTools — inspecter les composants
// Extension navigateur "Vue DevTools"
// Mesurer les performances
import { getCurrentInstance } from 'vue';
// Vue DevTools → Onglet Performance → Profiler
// Bonnes pratiques :
// ✅ :key stable et unique sur v-for
// ✅ computed plutôt que calculs dans le template
// ✅ defineAsyncComponent pour composants lourds
// ✅ shallowRef pour les grandes structures de données
// ✅ v-once pour le contenu vraiment statique
Cheat sheet Vue 3
Réactivité
| ref(val) | Valeur réactive → .value |
| reactive(obj) | Objet réactif profond |
| computed(fn) | Valeur dérivée mémoïsée |
| watch(src, fn) | Surveiller une source |
| watchEffect(fn) | Surveiller auto les deps |
| toRefs(obj) | Destructurer reactive |
Template
| {{ val }} | Interpolation |
| :prop="val" | Binding prop/attribut |
| @click="fn" | Event binding |
| v-model="ref" | Two-way binding |
| v-if / v-else | Conditionnel |
| v-for + :key | Itération (key obligatoire) |
Composants
| defineProps<T>() | Déclarer les props |
| defineEmits<E>() | Déclarer les événements |
| defineExpose() | Exposer au parent |
| <slot /> | Projection de contenu |
| provide/inject | Partage sans prop drilling |
Écosystème
| Vue Router | Navigation SPA |
| Pinia | État global (remplace Vuex) |
| VueUse | 100+ composables utiles |
| Nuxt | SSR / SSG avec Vue |
| Vite + vue-ts | Template officiel |