Composables Vue 3 Avances : Patterns Reutilisables et Questions d'Entretien 2026

Guide complet des composables Vue 3 avances : patterns reutilisables, gestion asynchrone, injection de dependances, validation de formulaires et questions d'entretien technique 2026.

Illustration des patterns avances de composables Vue 3 avec diagramme de composition et injection de dependances

Les composables constituent le mecanisme fondamental de reutilisation de logique dans Vue 3. Depuis l'adoption massive de la Composition API, la capacite a concevoir des composables robustes, typesafe et testables distingue les developpeurs seniors des profils intermediaires lors des entretiens techniques. En 2026, la maitrise de ces patterns depasse le simple usage de ref et computed : elle implique la gestion fine du cycle de vie, la composition de fonctions reactives et l'architecture modulaire d'applications complexes.

Cet article analyse en profondeur les patterns avances de composables Vue 3, depuis l'anatomie d'un composable bien structure jusqu'aux strategies de test, en passant par la gestion asynchrone et l'injection de dependances. Chaque pattern est accompagne d'exemples concrets directement applicables en production et en entretien technique.

Qu'est-ce qu'un composable ?

Un composable est une fonction qui exploite la Composition API de Vue pour encapsuler et reutiliser de la logique stateful. Par convention, les composables commencent par le prefixe use (ex. useCounter, useFetchData). Contrairement aux mixins de Vue 2, les composables offrent un typage explicite, evitent les collisions de noms et rendent les dependances transparentes.

Anatomie d'un Composable Bien Structure

Un composable de qualite production respecte plusieurs principes architecturaux : typage strict des entrees et sorties, separation des responsabilites, et interface predictible. Le composable useCounter suivant illustre ces conventions fondamentales.

useCounter.tstypescript
import { ref, computed, type Ref } from 'vue'

interface UseCounterOptions {
  min?: number
  max?: number
  initialValue?: number
}

interface UseCounterReturn {
  count: Ref<number>
  doubled: Ref<number>
  increment: () => void
  decrement: () => void
  reset: () => void
}

export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
  const { min = 0, max = Infinity, initialValue = 0 } = options

  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment() {
    if (count.value < max) count.value++
  }

  function decrement() {
    if (count.value > min) count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return { count, doubled, increment, decrement, reset }
}

Plusieurs elements meritent attention dans cette implementation. L'interface UseCounterOptions definit un contrat clair pour la configuration, tandis que UseCounterReturn documente precisement ce que le composable expose. Le destructuring avec valeurs par defaut dans le corps de la fonction garantit un comportement previsible meme sans configuration explicite.

Le retour d'un objet nomme (plutot qu'un tableau) permet au consommateur de selectionner uniquement les proprietes necessaires via le destructuring. Cette convention, adoptee par l'ensemble de l'ecosysteme Vue, facilite la lisibilite et la maintenance du code appelant.

Les bornes min et max illustrent un pattern recurrent dans les composables de production : la validation des contraintes metier directement dans la logique reactive, evitant ainsi de disperser ces regles dans les composants consommateurs.

Composables Asynchrones avec Gestion d'Erreurs

La gestion de requetes HTTP constitue l'un des cas d'usage les plus frequents des composables. Un composable asynchrone bien concu doit gerer le cycle de vie complet d'une requete : chargement, succes, erreur et annulation. Le pattern suivant integre watchEffect pour la reactivite automatique et AbortController pour l'annulation propre des requetes.

useFetchData.tstypescript
import { ref, watchEffect, onUnmounted, toValue, type Ref, type MaybeRefOrGetter } from 'vue'

interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<string | null>
  isLoading: Ref<boolean>
  refresh: () => Promise<void>
}

export function useFetchData<T>(
  url: MaybeRefOrGetter<string>
): UseFetchReturn<T> {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<string | null>(null)
  const isLoading = ref(false)
  let abortController: AbortController | null = null

  async function fetchData() {
    // Cancel any in-flight request
    abortController?.abort()
    abortController = new AbortController()

    isLoading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url), {
        signal: abortController.signal
      })
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      data.value = await response.json()
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return
      error.value = err instanceof Error ? err.message : 'Unknown error'
    } finally {
      isLoading.value = false
    }
  }

  // Re-fetch when URL changes reactively
  watchEffect(() => {
    fetchData()
  })

  onUnmounted(() => abortController?.abort())

  return { data, error, isLoading, refresh: fetchData }
}

L'utilisation de MaybeRefOrGetter<string> pour le parametre url offre une flexibilite maximale : le consommateur peut passer une chaine statique, une ref, ou un getter. La fonction toValue() resout automatiquement le type sous-jacent, qu'il s'agisse d'une valeur brute ou reactive.

Le mecanisme d'annulation via AbortController previent les conditions de course (race conditions), probleme classique lorsqu'une nouvelle requete est declenchee avant la fin de la precedente. Le hook onUnmounted garantit le nettoyage des requetes en cours lors de la destruction du composant, evitant les fuites memoire et les mises a jour d'etat sur des composants demontes.

La methode refresh exposee dans le retour permet au composant consommateur de declencher manuellement un rechargement des donnees, pattern indispensable pour les actions utilisateur comme un bouton "Actualiser".

Hooks de cycle de vie dans les composables

Les hooks de cycle de vie (onMounted, onUnmounted, etc.) doivent etre appeles de maniere synchrone dans le corps du composable, jamais dans un callback asynchrone ou un setTimeout. Vue associe ces hooks a l'instance du composant active au moment de l'appel. Un appel differe peut provoquer des erreurs silencieuses ou rattacher le hook au mauvais composant.

Composition de Composables

La veritable puissance des composables reside dans leur capacite a se composer entre eux. Un composable de haut niveau peut orchestrer plusieurs composables specialises pour creer des fonctionnalites complexes tout en conservant une separation claire des responsabilites. Le pattern suivant combine recherche debouncee, pagination et chargement de donnees.

usePaginatedSearch.tstypescript
import { ref, computed, watch, type Ref } from 'vue'
import { useFetchData } from './useFetchData'
import { useDebouncedRef } from './useDebouncedRef'

interface UsePaginatedSearchReturn<T> {
  query: Ref<string>
  page: Ref<number>
  results: Ref<T[] | null>
  totalPages: Ref<number>
  isLoading: Ref<boolean>
  error: Ref<string | null>
  nextPage: () => void
  prevPage: () => void
}

export function usePaginatedSearch<T>(
  baseUrl: string,
  perPage = 20
): UsePaginatedSearchReturn<T> {
  const query = useDebouncedRef('', 300)
  const page = ref(1)
  const totalPages = ref(1)

  const apiUrl = computed(
    () => `${baseUrl}?q=${encodeURIComponent(query.value)}&page=${page.value}&limit=${perPage}`
  )

  const { data, error, isLoading } = useFetchData<{ items: T[]; total: number }>(apiUrl)

  const results = computed(() => data.value?.items ?? null)

  watch(data, (response) => {
    if (response) {
      totalPages.value = Math.ceil(response.total / perPage)
    }
  })

  // Reset to page 1 when query changes
  watch(query, () => { page.value = 1 })

  function nextPage() {
    if (page.value < totalPages.value) page.value++
  }

  function prevPage() {
    if (page.value > 1) page.value--
  }

  return { query, page, results, totalPages, isLoading, error, nextPage, prevPage }
}

Ce composable demontre plusieurs techniques avancees de composition. Le useDebouncedRef encapsule la logique de temporisation, evitant de surcharger l'API avec des requetes a chaque frappe. Le computed apiUrl reconstruit automatiquement l'URL lorsque la requete ou la page change, declenchant une nouvelle recuperation via useFetchData.

Le watch sur query reinitialise la page a 1 lors de chaque nouvelle recherche, comportement attendu par les utilisateurs mais souvent oublie dans les implementations naives. Ce detail illustre l'interet d'encapsuler la logique metier dans des composables plutot que de la disperser dans les composants.

La composition de composables suit le principe de responsabilite unique : useDebouncedRef gere le debouncing, useFetchData gere le cycle requete/reponse, et usePaginatedSearch orchestre le tout. Cette architecture facilite le test unitaire de chaque couche independamment.

Prêt à réussir tes entretiens Vue.js / Nuxt.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Injection de Dependances avec provide/inject

Pour les etats partages entre composants eloignes dans l'arbre, le systeme provide/inject de Vue offre une alternative elegante au prop drilling. Les composables peuvent encapsuler cette mecanique pour fournir une API propre et typesafe.

useTheme.tstypescript
import { provide, inject, ref, readonly, type InjectionKey, type Ref } from 'vue'

type Theme = 'light' | 'dark' | 'system'

interface ThemeContext {
  theme: Readonly<Ref<Theme>>
  setTheme: (t: Theme) => void
  resolvedTheme: Readonly<Ref<'light' | 'dark'>>
}

const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')

export function provideTheme(initial: Theme = 'system') {
  const theme = ref<Theme>(initial)

  const resolvedTheme = computed<'light' | 'dark'>(() => {
    if (theme.value !== 'system') return theme.value
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  })

  function setTheme(t: Theme) {
    theme.value = t
  }

  const context: ThemeContext = {
    theme: readonly(theme),
    setTheme,
    resolvedTheme: readonly(resolvedTheme)
  }

  provide(ThemeKey, context)
  return context
}

export function useTheme(): ThemeContext {
  const context = inject(ThemeKey)
  if (!context) {
    throw new Error('useTheme() requires a parent component to call provideTheme()')
  }
  return context
}

L'utilisation d'un InjectionKey type avec Symbol garantit l'unicite de la cle d'injection et fournit l'inference de type automatique lors de l'appel a inject. Ce pattern elimine les risques de collision de cles qui existaient avec les cles basees sur des chaines de caracteres.

La separation entre provideTheme et useTheme etablit une frontiere architecturale claire : le composant racine (ou layout) appelle provideTheme pour initialiser le contexte, tandis que les composants descendants appellent useTheme pour le consommer. L'utilisation de readonly sur les refs exposees protege contre les mutations accidentelles depuis les consommateurs.

Le resolvedTheme computed illustre un pattern frequent : transformer une valeur de configuration abstraite ('system') en valeur concrete ('light' ou 'dark') basee sur les preferences systeme. Ce niveau d'abstraction simplifie considerablement la logique des composants consommateurs.

Composable de Validation de Formulaires

La validation de formulaires represente un cas d'usage complexe qui beneficie enormement de l'encapsulation dans un composable. Le pattern suivant fournit une validation declarative basee sur des regles, avec gestion reactive des erreurs et etat de validite global.

useFormValidation.tstypescript
import { reactive, computed, type UnwrapNestedRefs } from 'vue'

type ValidationRule<T> = (value: T) => string | true
type FieldRules<T> = { [K in keyof T]?: ValidationRule<T[K]>[] }

interface UseFormReturn<T extends Record<string, any>> {
  fields: UnwrapNestedRefs<T>
  errors: Record<keyof T, string>
  isValid: Ref<boolean>
  validate: () => boolean
  resetErrors: () => void
}

export function useFormValidation<T extends Record<string, any>>(
  initialValues: T,
  rules: FieldRules<T>
): UseFormReturn<T> {
  const fields = reactive({ ...initialValues }) as UnwrapNestedRefs<T>

  const errors = reactive(
    Object.keys(initialValues).reduce(
      (acc, key) => ({ ...acc, [key]: '' }),
      {} as Record<keyof T, string>
    )
  )

  function validate(): boolean {
    let valid = true
    for (const key of Object.keys(rules) as (keyof T)[]) {
      const fieldRules = rules[key] || []
      errors[key] = '' as any
      for (const rule of fieldRules) {
        const result = rule(fields[key])
        if (result !== true) {
          errors[key] = result as any
          valid = false
          break // Stop at first error per field
        }
      }
    }
    return valid
  }

  function resetErrors() {
    for (const key of Object.keys(errors)) {
      (errors as any)[key] = ''
    }
  }

  const isValid = computed(() =>
    Object.values(errors).every((e) => e === '')
  )

  return { fields, errors, isValid, validate, resetErrors }
}

Ce composable exploite reactive plutot que ref pour l'objet fields, ce qui permet un acces direct aux proprietes sans .value. Le systeme de regles adopte une convention simple : chaque regle retourne true en cas de succes ou un message d'erreur sous forme de chaine. L'instruction break arrete la validation au premier echec par champ, evitant l'accumulation de messages d'erreur.

Le type FieldRules<T> exploite les mapped types de TypeScript pour garantir que les regles correspondent aux champs du formulaire. Cette approche detecte les erreurs de typage a la compilation, reduisant les bugs en production.

Le computed isValid offre un indicateur reactif de l'etat global du formulaire, directement utilisable pour activer ou desactiver un bouton de soumission sans logique supplementaire dans le composant.

Tester les Composables

Les composables necessitent un contexte Vue actif pour fonctionner correctement, car ils dependent du systeme de reactivite et des hooks de cycle de vie. La technique standard consiste a creer un composant wrapper minimal qui instancie le composable dans son setup. Vitest et Vue Test Utils fournissent les outils necessaires.

useCounter.spec.tstypescript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import { useCounter } from './useCounter'

function withSetup<T>(composable: () => T): { result: T; unmount: () => void } {
  let result!: T
  const wrapper = mount(
    defineComponent({
      setup() {
        result = composable()
        return () => h('div')
      }
    })
  )
  return { result, unmount: () => wrapper.unmount() }
}

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { result } = withSetup(() => useCounter())
    expect(result.count.value).toBe(0)
  })

  it('respects min and max boundaries', () => {
    const { result } = withSetup(() =>
      useCounter({ min: 0, max: 3, initialValue: 3 })
    )
    result.increment()
    expect(result.count.value).toBe(3) // Capped at max

    result.count.value = 0
    result.decrement()
    expect(result.count.value).toBe(0) // Capped at min
  })

  it('computes doubled value reactively', () => {
    const { result } = withSetup(() => useCounter({ initialValue: 5 }))
    expect(result.doubled.value).toBe(10)
    result.increment()
    expect(result.doubled.value).toBe(12)
  })
})

La fonction utilitaire withSetup constitue un pattern reutilisable pour tester n'importe quel composable. Elle monte un composant minimal dont le seul role est d'executer le composable dans un contexte reactif valide. Le retour de la methode unmount permet de tester le comportement de nettoyage des hooks onUnmounted.

Les tests couvrent trois aspects essentiels : l'initialisation par defaut, le respect des contraintes metier (bornes min/max), et la reactivite des valeurs calculees. Cette couverture represente le minimum attendu pour un composable de production.

Pour les composables asynchrones comme useFetchData, les tests necessitent des mocks de fetch et l'utilisation de flushPromises() pour attendre la resolution des promesses avant les assertions. Ce sujet constitue une question d'entretien classique sur la maitrise des tests asynchrones avec Vue.

VueUse : la bibliotheque de reference

VueUse propose plus de 200 composables couvrant les interactions navigateur, les capteurs, les animations et les utilitaires reactifs. Avant de developper un composable personnalise, il est recommande de verifier si VueUse propose deja une solution. Cependant, comprendre les patterns sous-jacents reste indispensable pour adapter, etendre ou debugger ces composables en production.

Questions d'Entretien Frequentes

Les composables Vue 3 constituent un sujet incontournable des entretiens techniques en 2026. Les questions suivantes couvrent les points evalues par les recruteurs, du niveau intermediaire au niveau senior.

Quelle est la difference entre un composable et un mixin ? Les mixins Vue 2 fusionnent les options dans le composant, provoquant des collisions de noms, des sources de donnees implicites et une absence de typage. Les composables exposent des interfaces explicites via des valeurs de retour typees, permettent le renommage par destructuring et rendent les dependances transparentes. Les composables sont superieurs en termes de maintenabilite, testabilite et compatibilite TypeScript.

Comment gerer les effets de bord dans un composable ? Tout effet de bord (ecouteur d'evenement, timer, abonnement WebSocket) doit etre nettoye dans un hook onUnmounted ou via le mecanisme de cleanup de watchEffect. L'omission de ce nettoyage provoque des fuites memoire et des comportements imprevisibles lors du remontage de composants.

Peut-on appeler un composable en dehors de setup() ? Non. Les composables doivent etre appeles de maniere synchrone dans la fonction setup() d'un composant ou dans un autre composable. Un appel en dehors de ce contexte empeche Vue d'associer les hooks de cycle de vie et la reactivite a l'instance du composant.

Comment partager un etat global avec les composables ? Deux approches principales existent. Le pattern singleton cree une instance reactive au niveau du module (en dehors de la fonction composable) partagee entre tous les consommateurs. Le pattern provide/inject utilise l'arbre de composants pour propager un etat contextuel. Le choix depend du cas d'usage : singleton pour un etat veritablement global, provide/inject pour un etat scope a un sous-arbre.

Comment tester un composable qui utilise des hooks de cycle de vie ? La technique withSetup (presentee ci-dessus) cree un composant wrapper minimal qui fournit le contexte Vue necessaire. Pour les hooks comme onMounted, le composant doit etre monte dans un DOM reel ou virtuel via Vue Test Utils. Les composables purement reactifs (sans hooks de cycle de vie) peuvent etre testes plus simplement avec effectScope de Vue.

Quelle strategie adopter pour les composables asynchrones en SSR ? En contexte SSR (Nuxt ou rendu serveur Vue), les composables asynchrones necessitent une attention particuliere. onMounted ne s'execute pas cote serveur, et les appels fetch doivent etre geres via les mecanismes SSR du framework (comme useAsyncData dans Nuxt). Le composable doit distinguer l'environnement d'execution et adapter son comportement en consequence.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Conclusion

Les composables Vue 3 representent bien plus qu'une evolution syntaxique par rapport aux mixins : ils incarnent un changement de paradigme dans la facon de structurer la logique reutilisable dans les applications frontend. Leur maitrise constitue un avantage competitif significatif, tant en production qu'en entretien technique.

Les points essentiels a retenir :

  • Typage strict : definir des interfaces explicites pour les options et les valeurs de retour renforce la fiabilite et l'experience developpeur
  • Gestion du cycle de vie : toujours nettoyer les effets de bord dans onUnmounted pour eviter les fuites memoire
  • Composition : concevoir des composables specialises qui se combinent plutot que des composables monolithiques
  • Reactivite fine : exploiter MaybeRefOrGetter, toValue et watchEffect pour maximiser la flexibilite
  • Injection de dependances : utiliser provide/inject avec des InjectionKey types pour les etats partages dans l'arbre de composants
  • Testabilite : la technique withSetup fournit un contexte reactif minimal pour valider chaque composable independamment
  • Validation : encapsuler les regles metier dans des composables de validation garantit leur coherence a travers l'application

La capacite a concevoir, composer et tester des composables robustes distingue les profils seniors sur le marche Vue en 2026. Ces patterns constituent le socle technique sur lequel reposent les applications Vue 3 et Nuxt 3 de production.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#vue
#composables
#composition-api
#interview
#deep-dive

Partager

Articles similaires