Desain Komponen yang Tidak Terlalu Pintar atau Terlalu Bodoh
Salah satu kebiasaan yang paling sering bikin frontend Vue jadi berantakan bukan soal state management atau library apa yang dipakai, tapi soal siapa yang bertanggung jawab atas apa di dalam satu komponen. Saya sering lihat dua ekstrem: komponen yang "terlalu pintar" — dia yang fetch data, dia yang atur state global, dia juga yang render UI dan tahu detail bisnis — atau sebaliknya, komponen yang "terlalu bodoh" sampai logic sederhana pun ditaruh di parent, bikin komponen itu jadi tidak bisa dipakai di tempat lain tanpa nyeret setengah aplikasi. Dua-duanya bikin proyek susah di-maintain jangka panjang, apalagi kalau tim ikut berkembang dan komponen dipakai berulang di banyak halaman.
Kenapa pembagian ini penting
Di sistem yang saya kerjakan sehari-hari — dashboard klinis, modul administrasi, panel di Filament — pola yang sama selalu muncul: satu form atau satu tabel yang tadinya sederhana, lama-lama dipakai di banyak konteks berbeda. Kalau komponen itu dari awal digabung antara "cara menampilkan" dan "dari mana data datang", setiap kali dipakai di tempat baru saya harus modifikasi komponen aslinya, bukan sekadar pasang di tempat baru. Efeknya komponen jadi penuh if khusus konteks, props yang membengkak, dan risiko regresi di halaman lain setiap kali ada perubahan kecil.
Komponen presentasi vs komponen yang tahu data
Prinsip yang saya pegang: pisahkan komponen presentasi (sering disebut "dumb component" atau "presentational component") dari komponen yang mengurus data ("smart component" atau "container component"). Komponen presentasi hanya menerima data lewat props, dan mengomunikasikan interaksi lewat emit — dia tidak tahu dari mana data itu datang, apakah dari API, dari store, atau dari data dummy saat testing.
<!-- PatientCard.vue (presentational) -->
<template>
<div class="card">
<h3>{{ patient.name }}</h3>
<p>{{ patient.status }}</p>
<button @click="$emit('select', patient.id)">Pilih</button>
</div>
</template>
<script setup>
defineProps(['patient'])
defineEmits(['select'])
</script>Komponen ini tidak tahu apa-apa soal API, Pinia store, atau autentikasi. Dia bisa dipakai di halaman mana pun, ditest tanpa mocking apa pun, dan gampang dimengerti orang baru di tim hanya dengan baca satu file. Sebaliknya, komponen container yang mengurus pengambilan data:
<!-- PatientListContainer.vue (container) -->
<script setup>
import { ref, onMounted } from 'vue'
import PatientCard from './PatientCard.vue'
const patients = ref([])
onMounted(async () => {
patients.value = await fetch('/api/patients').then(r => r.json())
})
</script>
<template>
<PatientCard
v-for="p in patients"
:key="p.id"
:patient="p"
@select="handleSelect"
/>
</template>Container ini boleh "kotor" — dia tahu soal fetch, error handling, loading state — tapi kekotoran itu terisolasi di satu tempat, tidak menyebar ke komponen presentasi yang dipakai di banyak tempat.
Batas yang sering kabur
Yang sering jadi jebakan adalah komponen "setengah pintar" — misalnya komponen presentasi yang diam-diam import useAuthStore hanya untuk cek satu permission, atau komponen container yang juga langsung merender markup kompleks. Saya biasa pakai aturan kasar: kalau sebuah komponen butuh import dari store, composable yang manggil API, atau router, dia otomatis masuk kategori "tahu data", dan sebaiknya markup di dalamnya dijaga seminimal mungkin — cukup memanggil komponen presentasi di bawahnya. Kalau sebuah komponen sudah menerima semua kebutuhannya lewat props, jangan tergoda menambah satu import store "sementara saja", karena itu awal dari komponen jadi susah dipakai ulang lagi.
Aturan lain yang membantu: komponen presentasi sebaiknya tidak tahu bentuk endpoint atau struktur response API secara langsung — biarkan container yang menerjemahkan data mentah jadi bentuk yang props-nya sudah rapi. Ini bikin, misalnya kalau backend berubah format response, saya cukup ubah satu container, bukan menelusuri semua komponen yang memakai data itu.
Takeaway
Intinya bukan soal jumlah file atau seberapa "murni" arsitekturnya, tapi soal di mana batas tanggung jawab itu ditaruh secara sengaja. Komponen presentasi yang benar-benar bodoh gampang ditest, gampang dipakai ulang, gampang dibaca. Komponen container yang jelas cakupannya bikin perubahan sumber data tidak merembet ke seluruh aplikasi. Kalau ragu di tengah menulis komponen baru, pertanyaan sederhana yang biasa saya pakai: "kalau saya copy komponen ini ke proyek lain tanpa store dan API yang sama, apakah dia masih jalan kalau saya kasih props manual?" Kalau jawabannya tidak, itu tanda sudah waktunya dipecah.