Vue 3 Script Setup dan defineModel di 2026: Sintaks Modern dan Pertanyaan Interview

Panduan lengkap tentang sintaks script setup Vue 3, defineModel untuk two-way binding, reactive props destructure di Vue 3.5, dan pertanyaan interview yang sering diajukan.

Vue 3 script setup dan defineModel tutorial

Sintaks script setup Vue 3 mengubah cara penulisan komponen dengan menghilangkan boilerplate dan meningkatkan integrasi TypeScript. Dikombinasikan dengan defineModel, yang diperkenalkan di Vue 3.3 sebagai fitur eksperimental dan distabilkan di Vue 3.4, pembuatan komponen dengan two-way data binding membutuhkan kode yang jauh lebih sedikit dibandingkan dengan Options API.

Vue 3.5 Reactive Props Destructure

Mulai dari Vue 3.5, props dapat di-destructure langsung dari defineProps dengan tetap mempertahankan reaktivitas. Ini menghilangkan kebutuhan untuk toRefs dalam kebanyakan kasus.

Dasar-Dasar Script Setup di Vue 3.5

Sintaks <script setup> adalah compile-time sugar untuk Composition API di dalam Single File Components. Variabel yang dideklarasikan di level atas secara otomatis terekspos ke template, dan import dapat langsung digunakan tanpa registrasi.

vue
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User } from '@/types'

// Props dengan TypeScript interface
const props = defineProps<{
  user: User
  showEmail?: boolean
}>()

// Reactive state
const isExpanded = ref(false)

// Computed property
const displayName = computed(() => {
  return `${props.user.firstName} ${props.user.lastName}`
})

// Event emitter dengan typed payload
const emit = defineEmits<{
  select: [userId: string]
  delete: [userId: string]
}>()

// Methods adalah plain functions
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 ? 'Collapse' : 'Expand' }}
    </button>
  </div>
</template>

Compiler menangani export statements dan registrasi komponen secara otomatis. Ini mengurangi beban kognitif dibandingkan dengan mengembalikan nilai secara eksplisit dari fungsi setup().

Reactive Props Destructure (Vue 3.5+)

Sebelum Vue 3.5, destructuring props merusak reaktivitas karena destructuring JavaScript membuat snapshot dari nilai pada saat itu. Vue 3.5 memperkenalkan tracking di level compiler yang mempertahankan reaktivitas untuk props yang di-destructure.

vue
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watch } from 'vue'

// Destructure dengan defaults - tetap reaktif di 3.5+
const { query = '', placeholder = 'Search...' } = defineProps<{
  query?: string
  placeholder?: string
}>()

// Watch berfungsi pada props yang di-destructure
watch(
  () => query,
  (newQuery) => {
    console.log('Query changed:', newQuery)
  }
)
</script>

<template>
  <input
    :value="query"
    :placeholder="placeholder"
    @input="$emit('update:query', ($event.target as HTMLInputElement).value)"
  />
</template>

Sintaks ini membutuhkan Vue 3.5 atau lebih tinggi. Dokumentasi Vue 3.5 release announcement mencatat daftar lengkap perilaku reactive props destructure.

defineModel untuk Two-Way Binding

Sebelum defineModel, implementasi v-model pada komponen kustom memerlukan deklarasi prop dan emit update event secara manual. Pola ini muncul di banyak codebase Vue:

vue
<!-- Cara lama - verbose tapi eksplisit -->
<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>

Dengan defineModel, komponen yang sama menjadi jauh lebih singkat:

vue
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel membuat ref yang sync dengan v-model
const model = defineModel<string>({ default: '' })
</script>

<template>
  <input
    v-model="model"
    type="text"
    class="text-input"
  />
</template>

Komponen parent menggunakan sintaks v-model standar:

vue
<!-- 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>Username: {{ username }}</p>
</template>

defineModel mengembalikan ref yang tetap tersinkronisasi dengan nilai yang di-bind dari parent. Mutasi pada ref ini secara otomatis emit update event.

Siap menguasai wawancara Vue.js / Nuxt.js Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Named Models dan Multiple v-model Bindings

Komponen terkadang membutuhkan multiple two-way bindings. Vue mendukung named v-model bindings, dan defineModel menanganinya dengan menerima argumen nama.

vue
<!-- DateRangePicker.vue -->
<script setup lang="ts">
// Named models untuk multiple v-model bindings
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })

// Validation 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>to</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">End date must be after start date</p>
  </div>
</template>

Parent mengikat kedua model:

vue
<DateRangePicker v-model:start="filterStart" v-model:end="filterEnd" />

Modifier v-model dengan defineModel

Modifier bawaan Vue seperti .trim, .number, dan .lazy bekerja secara otomatis dengan defineModel. Modifier kustom memerlukan penanganan eksplisit melalui elemen kedua dari tuple yang dikembalikan.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Akses modifiers melalui nilai return kedua
const [model, modifiers] = defineModel<number>({
  default: 0,
  // Fungsi transform untuk modifier kustom
  set(value) {
    // Bulatkan ke 2 desimal jika modifier 'round' ada
    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>

Penggunaan dengan modifier kustom:

vue
<CurrencyInput v-model.round="price" />

Pola TypeScript untuk Script Setup

Type safety di komponen <script setup> bergantung pada generics yang diteruskan ke compiler macros. Polanya sedikit berbeda antara props, emits, dan models.

vue
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string }">
import { computed } from 'vue'

// Generic component props
const props = defineProps<{
  items: T[]
  selected?: T
}>()

const emit = defineEmits<{
  select: [item: T]
  remove: [id: string]
}>()

// Slots juga bisa di-type
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">No items</slot>
  </div>
</template>

Generic components, tersedia sejak Vue 3.3, memungkinkan komponen typed yang reusable tanpa mengorbankan type inference di kode yang mengkonsumsinya. Untuk pola lebih dalam, dokumentasi Vue tentang TypeScript mencakup edge cases.

Pertanyaan Interview Umum tentang Script Setup

Pewawancara yang menguji pengetahuan Vue sering fokus pada perbedaan antara script setup dan Options API, serta mekanisme reaktivitas.

Pertanyaan: Mengapa destructuring props merusak reaktivitas, dan bagaimana Vue 3.5 menyelesaikan ini?

Dalam JavaScript, destructuring membuat variabel baru yang menyimpan nilai pada saat destructure. Jika props.count berubah kemudian, const { count } = props yang di-destructure tetap menyimpan nilai lama. Compiler Vue 3.5 mentransformasi variabel yang di-destructure menjadi getters yang mengevaluasi ulang saat diakses, mempertahankan reaktivitas tanpa overhead runtime.

Pertanyaan: Apa yang terjadi di balik layar ketika defineModel digunakan?

Compiler mengembangkan defineModel menjadi deklarasi prop dan registrasi emit. Pada runtime, ini membuat ref yang getter-nya membaca nilai prop dan setter-nya memanggil emit('update:modelValue', newValue). Ref ini bukan salinan sejati dari prop tetapi proxy yang meneruskan reads dan writes dengan tepat.

Pertanyaan: Kapan komponen harus menggunakan script setup versus fungsi setup reguler?

Script setup lebih disukai untuk kebanyakan single-file components karena mengurangi boilerplate dan meningkatkan dukungan IDE. Fungsi setup() reguler diperlukan ketika komponen perlu mengekspos methods ke komponen parent melalui defineExpose dalam pola kompleks, atau ketika membangun renderless components yang mengembalikan render functions secara langsung. Untuk persiapan interview, eksplorasi lebih lanjut pola Vue composables.

Migrasi dari Options API ke Script Setup

Codebase yang ada sering berisi komponen Options API yang dapat diuntungkan dari migrasi. Konversi mengikuti pola yang dapat diprediksi:

vue
<!-- Sebelum: 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>
vue
<!-- Sesudah: 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>

Versi yang dimigrasi lebih pendek dan type inference bekerja tanpa cast PropType. Tim Vue menyediakan vue-codemod untuk migrasi otomatis, meskipun review manual tetap diperlukan untuk komponen kompleks.

Pertimbangan Performa di Script Setup

Komponen script setup dikompilasi ke kode yang lebih efisien karena compiler mengetahui bentuk pasti dari komponen pada saat build. Secara spesifik:

  • Template bindings resolve pada compile time daripada runtime property lookup
  • Unused imports di-tree-shake lebih efektif
  • Tidak adanya wrapping function mengurangi closure overhead

Untuk aplikasi yang memprioritaskan bundle size, script setup dengan Vapor Mode (tersedia di Vue 3.6+) menghilangkan virtual DOM sepenuhnya untuk komponen yang opted-in, menghasilkan operasi DOM langsung sebagai gantinya.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Kesimpulan Utama untuk Vue 3 Script Setup dan defineModel

  • Script setup mengurangi boilerplate komponen dengan secara otomatis mengekspos top-level bindings ke templates dan menangani registrasi komponen
  • defineModel menggantikan pola manual prop + emit untuk v-model, mengembalikan ref yang sync dengan nilai yang di-bind parent
  • Vue 3.5 memperkenalkan reactive props destructure, memungkinkan const { prop } = defineProps() tanpa kehilangan reaktivitas
  • Named models mendukung multiple v-model bindings pada satu komponen menggunakan defineModel('name')
  • TypeScript generics bekerja dengan defineProps, defineEmits, defineSlots, dan generic components melalui atribut generic
  • Migrasi dari Options API mengikuti pola mekanis: data menjadi refs, computed tetap computed, methods menjadi functions, dan compiler menangani exports
Tantangan harian

Bisakah kamu menemukan bug di Vue.js / Nuxt.js?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 18 September 2026

Tag

#vue 3
#script setup
#defineModel
#composition api
#typescript

Bagikan

Artikel terkait