Composable Tingkat Lanjut di Vue 3: Pola Reusable dan Pertanyaan Wawancara 2026
Kuasai composable tingkat lanjut Vue 3 dengan pola reusable, penanganan error asinkron, dependency injection, dan validasi formulir. Dilengkapi pertanyaan wawancara teknis terkini untuk tahun 2026.

Composition API pada Vue 3 telah mengubah cara pengembang mengorganisasi dan menggunakan kembali logika dalam aplikasi frontend. Di pusat transformasi ini terdapat composable: fungsi-fungsi yang mengenkapsulasi state reaktif, logika bisnis, dan efek samping ke dalam unit-unit independen yang dapat digunakan ulang. Berbeda dengan mixin pada Vue 2 yang sering menimbulkan konflik penamaan dan dependensi tersembunyi, composable menawarkan transparansi penuh, dukungan tipado statis dengan TypeScript, serta komposisi yang eksplisit.
Pada tahun 2026, penguasaan pola composable tingkat lanjut telah menjadi persyaratan mendasar untuk posisi pengembang Vue level menengah hingga senior. Panduan ini mengulas pola-pola yang paling sering ditanyakan dalam wawancara teknis, mulai dari anatomi composable yang terstruktur dengan baik hingga dependency injection menggunakan provide/inject, termasuk strategi testing yang memvalidasi reaktivitas setiap komponen.
Composable adalah sebuah fungsi yang memanfaatkan Composition API Vue untuk mengenkapsulasi dan menggunakan kembali logika yang memiliki state. Secara konvensi, nama composable selalu diawali dengan use (misalnya useCounter, useFetch). Setiap composable mengembalikan ref reaktif, computed, dan fungsi-fungsi yang dapat dikonsumsi langsung oleh komponen, tanpa pewarisan maupun coupling implisit.
Anatomi Composable yang Terstruktur dengan Baik
Composable yang efektif mengikuti prinsip-prinsip yang jelas: menerima opsi konfigurasi melalui objek bertipe, mengembalikan interface yang eksplisit, dan menjaga state internalnya terisolasi dari konsumen lain. Contoh berikut mengimplementasikan counter dengan batas yang dapat dikonfigurasi serta nilai computed turunan.
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 }
}Beberapa aspek dari pola ini layak mendapat perhatian khusus. Interface UseCounterReturn mendefinisikan kontrak composable secara eksplisit, sehingga memudahkan autocompletion di editor dan mencegah perubahan API publik yang tidak disengaja. Objek opsi dengan nilai default memungkinkan konfigurasi fleksibel tanpa membebani signature fungsi. Setiap pemanggilan useCounter() menciptakan instance state yang independen, sehingga menghilangkan sepenuhnya masalah shared state yang sebelumnya menjangkiti mixin.
Pola "opsi masukan, interface bertipe keluaran" ini merupakan fondasi untuk membangun composable yang lebih kompleks.
Composable Asinkron dengan Penanganan Error
Operasi jaringan merupakan salah satu kasus penggunaan yang paling umum untuk composable. Pola berikut mengimplementasikan fungsi fetching reaktif yang secara otomatis membatalkan request yang sedang berjalan, menangani error secara granular, dan merespons perubahan pada URL sumber.
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 }
}Composable ini memperkenalkan beberapa konsep tingkat lanjut. Tipe MaybeRefOrGetter<string> menerima baik nilai statis, ref reaktif, maupun fungsi getter, sehingga memberikan fleksibilitas maksimal bagi konsumen. Fungsi toValue() mengekstrak nilai yang mendasarinya terlepas dari jenis input yang diberikan. Penggunaan AbortController mencegah race condition dengan membatalkan request yang sudah usang ketika URL berubah sebelum respons sebelumnya tiba.
watchEffect membentuk pelacakan reaktif secara otomatis: setiap ref atau getter yang diakses di dalam callback akan menjadi dependensi. Apabila URL berupa ref dan nilainya berubah, efek tersebut akan dieksekusi ulang secara otomatis. Hook onUnmounted menjamin pembersihan resource ketika komponen dihancurkan.
Hook seperti onMounted, onUnmounted, dan watchEffect hanya berfungsi dengan benar ketika composable dipanggil di dalam setup() atau <script setup>. Memanggil composable di luar konteks ini (misalnya di dalam callback asinkron yang ditunda) akan menghasilkan peringatan runtime dan efek yang tidak pernah terdaftar. Pembatasan ini merupakan salah satu pertanyaan yang paling sering muncul dalam wawancara teknis Vue.
Komposisi Antar-Composable
Kekuatan sesungguhnya dari composable muncul ketika satu composable digabungkan dengan yang lainnya. Contoh berikut membangun fitur pencarian berpaginasi dengan memanfaatkan kembali useFetchData dan useDebouncedRef hipotetis, menunjukkan bagaimana komposisi menghasilkan fungsionalitas kompleks dari potongan-potongan sederhana.
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 }
}Komposisi terjadi di berbagai tingkatan. useDebouncedRef mengembalikan ref dengan debounce bawaan yang mencegah request berlebihan selama pengetikan. useFetchData menerima apiUrl sebagai computed, yang berarti setiap perubahan pada query atau page akan menghitung ulang URL dan secara otomatis memicu request baru. Watcher pada query mereset paginasi ke halaman 1 setiap kali kata kunci pencarian berubah.
Pola ini mendemonstrasikan prinsip fundamental: setiap composable menyelesaikan satu masalah spesifik, dan komposisi menghubungkan mereka tanpa coupling. Dalam wawancara teknis, menjelaskan rantai reaktivitas ini (query berubah -> apiUrl dihitung ulang -> useFetchData dieksekusi ulang -> results diperbarui) menunjukkan pemahaman mendalam tentang sistem reaktif Vue.
Siap menguasai wawancara Vue.js / Nuxt.js Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Dependency Injection dengan provide/inject
Beberapa state perlu dibagikan melalui pohon komponen tanpa prop drilling. Sistem provide/inject Vue, yang dikombinasikan dengan composable, menciptakan pola konteks yang bertipe dan aman.
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
}Pola ini memisahkan penyediaan (provision) dan konsumsi. Komponen root (atau layout) memanggil provideTheme() sekali saja, dan komponen turunan mana pun dapat mengakses konteks melalui useTheme() tanpa memperhatikan kedalaman pohon. Penggunaan InjectionKey<ThemeContext> dengan Symbol menjamin keamanan tipe pada saat kompilasi.
Fungsi readonly() membungkus ref yang diekspos untuk mencegah mutasi langsung dari komponen konsumen. Hanya setTheme() yang dapat mengubah tema, sehingga memaksa aliran data searah. Properti resolvedTheme menyelesaikan nilai 'system' dengan menanyakan media query browser, selalu menyediakan nilai konkret ('light' atau 'dark') yang dapat langsung digunakan komponen untuk menerapkan style.
Dalam wawancara, pola ini sering dibandingkan dengan React Context atau store Pinia. Perbedaan utamanya adalah provide/inject beroperasi pada tingkat pohon komponen (bukan global) dan tidak memerlukan dependensi eksternal.
Composable Validasi Formulir
Validasi formulir merupakan domain di mana composable benar-benar bersinar, menggantikan logika repetitif dengan pola deklaratif berbasis aturan.
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 }
}Composable ini menerima nilai awal dan peta aturan validasi per field. Setiap aturan adalah fungsi yang mengembalikan true jika nilai valid, atau string berisi pesan error. Metode validate() melakukan iterasi pada aturan-aturan dan berhenti pada error pertama di setiap field, menghindari tampilan pesan error ganda yang dapat membingungkan pengguna.
Penggunaan reactive() sebagai pengganti ref() untuk fields dan errors menyederhanakan akses di template: fields.email alih-alih fields.value.email. Properti computed isValid diperbarui secara otomatis ketika error berubah, memungkinkan pengaktifan atau penonaktifan tombol submit secara reaktif.
Pola ini bersifat extensible. Aturan asinkron (verifikasi ketersediaan email), validasi lintas-field (konfirmasi kata sandi), dan pesan error yang telah diinternasionalisasi dapat ditambahkan tanpa mengubah struktur dasar composable.
Testing Composable
Composable yang menggunakan API reaktif Vue memerlukan konteks komponen agar dapat berfungsi dengan benar. Fungsi helper withSetup menyelesaikan kebutuhan ini dengan membuat komponen minimal yang menjalankan composable di dalam setup().
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)
})
})Fungsi withSetup merupakan pola standar dalam ekosistem Vue untuk menguji composable secara terisolasi. Pola ini me-mount komponen wrapper yang memanggil composable di dalam setup(), menyediakan akses ke hasil serta fungsi unmount() untuk mensimulasikan penghancuran komponen.
Setiap test memvalidasi aspek spesifik: inisialisasi dengan nilai default, kepatuhan terhadap batas yang dikonfigurasi, dan reaktivitas nilai computed. Test ketiga sangat penting karena memverifikasi bahwa doubled diperbarui secara otomatis ketika count berubah melalui increment(), mengonfirmasi bahwa graf reaktif berfungsi dengan benar.
Untuk composable asinkron seperti useFetchData, test memerlukan mock fetch dan penanganan promise menggunakan flushPromises() dari Vue Test Utils. Dalam wawancara, menunjukkan keakraban dengan pola testing ini mengkomunikasikan kematangan profesional dan komitmen terhadap kualitas kode.
VueUse (vueuse.org) adalah pustaka composable utilitas paling banyak digunakan di ekosistem Vue, dengan lebih dari 200 fungsi yang mencakup sensor browser, animasi, state, jaringan, dan penyimpanan. Sebelum membangun composable kustom, memeriksa apakah VueUse sudah menyediakan solusi yang telah teruji dan teroptimasi merupakan praktik terbaik yang dihargai oleh pewawancara.
Pertanyaan Umum dalam Wawancara Teknis
Pertanyaan-pertanyaan berikut sering muncul dalam proses seleksi untuk posisi Vue level menengah dan senior di tahun 2026:
1. Apa perbedaan antara composable dan mixin? Mixin menggabungkan properti ke dalam komponen secara implisit, sehingga menimbulkan konflik penamaan dan menyulitkan pelacakan asal setiap properti. Composable mengembalikan nilai secara eksplisit, menawarkan tipado lengkap dengan TypeScript, dan memungkinkan penamaan ulang variabel saat melakukan destructuring pada nilai kembalian.
2. Mengapa composable harus dipanggil di dalam setup()?
Vue mengasosiasikan lifecycle hook (onMounted, onUnmounted, watch, watchEffect) dengan instance komponen yang aktif selama setup(). Memanggil composable di luar konteks ini memutus asosiasi tersebut, mengakibatkan efek yang tidak pernah terdaftar dan tidak pernah dibersihkan.
3. Bagaimana cara berbagi state antar beberapa komponen dengan composable?
Terdapat dua strategi utama. Untuk state global, ref dideklarasikan di luar fungsi composable (pola singleton). Untuk state kontekstual pada subpohon komponen, digunakan provide/inject dengan InjectionKey bertipe, seperti yang didemonstrasikan pada contoh useTheme.
4. Apa itu MaybeRefOrGetter dan mengapa penting?
Ini adalah tipe utilitas Vue yang menerima nilai biasa, Ref, atau fungsi getter. Tipe ini memungkinkan composable menerima input baik statis maupun reaktif, sehingga memaksimalkan fleksibilitas bagi konsumen. Fungsi toValue() mengekstrak nilai yang mendasari tanpa memperhatikan jenis input.
5. Bagaimana cara menguji composable yang menggunakan onMounted atau onUnmounted?
Digunakan fungsi helper seperti withSetup yang me-mount komponen wrapper. Komponen ini menjalankan composable di dalam setup(), menyediakan konteks instance yang diperlukan. Untuk memverifikasi pembersihan, unmount() dipanggil kemudian diperiksa apakah efek samping (listener, timer, abort controller) telah dihapus.
6. Kapan sebaiknya menggunakan Pinia dibandingkan composable dengan provide/inject?
Pinia lebih sesuai ketika state perlu diakses secara global dari komponen mana pun tanpa relasi hierarkis, ketika diperlukan persistensi (localStorage/sessionStorage), ketika diperlukan integrasi dengan Vue DevTools untuk debugging, atau ketika beberapa pohon komponen perlu berbagi state yang sama. Composable dengan provide/inject lebih tepat untuk state kontekstual yang terbatas pada subpohon tertentu.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Kesimpulan
Composable pada Vue 3 lebih dari sekadar alternatif pengganti mixin: composable merupakan paradigma organisasi kode yang dapat diskalakan, mulai dari fungsi utilitas sederhana hingga sistem kompleks dengan dependency injection dan komposisi bertingkat. Penguasaan pola-pola ini membedakan pengembang yang sekadar menggunakan Vue dari mereka yang memahami sistem reaktifnya secara mendalam.
Poin-poin utama yang perlu dikonsolidasi:
- Struktur yang jelas: Interface bertipe untuk input dan output dengan
UseXxxOptionsdanUseXxxReturn - Isolasi state: Setiap pemanggilan menciptakan instance independen, menghilangkan konflik
- Komposisi eksplisit: Composable yang mengonsumsi composable lain menciptakan rantai reaktivitas yang dapat diprediksi
- Pengelolaan resource:
AbortControllerdanonUnmountedmenjamin pembersihan efek samping - Injection bertipe:
provide/injectdenganInjectionKeymenggantikan prop drilling tanpa mengorbankan keamanan tipe - Testabilitas: Pola
withSetupmemungkinkan validasi reaktivitas secara terisolasi penuh - Pragmatisme: Memeriksa VueUse terlebih dahulu sebelum mengimplementasi ulang fungsionalitas umum
Mempersiapkan pola-pola ini dengan implementasi konkret serta kemampuan menjelaskan keputusan desain di balik masing-masing pola memberikan keunggulan signifikan dalam proses seleksi teknis untuk posisi Vue di tahun 2026.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Vue 3 Pinia vs Vuex: Panduan Lengkap State Management dan Pertanyaan Interview 2026
Perbandingan mendalam Pinia vs Vuex: arsitektur, TypeScript support, Composition API, performa, strategi migrasi, dan pertanyaan interview Vue state management 2026.

Nuxt 4 di Tahun 2026: Struktur Direktori Baru dan Panduan Migrasi dari Nuxt 3
Panduan lengkap Nuxt 4 mencakup struktur direktori app/, singleton data fetching, shallow reactivity, serta langkah migrasi bertahap dari Nuxt 3 untuk developer Vue.js.

Pertanyaan wawancara Vue.js: 25 pertanyaan untuk meraih pekerjaan
Persiapkan wawancara Vue.js dengan 25 pertanyaan penting ini. Dari reaktivitas hingga composables, kuasai konsep utama untuk wawancara berikutnya.