# Questions d'entretien Vue.js essentielles : 25 questions pour réussir > Préparez vos entretiens Vue.js avec ces 25 questions essentielles. De la réactivité aux composables, maîtrisez les concepts clés pour décrocher le poste. - Published: 2026-02-05 - Updated: 2026-03-31 - Author: SharpSkill - Tags: vue.js, entretien, frontend, javascript, questions techniques - Reading time: 15 min --- Les entretiens Vue.js évaluent bien plus que la syntaxe du framework. Les recruteurs cherchent à comprendre la maîtrise du système de réactivité, l'organisation du code avec la Composition API, et la capacité à résoudre des problèmes concrets de performance et d'architecture. > **Conseil de préparation** > > Chaque question est accompagnée d'une réponse détaillée et d'exemples de code. Pour les entretiens techniques, il est recommandé de pratiquer en expliquant les concepts à voix haute, comme lors d'un vrai entretien. ## Questions fondamentales sur Vue.js ### 1. Quelle est la différence entre ref et reactive dans Vue 3 ? Cette question teste la compréhension du système de réactivité, fondamental dans Vue 3. La différence principale réside dans les types de données gérés et la syntaxe d'accès. `ref` crée une référence réactive pour les valeurs primitives (string, number, boolean) et nécessite `.value` pour accéder à la valeur dans le script. `reactive` crée un proxy réactif pour les objets et permet un accès direct aux propriétés. ```javascript // Exemple comparatif ref vs reactive import { ref, reactive } from 'vue' // ref: pour les primitives // Nécessite .value dans le script const count = ref(0) count.value++ // Accès avec .value // reactive: pour les objets complexes // Accès direct aux propriétés const user = reactive({ name: 'Alice', age: 25 }) user.age++ // Pas de .value nécessaire // Attention: reactive perd la réactivité si réassigné // user = { name: 'Bob', age: 30 } // ❌ Casse la réactivité Object.assign(user, { name: 'Bob', age: 30 }) // ✅ Correct ``` La règle générale : utiliser `ref` pour les valeurs simples et `reactive` pour les objets structurés avec plusieurs propriétés liées. ### 2. Comment fonctionne le système de réactivité de Vue 3 ? Vue 3 utilise les Proxies JavaScript (ES6) pour intercepter les opérations sur les objets réactifs. Contrairement à Vue 2 qui utilisait `Object.defineProperty`, cette approche permet de détecter l'ajout et la suppression de propriétés dynamiquement. ```javascript // Démonstration simplifiée du principe de réactivité // Vue utilise les Proxies pour tracker les dépendances const handler = { // Interception de la lecture get(target, key, receiver) { track(target, key) // Enregistre la dépendance return Reflect.get(target, key, receiver) }, // Interception de l'écriture set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) trigger(target, key) // Déclenche les mises à jour return result } } // Création d'un proxy réactif const reactiveObject = new Proxy(originalObject, handler) ``` Les avantages des Proxies incluent : détection des nouvelles propriétés, support des Map/Set, et meilleure performance sur les objets volumineux. ### 3. Expliquez la différence entre computed et watch Les `computed` et `watch` répondent à des besoins différents dans la gestion de la réactivité. **Computed** : calcule une valeur dérivée à partir d'autres données réactives. Les valeurs sont mises en cache et ne se recalculent que lorsque les dépendances changent. Idéal pour les transformations de données. **Watch** : exécute des effets secondaires en réponse aux changements. Utile pour les appels API, les interactions avec le DOM, ou les opérations asynchrones. ```javascript // Comparaison computed vs watch import { ref, computed, watch } from 'vue' const firstName = ref('Jean') const lastName = ref('Dupont') // computed: valeur dérivée avec cache // Se recalcule uniquement si firstName ou lastName change const fullName = computed(() => { console.log('Calcul du nom complet') // Appelé une seule fois return `${firstName.value} ${lastName.value}` }) // Accès multiple = une seule exécution (cache) console.log(fullName.value) // "Jean Dupont" console.log(fullName.value) // Pas de recalcul // watch: effet secondaire sans cache // Exécuté à chaque changement watch(firstName, async (newName, oldName) => { // Effet secondaire: appel API await saveToServer({ firstName: newName }) console.log(`Nom changé de ${oldName} à ${newName}`) }) ``` ### 4. Qu'est-ce que le Virtual DOM et comment Vue l'utilise ? Le Virtual DOM est une représentation JavaScript légère du DOM réel. Vue maintient un arbre virtuel en mémoire et calcule les différences (diffing) entre l'état précédent et le nouvel état pour appliquer uniquement les changements nécessaires au DOM réel. ```javascript // Représentation conceptuelle du Virtual DOM // Vue crée cette structure en interne const vnode = { type: 'div', props: { class: 'container', id: 'app' }, children: [ { type: 'h1', props: {}, children: 'Titre' }, { type: 'p', props: {}, children: 'Contenu du paragraphe' } ] } // Lors d'un changement, Vue compare les vnodes // et met à jour uniquement les éléments modifiés ``` Les optimisations de Vue 3 incluent : hoisting des nœuds statiques, patch flags pour identifier les types de changements, et tree-shaking du compilateur. ### 5. Comment gérer la communication entre composants non liés ? Plusieurs patterns existent pour la communication entre composants sans relation parent-enfant directe. ```javascript // Solution 1: Event Bus (petites applications) // eventBus.js import { ref } from 'vue' const bus = ref(new Map()) export function useEventBus() { // Émettre un événement const emit = (event, payload) => { const callbacks = bus.value.get(event) || [] callbacks.forEach(cb => cb(payload)) } // Écouter un événement const on = (event, callback) => { if (!bus.value.has(event)) { bus.value.set(event, []) } bus.value.get(event).push(callback) } return { emit, on } } ``` ```javascript // Solution 2: Provide/Inject pour les composants imbriqués // Ancêtre import { provide, ref } from 'vue' const sharedState = ref('valeur partagée') provide('stateKey', sharedState) // Descendant (n'importe quel niveau) import { inject } from 'vue' const state = inject('stateKey') ``` Pour les applications plus complexes, Pinia reste la solution recommandée pour la gestion d'état globale. ## Questions sur la Composition API ### 6. Quels sont les avantages de la Composition API par rapport à l'Options API ? La Composition API offre plusieurs avantages structurels par rapport à l'Options API traditionnelle. **Organisation par fonctionnalité** : le code lié à une même fonctionnalité est regroupé, contrairement à l'Options API qui sépare par type (data, methods, computed). **Réutilisation via composables** : extraction facile de la logique dans des fonctions réutilisables. **Meilleur support TypeScript** : inférence de types naturelle sans décorateurs. ```javascript // Options API: code fragmenté par type export default { data() { return { searchQuery: '', results: [] } }, computed: { hasResults() { return this.results.length > 0 } }, methods: { async search() { this.results = await fetchResults(this.searchQuery) } }, watch: { searchQuery: 'search' } } // Composition API: code groupé par fonctionnalité import { ref, computed, watch } from 'vue' export function useSearch() { const searchQuery = ref('') const results = ref([]) const hasResults = computed(() => results.value.length > 0) const search = async () => { results.value = await fetchResults(searchQuery.value) } watch(searchQuery, search) return { searchQuery, results, hasResults, search } } ``` ### 7. Comment créer un composable réutilisable ? Les composables sont des fonctions qui encapsulent de la logique réactive. Les conventions incluent : préfixe `use`, retour d'un objet avec l'état et les méthodes, et gestion du nettoyage. ```javascript // composables/useLocalStorage.js import { ref, watch } from 'vue' // Composable pour synchroniser un état avec localStorage export function useLocalStorage(key, defaultValue) { // Récupérer la valeur initiale depuis localStorage const storedValue = localStorage.getItem(key) const data = ref( storedValue ? JSON.parse(storedValue) : defaultValue ) // Synchroniser les changements vers localStorage watch( data, (newValue) => { if (newValue === null) { localStorage.removeItem(key) } else { localStorage.setItem(key, JSON.stringify(newValue)) } }, { deep: true } // Observer les objets imbriqués ) return data } // Utilisation dans un composant const theme = useLocalStorage('theme', 'light') const userPrefs = useLocalStorage('prefs', { notifications: true }) ``` > **Convention de nommage** > > Les composables suivent la convention `useXxx` pour indiquer leur nature réutilisable. Cette convention améliore la lisibilité et facilite l'identification des dépendances réactives. ### 8. Expliquez watchEffect vs watch `watchEffect` et `watch` servent à réagir aux changements, mais avec des approches différentes. **watchEffect** : exécute immédiatement et re-exécute automatiquement quand ses dépendances réactives changent. Les dépendances sont trackées automatiquement. **watch** : observe des sources spécifiques et fournit les anciennes et nouvelles valeurs. Plus de contrôle sur le déclenchement. ```javascript // Comparaison watchEffect vs watch import { ref, watch, watchEffect } from 'vue' const userId = ref(1) const userData = ref(null) // watchEffect: tracking automatique // S'exécute immédiatement watchEffect(async () => { // userId est automatiquement tracké const response = await fetch(`/api/users/${userId.value}`) userData.value = await response.json() }) // watch: sources explicites avec anciennes valeurs watch(userId, async (newId, oldId) => { console.log(`User changé de ${oldId} à ${newId}`) const response = await fetch(`/api/users/${newId}`) userData.value = await response.json() }, { immediate: true // Exécuter immédiatement comme watchEffect }) // watchEffect avec cleanup watchEffect((onCleanup) => { const controller = new AbortController() fetch(`/api/users/${userId.value}`, { signal: controller.signal }).then(/* ... */) // Cleanup: annuler la requête précédente onCleanup(() => controller.abort()) }) ``` ### 9. Comment gérer les props avec TypeScript dans script setup ? La syntaxe ` ``` ## Questions sur les performances ### 10. Quelles techniques d'optimisation des performances connaissez-vous ? Vue 3 offre plusieurs mécanismes d'optimisation des performances. ```javascript // 1. v-once: rendu unique pour le contenu statique // 2. v-memo: mémorisation conditionnelle // 3. shallowRef/shallowReactive: réactivité superficielle import { shallowRef, triggerRef } from 'vue' // Ne track que le remplacement de la ref, pas les mutations internes const largeList = shallowRef([/* milliers d'éléments */]) // Forcer la mise à jour après mutation largeList.value.push(newItem) triggerRef(largeList) // Déclencher manuellement le re-rendu ``` ```javascript // 4. Composants asynchrones pour le code-splitting import { defineAsyncComponent } from 'vue' const HeavyComponent = defineAsyncComponent({ loader: () => import('./HeavyComponent.vue'), loadingComponent: LoadingSpinner, delay: 200, // Délai avant d'afficher le loading errorComponent: ErrorDisplay, timeout: 3000 }) // 5. KeepAlive pour le cache des composants ``` ### 11. Comment éviter les re-rendus inutiles ? Les re-rendus inutiles peuvent impacter les performances. Plusieurs stratégies permettent de les minimiser. ```javascript // Problème: fonction créée à chaque rendu // Solution: utiliser une méthode ou une ref ``` ```javascript // Utilisation de computed pour les calculs coûteux import { computed } from 'vue' // ❌ Recalculé à chaque rendu const getFilteredItems = () => { return items.value.filter(/* logique complexe */) } // ✅ Mis en cache, recalculé uniquement si items change const filteredItems = computed(() => { return items.value.filter(/* logique complexe */) }) ``` ### 12. Expliquez le lazy loading des composants et routes Le lazy loading permet de charger le code à la demande, réduisant la taille du bundle initial. ```javascript // router/index.js import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', // Chargement immédiat (bundle principal) component: () => import('@/views/Home.vue') }, { path: '/dashboard', // Chunk séparé avec nom personnalisé component: () => import( /* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue' ), // Lazy loading des routes enfants children: [ { path: 'analytics', component: () => import('@/views/Analytics.vue') } ] }, { path: '/admin', // Préchargement sur hover du lien component: () => import('@/views/Admin.vue'), meta: { prefetch: true } } ] }) export default router ``` ## Questions sur le routage avec Vue Router ### 13. Comment protéger les routes avec des guards ? Les navigation guards permettent de contrôler l'accès aux routes. ```javascript // router/index.js import { createRouter } from 'vue-router' import { useAuthStore } from '@/stores/auth' const router = createRouter({ routes: [ { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true, roles: ['admin', 'user'] } } ] }) // Guard global: vérifie l'authentification router.beforeEach(async (to, from, next) => { const auth = useAuthStore() // Route publique if (!to.meta.requiresAuth) { return next() } // Vérifier l'authentification if (!auth.isAuthenticated) { return next({ path: '/login', query: { redirect: to.fullPath } }) } // Vérifier les rôles si spécifiés if (to.meta.roles && !to.meta.roles.includes(auth.user.role)) { return next('/unauthorized') } next() }) // Guard au niveau du composant export default { beforeRouteEnter(to, from, next) { // Pas accès à this ici next(vm => { // Accès à l'instance du composant via vm vm.loadData() }) }, beforeRouteLeave(to, from, next) { // Confirmer avant de quitter si formulaire modifié if (this.hasUnsavedChanges) { const answer = confirm('Quitter sans sauvegarder ?') next(answer) } else { next() } } } ``` ### 14. Comment passer des props aux routes ? Vue Router permet de découpler les composants des paramètres de route. ```javascript // Configuration des routes avec props const routes = [ { path: '/user/:id', component: UserProfile, // Mode boolean: passe les params comme props props: true }, { path: '/search', component: SearchResults, // Mode fonction: transformation personnalisée props: (route) => ({ query: route.query.q, page: parseInt(route.query.page) || 1, filters: route.query.filters?.split(',') || [] }) }, { path: '/static', component: StaticPage, // Mode objet: props statiques props: { sidebar: true, theme: 'dark' } } ] ``` ```javascript // UserProfile.vue // SearchResults.vue ``` ## Questions sur Pinia et la gestion d'état ### 15. Quelles sont les différences entre Pinia et Vuex ? Pinia est le gestionnaire d'état officiel pour Vue 3, remplaçant Vuex avec une API simplifiée. | Caractéristique | Vuex | Pinia | |----------------|------|-------| | Mutations | Requises | Non nécessaires | | Modules | Configuration complexe | Stores indépendants | | TypeScript | Support limité | Natif et complet | | API | Options | Composition + Options | | DevTools | Support | Support complet | ```javascript // Store Pinia avec Composition API import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCartStore = defineStore('cart', () => { // State const items = ref([]) const discountCode = ref(null) // Getters (computed) const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) ) const totalPrice = computed(() => { const subtotal = items.value.reduce( (sum, item) => sum + item.price * item.quantity, 0 ) return discountCode.value ? subtotal * 0.9 : subtotal }) // Actions (fonctions directes) function addItem(product) { const existing = items.value.find(i => i.id === product.id) if (existing) { existing.quantity++ } else { items.value.push({ ...product, quantity: 1 }) } } function removeItem(productId) { const index = items.value.findIndex(i => i.id === productId) if (index > -1) { items.value.splice(index, 1) } } async function checkout() { const response = await api.createOrder(items.value) items.value = [] return response } return { items, discountCode, totalItems, totalPrice, addItem, removeItem, checkout } }) ``` ### 16. Comment persister l'état d'un store Pinia ? La persistance permet de conserver l'état entre les sessions utilisateur. ```javascript // plugins/piniaPersistedState.js import { watch } from 'vue' export function createPersistedState(options = {}) { const { key = 'pinia', storage = localStorage, paths = null } = options return ({ store }) => { // Restaurer l'état au démarrage const savedState = storage.getItem(`${key}-${store.$id}`) if (savedState) { store.$patch(JSON.parse(savedState)) } // Persister les changements watch( () => store.$state, (state) => { const toSave = paths ? paths.reduce((acc, path) => { acc[path] = state[path] return acc }, {}) : state storage.setItem( `${key}-${store.$id}`, JSON.stringify(toSave) ) }, { deep: true } ) } } // main.js import { createPinia } from 'pinia' import { createPersistedState } from './plugins/piniaPersistedState' const pinia = createPinia() pinia.use(createPersistedState({ key: 'app-state', paths: ['user', 'preferences'] // Persister seulement ces clés })) ``` > **Données sensibles** > > Éviter de persister des données sensibles (tokens, mots de passe) dans localStorage. Utiliser des solutions sécurisées comme les cookies httpOnly pour les tokens d'authentification. ## Questions avancées ### 17. Comment implémenter un système de plugins Vue ? Les plugins permettent d'étendre Vue avec des fonctionnalités globales. ```javascript // plugins/analyticsPlugin.js export const AnalyticsPlugin = { install(app, options = {}) { const { trackingId, debug = false } = options // Injection globale disponible dans tous les composants const analytics = { trackEvent(category, action, label) { if (debug) { console.log('Analytics:', { category, action, label }) } // Logique d'envoi vers le service analytics window.gtag?.('event', action, { event_category: category, event_label: label }) }, trackPage(path) { window.gtag?.('config', trackingId, { page_path: path }) } } // Rendre disponible via inject app.provide('analytics', analytics) // Ajouter une propriété globale (déconseillé en Composition API) app.config.globalProperties.$analytics = analytics // Directive personnalisée pour le tracking au clic app.directive('track', { mounted(el, binding) { el.addEventListener('click', () => { analytics.trackEvent('click', binding.value, el.textContent) }) } }) // Tracking automatique des changements de route app.mixin({ mounted() { if (this.$route) { analytics.trackPage(this.$route.path) } } }) } } // main.js import { AnalyticsPlugin } from './plugins/analyticsPlugin' app.use(AnalyticsPlugin, { trackingId: 'UA-XXXXX-X', debug: import.meta.env.DEV }) ``` ### 18. Expliquez les Render Functions et leur utilité Les render functions offrent un contrôle total sur le rendu, utile pour les composants hautement dynamiques. ```javascript // components/DynamicHeading.js import { h } from 'vue' // Composant fonctionnel avec render function export const DynamicHeading = { props: { level: { type: Number, default: 1, validator: (v) => v >= 1 && v <= 6 } }, setup(props, { slots }) { // h() crée un vnode // Arguments: type, props, children return () => h( `h${props.level}`, { class: 'dynamic-heading' }, slots.default?.() ) } } // Composant avec logique conditionnelle complexe export const ConditionalWrapper = { props: ['condition', 'wrapper'], setup(props, { slots }) { return () => { if (props.condition) { return h(props.wrapper, null, slots.default?.()) } return slots.default?.() } } } // Utilisation Titre niveau 2 Contenu conditionnel ``` ### 19. Comment tester les composants Vue avec Vitest ? Les tests unitaires valident le comportement isolé des composants. ```javascript // components/__tests__/Counter.spec.js import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import Counter from '../Counter.vue' describe('Counter', () => { it('affiche la valeur initiale', () => { const wrapper = mount(Counter, { props: { initialValue: 5 } }) expect(wrapper.text()).toContain('5') }) it('incrémente la valeur au clic', async () => { const wrapper = mount(Counter) await wrapper.find('button.increment').trigger('click') expect(wrapper.text()).toContain('1') }) it('émet un événement lors du changement', async () => { const wrapper = mount(Counter) await wrapper.find('button.increment').trigger('click') expect(wrapper.emitted('change')).toBeTruthy() expect(wrapper.emitted('change')[0]).toEqual([1]) }) it('appelle le service au submit', async () => { const mockSubmit = vi.fn() const wrapper = mount(Counter, { global: { provide: { submitService: mockSubmit } } }) await wrapper.find('form').trigger('submit') expect(mockSubmit).toHaveBeenCalled() }) }) ``` ### 20. Comment gérer les erreurs globalement dans Vue ? Vue 3 offre plusieurs mécanismes pour capturer et gérer les erreurs. ```javascript // main.js import { createApp } from 'vue' const app = createApp(App) // Handler global pour les erreurs de composants app.config.errorHandler = (err, instance, info) => { // err: l'erreur // instance: l'instance du composant // info: chaîne décrivant où l'erreur s'est produite console.error('Erreur Vue:', err) console.error('Composant:', instance?.$options?.name) console.error('Info:', info) // Envoyer à un service de monitoring errorTracker.captureException(err, { component: instance?.$options?.name, info }) } // Handler pour les avertissements (dev uniquement) app.config.warnHandler = (msg, instance, trace) => { console.warn('Avertissement Vue:', msg) } ``` ```javascript // Composant ErrorBoundary ``` ## Questions sur les bonnes pratiques ### 21. Quelles conventions de nommage suivre dans Vue ? Les conventions de nommage améliorent la lisibilité et la maintenabilité du code. ```javascript // Nommage des composants // PascalCase pour les fichiers et le nom // BaseButton.vue, AppHeader.vue, TheNavbar.vue // Props: camelCase en JS, kebab-case en template defineProps<{ userName: string // JS isActive: boolean // JS }>() // // Events: camelCase avec préfixe action const emit = defineEmits<{ (e: 'updateValue', value: string): void // ✅ (e: 'submit'): void // ✅ (e: 'value-updated'): void // ❌ Éviter }>() // Composables: préfixe use // useAuth.js, useFetch.js, useLocalStorage.js // Stores Pinia: préfixe use + Store suffix // useUserStore, useCartStore, useSettingsStore // Constantes: SCREAMING_SNAKE_CASE const MAX_RETRY_COUNT = 3 const API_BASE_URL = '/api/v1' ``` ### 22. Comment structurer un projet Vue à grande échelle ? Une structure modulaire facilite la navigation et la maintenance. ``` src/ ├── assets/ # Fichiers statiques ├── components/ │ ├── ui/ # Composants génériques (Button, Modal) │ └── common/ # Composants métier réutilisables ├── composables/ # Logique réutilisable │ ├── useAuth.js │ └── useFetch.js ├── layouts/ # Layouts de page │ ├── DefaultLayout.vue │ └── AuthLayout.vue ├── modules/ # Modules fonctionnels │ ├── auth/ │ │ ├── components/ │ │ ├── composables/ │ │ ├── stores/ │ │ └── views/ │ └── dashboard/ ├── plugins/ # Plugins Vue ├── router/ │ ├── index.js │ └── guards.js ├── stores/ # Stores Pinia globaux ├── types/ # Types TypeScript ├── utils/ # Utilitaires purs └── views/ # Pages/Routes ``` ### 23. Quand utiliser v-if vs v-show ? Le choix entre `v-if` et `v-show` dépend de la fréquence de basculement. ```javascript // v-if: coût initial faible, basculement coûteux // Retire/ajoute l'élément du DOM // Idéal pour: conditions rarement modifiées // v-show: coût initial plus élevé, basculement rapide // Utilise display: none // Idéal pour: basculements fréquents ``` ### 24. Comment optimiser les listes avec v-for ? L'optimisation des listes est cruciale pour les performances avec de nombreux éléments. ```javascript // Toujours utiliser :key avec un identifiant unique stable // Filtrage et tri: utiliser computed // Virtualisation pour les très longues listes ``` ### 25. Expliquez le pattern de composants sans rendu (Renderless) Les composants renderless encapsulent la logique sans imposer de structure HTML. ```javascript // components/MouseTracker.vue ``` ```javascript // Utilisation: contrôle total sur le rendu ``` Ce pattern sépare complètement la logique de la présentation, maximisant la réutilisabilité. ## Conclusion Ces 25 questions couvrent les concepts essentiels évalués lors des entretiens Vue.js : - ✅ **Réactivité** : `ref`, `reactive`, `computed`, `watch` - ✅ **Composition API** : composables, `script setup`, TypeScript - ✅ **Performances** : lazy loading, virtualisation, optimisations - ✅ **Vue Router** : guards, props, navigation - ✅ **Pinia** : stores, persistance, actions asynchrones - ✅ **Bonnes pratiques** : structure, conventions, patterns avancés La préparation efficace combine la compréhension théorique avec la pratique du code. Chaque concept abordé ici peut faire l'objet de questions de suivi approfondies lors de l'entretien. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/vue-nuxt/questions-entretien-vuejs-essentielles