Runes adalah kata kunci Svelte 5 yang diawali $ dan dipanggil seperti function, misalnya $state, $derived, $effect, $props, dan $bindable. Dengan runes, kamu menandai sendiri mana yang reaktif, alih-alih Svelte menebak dari let dan $:. Bayangin papan stok di dapur Warung Bu Sri. Dulu, pegawai cuma memperbarui papan kalau ada yang menulis ulang angka di buku dengan tanda sama dengan; kalau ada yang diam-diam menambah isi toples, papannya diam. Sekarang Bu Sri menempel stiker di toples yang harus dipantau. Toples berstiker diawasi terus, termasuk saat isinya ditambah, angka di papan yang dihitung dari toples itu ikut berubah sendiri, dan bel dapur berbunyi tiap ada perubahan. Stiker itu $state, angka di papan $derived, dan bel dapur $effect.
🧪 Tebakan cepat: di pelajaran Proyek,
belanja.push(barang)gak memperbarui layar di Svelte 4. Sekarang ditulislet keranjang = $state([]), lalu tombolnya menjalankankeranjang.push("Es Teh"). ApakahKeranjang: 1 menumuncul? Jawabannya ada di bagian "Deep reactivity: push sekarang jalan".
$state dan $derived
<script>
let count = $state(0)
let double = $derived(count * 2)
</script>
<p class="count">Count: {count}</p>
<p class="double">Double: {double}</p>
<button onclick={() => count += 1}>+1</button>
$state(0)membuat state dengan nilai awal0. Membacanya dan mengubahnya tetap seperti variabel biasa:count,count += 1.$derived(count * 2)adalah nilai turunan, pengganti$: double = count * 2. Svelte menghitungnya ulang begitucountberubah. Untuk hitungan beberapa baris, pakai$derived.by(() => { ... return hasil }).onclick={...}adalah atribut event biasa, penggantion:click. Svelte 5 gak punya modifier seperti|preventDefault; panggile.preventDefault()sendiri di handler-nya.
Satu klik di compiler Svelte 5.16: Count: 0 dan Double: 0 jadi Count: 1 dan Double: 2.
Deep reactivity: push sekarang jalan
<script>
let keranjang = $state([])
function tambah() { keranjang.push("Es Teh") }
</script>
<p>Keranjang: {keranjang.length} menu</p>
<button onclick={tambah}>Tambah</button>
| Klik | Svelte 4 (let keranjang = [], on:click) |
Svelte 5 ($state([])) |
|---|---|---|
| awal | Keranjang: 0 menu |
Keranjang: 0 menu |
| 1 | Keranjang: 0 menu |
Keranjang: 1 menu |
| 2 | Keranjang: 0 menu |
Keranjang: 2 menu |
Ya, Keranjang: 1 menu muncul. Itu jawaban tebakannya. $state dengan array atau objek membungkusnya dalam proxy, yaitu objek perantara yang mencatat tiap kali isinya dibaca atau diubah, sampai ke dalam-dalamnya. Jadi push, keranjang[0].porsi += 1, dan splice semua memperbarui layar. Aturan Svelte 4, "cuma assignment yang memperbarui layar", gak berlaku lagi untuk $state.
Akibatnya, console.log(keranjang) mencetak proxy, bukan array biasa, dan mode dev memberi warning: Your `console.log` contained `$state` proxies. Consider using `$inspect(...)` or `$state.snapshot(...)` instead. console.log($state.snapshot(keranjang)) mencetak [ 'Es Teh' ].
$effect dan cleanup
$effect menjalankan kode samping (log, timer, document.title, localStorage) setelah DOM diperbarui, dan jalan lagi tiap kali state yang dibacanya berubah:
<script>
let count = $state(0)
$effect(() => {
const n = count
console.log("effect jalan, count =", n)
return () => console.log("cleanup, count tadi =", n)
})
</script>
<button onclick={() => count += 1}>+1</button>
Console-nya, dari dipasang, satu klik, lalu komponennya dilepas:
effect jalan, count = 0
cleanup, count tadi = 0
effect jalan, count = 1
cleanup, count tadi = 1
- Function yang di-return adalah cleanup: dia jalan sebelum effect berikutnya dan saat komponen dilepas. Di situ timer dihentikan (
clearInterval) dan listener dilepas, supaya gak menumpuk. const n = countmenyalin nilainya dulu. Kalau cleanup membacacountlangsung, dia mendapat nilai yang baru: dicoba, cleanup setelah klik pertama mencetak1, bukan0.- Untuk menghitung nilai, pakai
$derived, bukan$effectyang mengisi variabel lain.
$props, $bindable, dan event sebagai prop
<!-- MenuItem.svelte -->
<script>
let { nama, harga = 0, onpesan } = $props()
</script>
<p>{nama}: Rp {harga}</p>
<button onclick={() => onpesan?.({ nama, harga })}>Pesan</button>
<!-- InputPorsi.svelte -->
<script>
let { value = $bindable(1) } = $props()
</script>
<input type="number" bind:value />
Induknya menulis <MenuItem nama="Mie Ayam" harga={12000} onpesan={(m) => pesanan.push(m)} /> dan <InputPorsi bind:value={porsi} />.
$props()mengganti semuaexport let: destructuring dengan nilai bawaan,harga = 0.- Event komponen jadi prop berisi function,
onpesan, pengganticreateEventDispatcherdanon:pesan.?.membuatnya aman kalau induk gak mengirimnya. bind:valuedari induk cuma boleh ke prop yang ditandai$bindable. Tanpa tanda itu, Svelte berhenti dengan errorbind_not_bindable:A component is attempting to bind to a non-bindable property `value`.
Snippet dan {@render}: pengganti slot
<!-- Kartu.svelte -->
<script>
let { judul, children } = $props()
</script>
<div class="kartu">
<h3>{@render judul()}</h3>
{@render children()}
</div>
<Kartu>
{#snippet judul()}Mie Ayam{/snippet}
<p>Rp 12000</p>
</Kartu>
Hasilnya <div class="kartu"><h3>Mie Ayam</h3> <p>Rp 12000</p></div>. {#snippet judul()} mengganti slot bernama, dan isi lain di antara tag komponen otomatis jadi prop children, pengganti <slot />.
Peta Svelte 4 ke Svelte 5
| Jalur ini (Svelte 4) | Svelte 5 dengan runes |
|---|---|
let count = 0 |
let count = $state(0) |
$: double = count * 2 |
let double = $derived(count * 2) |
$: console.log(count) |
$effect(() => console.log(count)) |
export let nama |
let { nama } = $props() |
export let value, lalu bind:value dari induk |
let { value = $bindable() } = $props() |
on:click={f} |
onclick={f} |
on:submit dengan modifier preventDefault |
onsubmit={(e) => { e.preventDefault(); f() }} |
createEventDispatcher, on:pesan |
prop function onpesan |
<slot />, <slot name="judul" /> |
{@render children()}, {#snippet judul()} dan {@render judul()} |
<svelte:component this={X} /> |
<X /> |
beforeUpdate, afterUpdate |
$effect.pre, $effect |
writable dan $keranjang |
tetap jalan, atau $state di file .svelte.js |
$app/stores |
$app/state (SvelteKit) |
Yang gak berubah: {#if}, {#each}, {#await}, bind:, class:, use:, transition:, animate:, onMount, setContext dan getContext, serta <svelte:window> dan <svelte:head>.
Rune di file .svelte.js
Runes juga jalan di luar komponen, di file berakhiran .svelte.js. Keranjang bisa jadi state bersama tanpa writable():
// src/lib/keranjang.svelte.js
export const keranjang = $state([])
export function tambah(nama) {
keranjang.push({ nama, porsi: 1 })
}
Komponen mana pun yang mengimpornya dan membaca keranjang.length ikut diperbarui. Akhiran .svelte.js wajib, karena cuma file itu yang lewat compiler Svelte. State yang diekspor juga gak boleh diisi ulang: export let keranjang = $state([]) lalu keranjang = [] ditolak dengan Cannot export state from a module if it is reassigned. Ubah isinya, misalnya keranjang.length = 0.
Latihan: counter dengan runes
Editor latihan memakai Svelte 5 begitu kodemu memakai rune seperti $state(. Tulis let count = $state(0) dan let double = $derived(count * 2), lalu <p class="count">Count: {count}</p>, <p class="double">Double: {double}</p>, dan <button onclick={() => count += 1}>+1</button>. Sebelum diklik, halamannya Count: 0 dan Double: 0; setelah satu klik, Count: 1 dan Double: 2.
⚠️ Jebakan yang sering ketemu
- Sintaks Svelte 4 di komponen runes. Begitu ada rune,
$:ditolak (`$:` is not allowed in runes mode, use `$derived` or `$effect` instead),export letditolak, dan mencampuron:clickdenganonclickditolak (Mixing old (on:click) and new syntaxes for event handling is not allowed). - Membaca state di dalam cleanup. Nilainya sudah yang baru; salin dulu ke
const. $effectuntuk menghitung nilai. Pakai$derived. Effect yang mengisi state lain baru jalan setelah DOM diperbarui, jadi satu perubahan menggambar ulang dua kali.bind:valueke prop tanpa$bindable. Errorbind_not_bindable.
TL;DR: Runes menandai reaktivitas secara eksplisit. let count = $state(0) menggantikan let count = 0, let double = $derived(count * 2) menggantikan $: double = count * 2, dan $effect(() => {...}) menggantikan $: untuk efek samping, dengan function yang di-return sebagai cleanup sebelum run berikutnya dan saat komponen dilepas. $state dengan array atau objek adalah proxy yang reaktif sampai ke dalam, jadi keranjang.push(...) memperbarui layar, berbeda dari aturan assignment Svelte 4; cetak isinya dengan $state.snapshot. let { nama, onpesan } = $props() menggantikan export let dan createEventDispatcher, $bindable() mengizinkan bind: dari induk, onclick menggantikan on:click, dan {#snippet} dengan {@render} menggantikan slot. Runes juga jalan di file .svelte.js, jadi state bersama gak perlu writable(). Latihannya: $state(0), $derived(count * 2), <p class="count">, <p class="double">, dan tombol +1 dengan onclick, dari Count: 0, Double: 0 jadi Count: 1, Double: 2.