Skip to content
← Back to blog

Svelte 5 Runes di Project Nyata

6 min read
SvelteRunesSvelteKitProduction
Ilustrasi developer menulis kode Svelte 5 Runes di laptop, dengan diagram reaktivitas di layar
Svelte 5 Runes mempermudah reaktivitas, tetapi ada beberapa hal yang perlu diperhatikan ketika dipakai 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.raw ketika deep reactivity tidak diperlukan dan data lebih cocok diganti dengan reassignment; gunakan $state ketika mutasi nested memang menjadi bagian dari pola UI.
  • $derived.by ketika logika turunan membutuhkan branching atau beberapa langkah perhitungan.
  • untrack ketika sebuah $effect perlu membaca reactive state tetapi pembacaan tersebut tidak boleh menjadi dependency effect.
  • $inspect dan .svelte.ts untuk 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.

Related articles