Développement Web

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 ?

V

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.

Philosophie Vue
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
Vue sans bundler (usage simple)
<!-- 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

Créer un projet Vue 3
# 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
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> — HTML du composant
<template>
  <div class="card">
    <h2>{{ titre }}</h2>
    <p>Prix : {{ prix }} €</p>
    <button @click="acheter">
      Acheter
    </button>
  </div>
</template>
<script setup> — logique TypeScript
<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> — CSS local
<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 1rem;
}
/* scoped = appliqué à CE composant seulement
   → .card[data-v-xxxxxxxx] en prod */
</style>
Utiliser le composant
<!-- 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

Options API (Vue 2 + Vue 3)
// Organisation par type d'option
export default {
  name: 'Compteur',
  // État
  data() {
    return { count: 0, nom: '' };
  },
  // Propriétés calculées
  computed: {
    doubled() { return this.count * 2; }
  },
  // Surveillance
  watch: {
    count(val) { console.log(val); }
  },
  // Méthodes
  methods: {
    increment() { this.count++; }
  },
  // Cycle de vie
  mounted() {
    console.log('Monté');
  }
}

// ⚠ this partout — moins TypeScript-friendly
// ✅ Familier pour les devs Vue 2
// ✅ Lisible pour les petits composants
Composition API + <script setup> ✅
// Organisation par fonctionnalité
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';

// État
const count = ref(0);
const nom   = ref('');

// Propriété calculée
const doubled = computed(() => count.value * 2);

// Surveillance
watch(count, (val) => console.log(val));

// Méthode
function increment() { count.value++; }

// Cycle de vie
onMounted(() => console.log('Monté'));
</script>

// ✅ TypeScript natif — pas de this
// ✅ Réutilisable via composables
// ✅ Recommandé pour Vue 3

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

ref — valeur primitive réactive
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
reactive — objet réactif profond
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

computed — valeur dérivée
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(' ');
  }
});
watch & watchEffect
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

HookQuandUsage typique
onBeforeMountAvant le premier rendu DOMRarement utile
onMountedAprès insertion dans le DOMFetch initial, bibliothèques DOM, focus
onBeforeUpdateAvant re-renduLire le DOM avant update
onUpdatedAprès re-renduOpérations DOM post-update
onBeforeUnmountAvant destructionCleanup (timers, listeners)
onUnmountedAprès destructionCleanup final
onErrorCapturedErreur dans un enfantError boundary
Utilisation des hooks de cycle de vie
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

useFetch — composable générique
// 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');
useLocalStorage & useMouse
// 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

Directives intégrées
<!-- 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>
Autres directives & directives custom
<!-- 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

Les types de 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 -->
v-model sur composants personnalisés
// 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énements
<!-- 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">
defineEmits — événements typés
// 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

Slot par défaut & nommés
<!-- 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>
Scoped slots — passer des données au parent
<!-- 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

defineProps avec TypeScript
// 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
defineExpose — API publique du composant
// 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

Partager des données sans prop drilling
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>
Pattern — composable avec provide/inject
// 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

Teleport — déplacer dans le DOM
<!-- 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 — async components
<!-- 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

Configuration des routes
// 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;
Navigation & composables 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

Définir un store Pinia
// 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 };
});
Utiliser le store dans les composants
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

Service HTTP avec Axios
// 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}`),
};
useUsers composable complet
// 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

Optimisations Vue
// 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>
Lazy loading & code splitting
// 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-elseConditionnel
v-for + :keyIté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/injectPartage sans prop drilling

Écosystème

Vue RouterNavigation SPA
PiniaÉtat global (remplace Vuex)
VueUse100+ composables utiles
NuxtSSR / SSG avec Vue
Vite + vue-tsTemplate officiel