Svelte 5 Runes di Project Nyata

Saya sudah beberapa bulan memakai Svelte 5 Runes di project nyata. Bagian dasarnya mudah: let dengan $state, $derived, $effect, dan $props. Namun setelah kode bertambah besar, muncul persoalan yang
tidak selalu terlihat di dokumentasi atau contoh tutorial pendek.
Catatan ini bukan ulang cheat sheet migrasi. Ini kumpulan hal yang baru terasa setelah Runes dipakai untuk fitur yang benar-benar jalan di production.
$state adalah proxy, bukan sekadar variabel reaktif
Untuk primitif seperti let count = $state(0), perilakunya mudah dipahami. Tapi untuk objek dan array, $state dapat membuat nilai menjadi deeply reactive menggunakan proxy, sehingga perubahan pada properti
bersarang tetap bisa terdeteksi.
let config = $state({
theme: "light",
editor: { lineNumbers: true },
});
// Ini reaktif, cukup lewat mutasi biasa.
config.editor.lineNumbers = false; Hal ini nyaman untuk state UI. Namun ada dua implikasi yang sering terlewat.
Pertama, nilai yang dikelola $state bisa berupa proxy, sehingga ketika perlu mengirim data ke API
atau library yang mengharapkan objek biasa, lebih aman mengambil snapshot terlebih dahulu. Gunakan $state.snapshot() ketika memang membutuhkan representasi non-reactive dari state tersebut.
const plain = $state.snapshot(config);
fetch("/api/preferences", {
method: "POST",
body: JSON.stringify(plain),
}); Kedua, deep reactivity tidak selalu diperlukan. Untuk koleksi besar yang biasanya diganti secara
utuh dan tidak perlu dimutasi secara mendalam, $state.raw bisa menjadi pilihan yang lebih sesuai.
Nilainya tidak dibuat menjadi deeply reactive, sehingga perubahan harus dilakukan dengan reassignment.
let rows = $state.raw<Row[]>([]);
// Mutasi ini TIDAK membuat perubahan reactive:
// rows.push(row);
// Gunakan reassign.
rows = [...rows, row]; Jadi jangan menganggap $state.raw selalu “lebih murah” atau selalu lebih cepat. Pilih berdasarkan
bentuk data dan cara data tersebut dimutasi. $state cocok ketika deep reactivity memang berguna,
sedangkan $state.raw cocok ketika kamu ingin menyimpan nilai tanpa deep proxy dan siap melakukan
reassignment ketika nilainya berubah.
$derived.by untuk branching yang tidak muat di ekspresi
$derived bagus untuk ekspresi yang sederhana. Begitu logika mulai punya if, beberapa return, atau
loop, menulis semuanya sebagai ternary justru menyulitkan dibaca.
const status = $derived.by(() => {
if (loading) return "loading";
if (error) return "error";
if (!items.length) return "empty";
return "ready";
}); $derived.by menerima fungsi penuh dan tetap melacak dependency yang dibaca di dalamnya.
Saya memakainya untuk derived state yang membutuhkan sedikit lebih banyak logika, misalnya merangkum filter aktif, menghitung statistik, atau menghasilkan ringkasan data tanpa membuat state tambahan.
Prinsipnya tetap sama: kalau sebuah nilai bisa dihitung dari state lain, lebih baik menjadikannya $derived daripada membuat state baru lalu menyinkronkannya dengan $effect.
Hati-hati dengan nilai objek atau array baru di dalam $derived
Salah satu hal yang mudah luput adalah bahwa sebuah $derived bisa menghasilkan object atau array baru
ketika dependency-nya berubah.
let users = $state([...]);
const activeIds = $derived(
users.filter((u) => u.active).map((u) => u.id)
);
$effect(() => {
syncSelection(activeIds);
}); Ketika users berubah dan derived expression dihitung ulang, expression tersebut menghasilkan array
baru. Akibatnya, kode yang bergantung pada nilai derived tersebut dapat ikut dijalankan ketika nilai
derived dianggap berubah.
Ini bukan berarti setiap $derived yang menghasilkan array baru adalah masalah. Untuk operasi UI biasa,
pola seperti ini sangat normal.
Masalah baru terasa ketika derived value dipakai oleh pekerjaan yang mahal, misalnya sinkronisasi dengan library eksternal, validasi besar, atau side effect lainnya.
Karena itu, jangan hanya melihat biaya menghitung $derived. Perhatikan juga siapa yang membaca hasilnya dan apa yang dilakukan ketika hasil tersebut berubah.
Jika sebuah nilai hanya dibutuhkan untuk rendering, biasanya tidak ada alasan untuk mengoptimalkannya secara berlebihan.
untrack untuk menghindari dependency yang tidak disengaja
Salah satu konsep penting $effect adalah bahwa dependency ditentukan dari state yang dibaca selama
effect berjalan. Jadi, sekadar menulis state di dalam $effect tidak otomatis membuat state tersebut
menjadi dependency.
Contoh berikut:
let query = $state("");
let lastSync = $state<Date | null>(null);
$effect(() => {
runSearch(query);
// Menulis lastSync tidak membuat lastSync
// menjadi dependency effect.
lastSync = new Date();
}); Effect di atas bergantung pada query, karena query dibaca. lastSync hanya ditulis, sehingga tidak
menyebabkan effect subscribe ke dirinya sendiri.
Lalu kapan untrack berguna?
Misalnya sebuah effect perlu membaca state tambahan, tetapi perubahan state tersebut tidak boleh menyebabkan effect berjalan ulang:
let query = $state("");
let preferences = $state({
language: "id",
});
$effect(() => {
runSearch(query);
const language = untrack(() => preferences.language);
logSearch(query, language);
}); Tanpa untrack, pembacaan preferences.language menjadi dependency effect. Akibatnya perubahan
bahasa dapat menyebabkan effect berjalan lagi.
Dengan untrack, nilai tersebut tetap bisa dibaca, tetapi pembacaan itu tidak dicatat sebagai
dependency untuk effect tersebut.
Jadi, gunakan untrack bukan sebagai “obat” untuk setiap $effect, tetapi ketika memang ada pembacaan
reactive state yang ingin diperlakukan sebagai snapshot pada saat effect berjalan.
$inspect untuk melihat reaktivitas tanpa console.log
Saat bingung kenapa sebuah nilai berubah atau effect berjalan, console.log biasa sering kurang
nyaman untuk debugging reactive state.
$inspect dirancang khusus untuk membantu melihat nilai ketika terjadi perubahan dan dapat memberikan
informasi tambahan tentang asal perubahan tersebut.
<script>
let { count } = $props();
$inspect(count);
</script> Output-nya muncul di dev console ketika nilai berubah. $inspect hanya aktif selama development dan
menjadi no-op pada production build, sehingga tidak perlu menghapusnya satu per satu sebelum build
production.
Untuk debugging reaktivitas yang kompleks, ini jauh lebih berguna daripada menaruh console.log di
berbagai tempat dan mencoba menebak dependency mana yang menyebabkan perubahan.
Pindahkan state non-UI ke modul .svelte.ts
Runes tidak terbatas di dalam komponen. File dengan ekstensi .svelte.ts bisa digunakan untuk
menyimpan reactive state dan reusable logic yang dipakai lintas komponen.
// counter.svelte.ts
export function createCounter() {
let count = $state(0);
return {
get count() {
return count;
},
increment() {
count += 1;
},
};
} Pola getter di atas membuat consumer bisa membaca count, tetapi tidak mendapatkan setter langsung
untuk mengubahnya. Mutasi tetap dilakukan melalui API yang kita sediakan.
Ini berguna ketika ingin membuat stateful logic yang reusable tanpa harus membuat store dengan API yang lebih kompleks.
Yang perlu diingat: rune seperti $state diproses oleh compiler Svelte. Karena itu, untuk reactive
logic semacam ini gunakan file .svelte.ts atau .svelte.js, bukan modul .ts biasa.
Selain itu, perhatikan lifecycle state tersebut. State yang dibuat pada module scope dapat menjadi
shared state, sedangkan state yang dibuat di dalam createCounter() akan menjadi instance state untuk
setiap pemanggilan fungsi.
Kesimpulan
Dokumentasi Svelte 5 sudah cukup lengkap soal apa yang dilakukan tiap rune. Yang tidak selalu terlihat adalah kapan memilih satu di atas yang lain, dan bagaimana dependency reaktivitas berperilaku saat state mulai membesar.
Empat kebiasaan yang paling banyak menyelamatkan saya:
$state.rawketika deep reactivity tidak diperlukan dan data lebih cocok diganti dengan reassignment; gunakan$stateketika mutasi nested memang menjadi bagian dari pola UI.$derived.byketika logika turunan membutuhkan branching atau beberapa langkah perhitungan.untrackketika sebuah$effectperlu membaca reactive state tetapi pembacaan tersebut tidak boleh menjadi dependency effect.$inspectdan.svelte.tsuntuk membuat alur reaktivitas lebih mudah diamati dan logic stateful lebih mudah digunakan kembali.
Pada akhirnya, Runes bukan sekadar pengganti sintaks Svelte lama. Nilai sebenarnya baru terasa ketika kita mulai memikirkan dependency mana yang ingin dilacak, dependency mana yang tidak, dan seberapa dalam sebuah data perlu dibuat reactive.