Vue 3 Script Setup en defineModel in 2026: Moderne Syntax en Sollicitatievragen
Uitgebreide gids over Vue 3 script setup en defineModel voor tweerichtings-databinding. Behandelt reactieve props-destructuring, TypeScript-integratie en veelvoorkomende sollicitatievragen.

De Vue 3 Script Setup syntax transformeert hoe componenten worden geschreven door boilerplate te elimineren en TypeScript-integratie te verbeteren. Gecombineerd met defineModel, experimenteel geïntroduceerd in Vue 3.3 en gestabiliseerd in Vue 3.4, vereist het bouwen van componenten met tweerichtings-databinding veel minder code dan het Options API equivalent.
Vanaf Vue 3.5 kunnen props direct worden gedestructureerd uit defineProps terwijl de reactiviteit behouden blijft. Dit elimineert de noodzaak voor toRefs in de meeste gevallen.
Fundamenten van Script Setup in Vue 3.5
De <script setup> syntax is compile-time syntactische suiker voor de Composition API binnen Single File Components. Variabelen gedeclareerd op het hoogste niveau worden automatisch beschikbaar gesteld aan de template, en imports zijn direct bruikbaar zonder registratie.
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User } from '@/types'
// Props met TypeScript interface
const props = defineProps<{
user: User
showEmail?: boolean
}>()
// Reactieve state
const isExpanded = ref(false)
// Computed property
const displayName = computed(() => {
return `${props.user.firstName} ${props.user.lastName}`
})
// Event emitter met getypeerde payload
const emit = defineEmits<{
select: [userId: string]
delete: [userId: string]
}>()
// Methodes zijn gewone functies
function handleSelect() {
emit('select', props.user.id)
}
</script>
<template>
<div class="user-card" @click="handleSelect">
<h3>{{ displayName }}</h3>
<p v-if="showEmail">{{ user.email }}</p>
<button @click.stop="isExpanded = !isExpanded">
{{ isExpanded ? 'Inklappen' : 'Uitklappen' }}
</button>
</div>
</template>De compiler handelt export statements en componentregistratie automatisch af. Dit vermindert de cognitieve belasting vergeleken met het expliciet retourneren van waarden uit een setup() functie.
Reactieve Props Destructuring (Vue 3.5+)
Voor Vue 3.5 verbrak het destructureren van props de reactiviteit omdat JavaScript destructuring een snapshot van de waarde op dat moment creëert. Vue 3.5 introduceert compiler-niveau tracking dat de reactiviteit behoudt voor gedestructureerde props.
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watch } from 'vue'
// Destructureren met defaults - blijft reactief in 3.5+
const { query = '', placeholder = 'Zoeken...' } = defineProps<{
query?: string
placeholder?: string
}>()
// Watch werkt op gedestructureerde props
watch(
() => query,
(newQuery) => {
console.log('Query gewijzigd:', newQuery)
}
)
</script>
<template>
<input
:value="query"
:placeholder="placeholder"
@input="$emit('update:query', ($event.target as HTMLInputElement).value)"
/>
</template>Deze syntax vereist Vue 3.5 of hoger. De Vue 3.5 release-aankondiging documenteert het volledige gedrag van reactieve props destructuring.
defineModel voor Tweerichtings-binding
Voor defineModel vereiste het implementeren van v-model op een aangepast component het declareren van een prop en het handmatig emitten van een update-event. Dit patroon verschijnt in talloze Vue codebases:
<!-- De oude manier - uitgebreid maar expliciet -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
function updateValue(event: Event) {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>Met defineModel wordt hetzelfde component aanzienlijk korter:
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel creëert een ref die synchroniseert met v-model
const model = defineModel<string>({ default: '' })
</script>
<template>
<input
v-model="model"
type="text"
class="text-input"
/>
</template>Het oudercomponent gebruikt de standaard v-model syntax:
<!-- ParentForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import TextInput from './TextInput.vue'
const username = ref('')
</script>
<template>
<TextInput v-model="username" />
<p>Gebruikersnaam: {{ username }}</p>
</template>defineModel retourneert een ref die gesynchroniseerd blijft met de gebonden waarde van de ouder. Mutaties aan deze ref emitten automatisch het update-event.
Klaar om je Vue.js / Nuxt.js gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Benoemde Models en Meerdere v-model Bindingen
Componenten hebben soms meerdere tweerichtings-bindingen nodig. Vue ondersteunt benoemde v-model bindingen, en defineModel handelt deze af door een naam-argument te accepteren.
<!-- DateRangePicker.vue -->
<script setup lang="ts">
import { computed } from 'vue'
// Benoemde models voor meerdere v-model bindingen
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })
// Validatie computed
const isValidRange = computed(() => {
if (!startDate.value || !endDate.value) return true
return startDate.value <= endDate.value
})
</script>
<template>
<div class="date-range-picker">
<input
type="date"
:value="startDate?.toISOString().split('T')[0]"
@input="startDate = new Date(($event.target as HTMLInputElement).value)"
/>
<span>tot</span>
<input
type="date"
:value="endDate?.toISOString().split('T')[0]"
@input="endDate = new Date(($event.target as HTMLInputElement).value)"
/>
<p v-if="!isValidRange" class="error">Einddatum moet na de startdatum liggen</p>
</div>
</template>De ouder bindt beide models:
<DateRangePicker v-model:start="filterStart" v-model:end="filterEnd" />v-model Modifiers met defineModel
Vue's ingebouwde modifiers zoals .trim, .number en .lazy werken automatisch met defineModel. Aangepaste modifiers vereisen expliciete afhandeling via het tweede element van de geretourneerde tuple.
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Toegang tot modifiers via de tweede retourwaarde
const [model, modifiers] = defineModel<number>({
default: 0,
// Transformatiefuncties voor aangepaste modifiers
set(value) {
// Rond af op 2 decimalen als 'round' modifier aanwezig is
if (modifiers.round) {
return Math.round(value * 100) / 100
}
return value
}
})
</script>
<template>
<div class="currency-input">
<span>€</span>
<input
type="number"
step="0.01"
:value="model"
@input="model = parseFloat(($event.target as HTMLInputElement).value) || 0"
/>
</div>
</template>Gebruik met de aangepaste modifier:
<CurrencyInput v-model.round="price" />TypeScript Patronen voor Script Setup
Type safety in <script setup> componenten is gebaseerd op generics die worden doorgegeven aan compiler-macro's. Het patroon varieert licht tussen props, emits en models.
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string }">
import { computed } from 'vue'
// Generieke component props
const props = defineProps<{
items: T[]
selected?: T
}>()
const emit = defineEmits<{
select: [item: T]
remove: [id: string]
}>()
// Slots kunnen ook worden getypeerd
defineSlots<{
default: (props: { item: T; index: number }) => any
empty: () => any
}>()
const hasItems = computed(() => props.items.length > 0)
</script>
<template>
<ul v-if="hasItems">
<li
v-for="(item, index) in items"
:key="item.id"
:class="{ selected: selected?.id === item.id }"
@click="emit('select', item)"
>
<slot :item="item" :index="index">
{{ item.id }}
</slot>
</li>
</ul>
<div v-else>
<slot name="empty">Geen items</slot>
</div>
</template>Generieke componenten, beschikbaar sinds Vue 3.3, maken herbruikbare getypeerde componenten mogelijk zonder type-inferentie in de consumerende code op te offeren.
Veelvoorkomende Sollicitatievragen over Script Setup
Interviewers die Vue-kennis testen, focussen vaak op de verschillen tussen Script Setup en het Options API, evenals de mechanica van reactiviteit.
Vraag: Waarom verbreekt het destructureren van props de reactiviteit, en hoe lost Vue 3.5 dit op?
In JavaScript creëert destructuring nieuwe variabelen die de waarden op het moment van destructureren bevatten. Als props.count later verandert, houdt een gedestructureerde const { count } = props nog steeds de oude waarde vast. De compiler van Vue 3.5 transformeert de gedestructureerde variabelen naar getters die opnieuw evalueren wanneer ze worden benaderd, waardoor reactiviteit behouden blijft zonder runtime overhead.
Vraag: Wat gebeurt er onder de motorkap wanneer defineModel wordt gebruikt?
De compiler breidt defineModel uit naar een prop-declaratie en een emit-registratie. Tijdens runtime creëert het een ref waarvan de getter de prop-waarde leest en waarvan de setter emit('update:modelValue', newValue) aanroept. Deze ref is geen echte kopie van de prop maar een proxy die reads en writes correct doorstuurt.
Vraag: Wanneer moet een component Script Setup gebruiken versus een reguliere setup-functie?
Script Setup heeft de voorkeur voor de meeste Single File Components omdat het boilerplate vermindert en IDE-ondersteuning verbetert. Een reguliere setup() functie is nodig wanneer het component methodes moet blootstellen aan oudercomponenten via defineExpose in complexe patronen, of bij het bouwen van renderless componenten die render-functies direct retourneren.
Migratie van Options API naar Script Setup
Bestaande codebases bevatten vaak Options API componenten die baat hebben bij migratie. De conversie volgt een voorspelbaar patroon:
<!-- Voor: Options API -->
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
props: {
items: { type: Array as PropType<string[]>, required: true }
},
emits: ['select'],
data() {
return { searchTerm: '' }
},
computed: {
filteredItems() {
return this.items.filter(item =>
item.toLowerCase().includes(this.searchTerm.toLowerCase())
)
}
},
methods: {
selectItem(item: string) {
this.$emit('select', item)
}
}
})
</script><!-- Na: Script Setup -->
<script setup lang="ts">
import { ref, computed } from 'vue'
const props = defineProps<{ items: string[] }>()
const emit = defineEmits<{ select: [item: string] }>()
const searchTerm = ref('')
const filteredItems = computed(() =>
props.items.filter(item =>
item.toLowerCase().includes(searchTerm.value.toLowerCase())
)
)
function selectItem(item: string) {
emit('select', item)
}
</script>De gemigreerde versie is korter en type-inferentie werkt zonder PropType casts. Het Vue-team biedt vue-codemod voor geautomatiseerde migraties, hoewel handmatige review noodzakelijk blijft voor complexe componenten.
Prestatie-overwegingen bij Script Setup
Script Setup componenten compileren naar efficiëntere code omdat de compiler de exacte vorm van het component kent tijdens build-tijd. Specifiek:
- Template-bindingen worden opgelost tijdens compile-time in plaats van runtime property lookup
- Ongebruikte imports worden effectiever ge-tree-shaked
- Het ontbreken van een wrapper-functie vermindert closure-overhead
Voor applicaties die prioriteit geven aan bundlegrootte, elimineert Script Setup met Vapor Mode (beschikbaar vanaf Vue 3.6+) het virtuele DOM volledig voor opt-in componenten, en genereert in plaats daarvan directe DOM-operaties.
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Kernpunten over Vue 3 Script Setup en defineModel
- Script Setup vermindert component-boilerplate door automatisch top-level bindingen beschikbaar te stellen aan templates en componentregistratie af te handelen
defineModelvervangt het handmatige prop + emit patroon voorv-model, en retourneert een ref die synchroniseert met de gebonden waarde van de ouder- Vue 3.5 introduceert reactieve props destructuring, waardoor
const { prop } = defineProps()mogelijk is zonder reactiviteit te verliezen - Benoemde models ondersteunen meerdere
v-modelbindingen op een enkel component metdefineModel('name') - TypeScript-integratie verbetert met generieke componenten, getypeerde emits en getypeerde slot-ondersteuning via compiler-macro's
Zie jij de bug in Vue.js / Nuxt.js?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 18 september 2026
Tags
Delen
Gerelateerde artikelen

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.

Nuxt 4 in 2026: Nieuwe Directorystructuur en Migratie vanuit Nuxt 3
Uitgebreide gids over Nuxt 4 in 2026: de nieuwe app/-directorystructuur, shallow reactivity, verbeterde TypeScript-integratie, Unhead v2 en een praktische migratiechecklist voor Nuxt 3-projecten.

Vue 3 Reactivity Transform in 2026: $ref, $computed en Sollicitatievragen
Uitgebreide analyse van Vue 3 Reactivity Transform met $ref, $computed en $$ macro's. Praktische codevoorbeelden, migratie van ref() naar $ref, en veelgestelde sollicitatievragen voor senior Vue-ontwikkelaars.