Geavanceerde Vue 3 Composables: Herbruikbare Patronen en Interviewvragen 2026

Een diepgaande gids over geavanceerde Vue 3 composable-patronen, van async data-ophaling tot formuliervalidatie en dependency injection. Inclusief veelgestelde interviewvragen over de Composition API in 2026.

Vue 3 Composables Advanced Patterns

Vue 3 composables zijn uitgegroeid tot het standaardmechanisme voor het extraheren en hergebruiken van reactieve logica in componenten. Nu Vue 3.6 Alien Signals en Vapor Mode-compatibiliteit introduceert, heeft het beheersen van composable-patronen in 2026 directe invloed op applicatiearchitectuur, prestaties en onderhoudbaarheid.

Wat is een Vue 3 Composable?

Een composable is een functie die gebruikmaakt van de Composition API om stateful logica in te kapselen en te hergebruiken. In tegenstelling tot mixins bieden composables expliciete invoer en uitvoer, volledige TypeScript-inferentie en geen naamgevingsconflicten. De conventie is om composable-namen te voorzien van het voorvoegsel use (bijvoorbeeld useCounter, useFetch).

Anatomie van een Goed Gestructureerde Composable

Een productiewaardige composable volgt een voorspelbare structuur: configuratie accepteren via argumenten, intern reactieve state aanmaken en een getypeerd return-object blootstellen. Dit patroon waarborgt composeerbaarheid, testbaarheid en duidelijke API-grenzen.

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 }
}

Het expliciete return-type UseCounterReturn dient twee doelen: het documenteert de publieke API van de composable en het voorkomt onbedoelde blootstelling van interne implementatiedetails. Consumerende componenten destructureren precies wat ze nodig hebben, waardoor template-bindings transparant blijven.

Async Composables met Foutafhandeling en Laadstatussen

Het ophalen van data blijft een van de meest voorkomende toepassingen voor composables. Een robuuste async composable beheert laadstatus, foutafhandeling en automatische opschoning — patronen waar interviewers regelmatig naar vragen.

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 }
}

Verschillende details zijn hier van belang. Het parametertype MaybeRefOrGetter<string> accepteert gewone strings, refs of getters, wat maximale flexibiliteit biedt voor aanroepers. De AbortController voorkomt race conditions wanneer de URL snel verandert. Opschoning via onUnmounted vermijdt geheugenlekken en state-updates op vernietigde componenten.

Lifecycle Hooks Binnen Composables

Composables die onMounted, onUnmounted of andere lifecycle hooks aanroepen, moeten synchroon worden aangeroepen binnen setup(). Het aanroepen van een composable binnen een async callback of een setTimeout zal er stilzwijgend voor zorgen dat de lifecycle hook niet wordt geregistreerd, omdat er geen actieve component-instantie is.

Composable Compositie: Hogere Abstracties Bouwen

De werkelijke kracht van composables komt naar voren wanneer ze andere composables samenstellen. Dit weerspiegelt het principe van functionele compositie: kleine, gerichte eenheden worden gecombineerd tot complex gedrag zonder overerving.

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 }
}

Dit voorbeeld illustreert hoe usePaginatedSearch drie bestaande composables samenbrengt: useDebouncedRef voor vertraagde zoekinvoer, useFetchData voor het daadwerkelijk ophalen van data, en standaard Vue-reactiviteit voor paginabeheer. Het resultaat is een complete zoekoplossing met paginering die in elke component kan worden ingezet met een enkele functieaanroep. Deze aanpak elimineert codeduplicatie en maakt elke individuele composable onafhankelijk testbaar.

Klaar om je Vue.js / Nuxt.js gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Dependency Injection met provide/inject voor Composable-Architectuur

Voor applicatiebrede state die gedeeld moet worden over de componentenboom zonder prop drilling, bieden provide en inject een elegant mechanisme. Het combineren van deze Vue-primitieven met composables levert een schone architectuur op die zowel typeveilig als goed gestructureerd is.

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
}

Het patroon van gescheiden provide- en inject-functies versterkt de scheiding van verantwoordelijkheden. De provideTheme-functie wordt aangeroepen in een bovenliggende component (doorgaans het root-component of een layout-component), terwijl useTheme overal in de componentenboom beschikbaar is. Het gebruik van readonly voor de blootgestelde refs voorkomt dat consumerende componenten de state rechtstreeks muteren, waardoor alle wijzigingen via de setTheme-methode verlopen. De InjectionKey met generiek type zorgt ervoor dat TypeScript het juiste type afleidt bij het aanroepen van inject.

Herbruikbare Formuliervalidatie-Composable

Formuliervalidatie is een ander domein waar composables aanzienlijke waarde toevoegen. In plaats van validatielogica te verspreiden over meerdere componenten, centraliseert een validatie-composable de regels en foutafhandeling in een herbruikbare eenheid.

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 }
}

De generieke typeparameter T stelt deze composable in staat om met elk formuliermodel te werken. De validatieregels worden gedefinieerd als een array van functies per veld, die ofwel true retourneren (geldig) of een foutmelding als string (ongeldig). Het break-statement na de eerste fout per veld voorkomt dat gebruikers worden overspoeld met meerdere foutmeldingen tegelijk. De isValid computed property biedt een reactieve samenvatting van de gehele formulierstatus, wat handig is voor het conditioneel activeren van verzendknoppen.

Composables Testen in Isolatie

Het testen van composables vereist een lichte wrapper, aangezien composables binnen een actieve Vue component-context moeten draaien. Het withSetup-hulppatroon biedt een minimale component die de composable aanroept en het resultaat blootstelt voor assertions.

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)
  })
})

Dit testpatroon is bijzonder waardevol omdat het de composable isoleert van component-specifieke zorgen. De withSetup-functie creert een wegwerp-component uitsluitend voor het bieden van de benodigde Vue-context. Na het aanroepen is result direct beschikbaar voor assertions. De unmount-functie maakt het mogelijk om opschoningsgedrag te testen, zoals het afbreken van lopende verzoeken of het verwijderen van event listeners.

VueUse als Referentiearchitectuur

VueUse bevat meer dan 200 productie-composables die browser-API's, sensoren, animaties en hulpprogramma's omvatten. Het bestuderen van de broncode onthult consistente patronen: options-objecten voor configuratie, SSR-veiligheidscontroles en tryOnScopeDispose voor opschoning. Het dient als een uitstekende referentie voor architectuurbeslissingen rondom composables.

Veelgestelde Interviewvragen over Vue 3 Composables

Deze sectie behandelt de meest voorkomende technische interviewvragen over Vue 3 composables. Elke vraag weerspiegelt scenario's die zich voordoen in professionele Vue-ontwikkeling en die aantonen of een kandidaat de Composition API op diepgaand niveau begrijpt.

Waarin verschillen composables van mixins, en waarom zijn mixins afgeraden?

Mixins kampen met drie structurele problemen: impliciet samenvoegen van properties leidt tot naamgevingsconflicten, de herkomst van data en methoden wordt ondoorzichtig in componenten die meerdere mixins gebruiken, en TypeScript kan geen types afleiden via mixin-compositie. Composables lossen alle drie de problemen op door expliciete objecten te retourneren, waardoor de dataflow traceerbaar wordt, en door volledige type-inferentie te bieden via standaard functiesignaturen.

Wanneer moet een composable shallowRef gebruiken in plaats van ref?

Gebruik shallowRef wanneer de ref een groot object of array bevat dat in zijn geheel wordt vervangen in plaats van gemuteerd. Aangezien shallowRef alleen toewijzing bijhoudt (geen diepe property-wijzigingen), vermijdt het de overhead van diepe reactieve proxying. Vue 3.6 gebruikt om deze prestatieredenen standaard ondiepe reactiviteit in bepaalde contexten.

Wat gebeurt er als een composable onMounted aanroept binnen een async functie?

De registratie van de lifecycle hook faalt stilzwijgend. Vue koppelt lifecycle hooks aan de momenteel actieve component-instantie, die wordt ingesteld tijdens de synchrone uitvoering van setup(). Tegen de tijd dat een async callback wordt uitgevoerd, kan de actieve instantie null zijn of een andere component betreffen. De oplossing: roep lifecycle hooks synchroon aan op het bovenste niveau van de composable, en gebruik watchEffect of watch voor asynchrone logica.

Hoe werken composables samen met Vue's Vapor Mode in 3.6?

Vapor Mode compileert templates naar directe DOM-operaties en omzeilt daarmee de virtuele DOM. Composables werken identiek in Vapor Mode omdat ze opereren op de reactiviteitslaag, niet op de renderinglaag. De reactieve refs en computed properties van composables activeren fijnmazige DOM-updates die efficienter verlopen onder Vapor Mode, waardoor goed gestructureerde composables een prestatievoordeel opleveren.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Conclusie

Het beheersen van Vue 3 composables is in 2026 onmisbaar voor zowel het bouwen van schaalbare applicaties als het succesvol doorlopen van technische interviews. De belangrijkste inzichten uit dit artikel:

  • Structuur is bepalend: een goed gestructureerde composable accepteert configuratie via een options-object, beheert interne reactieve state en retourneert een getypeerd interface. Dit patroon garandeert herbruikbaarheid en testbaarheid.
  • Async composables vereisen discipline: het correct afhandelen van laadstatussen, fouten en het annuleren van lopende verzoeken via AbortController onderscheidt productiewaardige code van prototypes.
  • Compositie boven overerving: de kracht van composables ligt in het samenstellen van kleinere eenheden tot complexe functionaliteit. Dit functionele patroon elimineert de broosheid van overervingshierarchieen.
  • Dependency injection via provide/inject biedt een typeveilig alternatief voor global state management, vooral geschikt voor thema's, authenticatie en andere cross-cutting concerns.
  • Testbaarheid is ingebouwd: met het withSetup-patroon kunnen composables in volledige isolatie worden getest, los van de componenten die ze consumeren.
  • Vapor Mode-compatibiliteit: composables die correct zijn gestructureerd, profiteren automatisch van de prestatieverbeteringen van Vapor Mode, aangezien ze op de reactiviteitslaag opereren.

Het investeren in het doorgronden van deze patronen levert niet alleen betere applicaties op, maar bereidt ontwikkelaars ook voor op de diepgaande technische vragen die in moderne Vue-interviews aan bod komen.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#vue
#composables
#typescript
#interview

Delen

Gerelateerde artikelen