Svelte bukan framework yang menyuntikkan runtime ke browser. Ia compiler — kamu menulis komponen, Svelte mengubahnya jadi JavaScript vanilla tanpa virtual DOM, dan hasilnya bundle kecil dengan pembaruan UI yang hemat. Versi 5 memaksa saya menulis ulang kebiasaan lama, karena sistem reactivity-nya diganti total. Kalau variabel dan fungsi JavaScript masih terasa asing, siapkan dulu di panduan dasar JavaScript sebelum lanjut.
Apa yang berubah di Svelte 5
Svelte 4 dan sebelumnya mengandalkan sintaks $: untuk kode reaktif. Sintaks itu cuma bekerja di dalam komponen, dan aturannya — kapan sebuah statement dianggap reaktif — sering bikin bingung. Svelte 5 membuangnya dan mengganti dengan runes: fungsi berawalan $ seperti $state, $derived, dan $effect. Runes bersifat eksplisit dan bisa dipakai di mana saja, termasuk di file .js biasa.
Siapkan proyek
Kamu butuh Node.js 18 ke atas. Cek versinya dulu, lalu scaffold proyek memakai template Svelte dari Vite.
node -v
npm create vite@latest svelte-todo -- --template svelte
cd svelte-todo
npm install
npm run dev
Kalau CLI menanyakan tambahan — semacam rolldown-vite atau "install sekarang" — tekan Enter untuk memakai default. Setelah npm run dev, buka http://localhost:5173 dan kamu akan melihat halaman contoh Svelte.
Struktur project
Dua file yang akan kita sentuh ada di dalam src/. File main.js bertugas me-mount komponen root ke halaman:
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app'),
})
export default app
Mulai Svelte 5, fungsi mount menggantikan new App(...) dari versi lama. Komponen root-nya sendiri tinggal di src/App.svelte — kita akan menulis ulang isinya sebentar lagi.
Runes: $state, $derived, $effect
Tiga runes ini menutup sebagian besar kebutuhan reaktivitas sehari-hari. $state menyimpan nilai yang bisa berubah dan otomatis memicu pembaruan UI. $derived menghitung nilai dari state lain dan selalu sinkron. $effect menjalankan efek samping — logging, localStorage, panggilan API — setiap kali dependensinya berubah. Contoh kecilnya:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log(`count sekarang ${count}`);
});
</script>
<button onclick={() => count++}>
Klik saya ({count} — dobel: {doubled})
</button>
Klik tombolnya dan buka console browser. doubled tidak pernah kamu set manual — ia menghitung ulang sendiri setiap count berubah, dan $effect ikut berjalan. Satu hal yang bikin orang terbiasa React kaget: tidak ada setState. Kamu cukup menulis count++.
Bedanya $state dan variabel biasa: $state bersifat deeply reactive, jadi mengubah properti objek atau push ke array juga memicu pembaruan UI. Meski begitu, reassignment seperti todos = [...todos, item] tetap valid dan sering lebih mudah dibaca saat dependensinya kompleks.
Membangun aplikasi to-do
Sekarang ganti App.svelte dengan aplikasi to-do: input untuk menambah tugas, daftar yang bisa ditandai selesai atau dihapus, plus penyimpanan otomatis ke localStorage.
<script>
import TodoItem from './TodoItem.svelte';
function loadTodos() {
try {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
}
let todos = $state(loadTodos());
let newTodo = $state('');
let total = $derived(todos.length);
let doneCount = $derived(todos.filter((t) => t.done).length);
$effect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
});
function addTodo() {
const text = newTodo.trim();
if (!text) return;
todos = [...todos, { id: Date.now(), text, done: false }];
newTodo = '';
}
function toggle(id) {
todos = todos.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
);
}
function remove(id) {
todos = todos.filter((t) => t.id !== id);
}
</script>
<main>
<h1>To-Do Svelte 5</h1>
<form
onsubmit={(e) => {
e.preventDefault();
addTodo();
}}
>
<input bind:value={newTodo} placeholder="Tambah tugas..." />
<button type="submit">Tambah</button>
</form>
<p>{doneCount} dari {total} selesai</p>
{#if todos.length === 0}
<p>Belum ada tugas. Tambah satu dulu.</p>
{:else}
<ul>
{#each todos as todo (todo.id)}
<TodoItem
{todo}
onToggle={() => toggle(todo.id)}
onRemove={() => remove(todo.id)}
/>
{/each}
</ul>
{/if}
</main>
<style>
input {
padding: 8px;
font-size: 16px;
}
</style>
Beberapa hal yang terjadi di sini. $derived menghitung total dan doneCount dari array todos. $effect menyimpan todos ke localStorage setiap ada perubahan, sementara loadTodos() memuatnya kembali saat aplikasi dibuka. Sisanya fungsi biasa — addTodo, toggle, remove — yang tidak butuh API khusus untuk mengubah state.
Memecah ke komponen dengan $props
Daftar tugas lebih rapi kalau tiap item jadi komponen sendiri. Buat file src/TodoItem.svelte:
<script>
let { todo, onToggle, onRemove } = $props();
</script>
<li class:done={todo.done}>
<input type="checkbox" checked={todo.done} onchange={onToggle} />
<span>{todo.text}</span>
<button onclick={onRemove}>Hapus</button>
</li>
<style>
li {
display: flex;
align-items: center;
gap: 8px;
margin: 6px 0;
}
li.done span {
text-decoration: line-through;
color: #888;
}
</style>
$props() adalah cara Svelte 5 mendeklarasikan prop. Kalau dulu kamu menulis export let todo, sekarang cukup destructuring dari $props(). Perhatikan onToggle dan onRemove — itu callback biasa yang dikirim dari induk, bukan event listener. Pola ini menggantikan createEventDispatcher di Svelte 4.
Binding dan event handling
Form di App.svelte memakai dua fitur yang patut dicermati. bind:value={newTodo} menyambungkan input ke state dua arah — ketik apa pun langsung masuk ke newTodo. Handler onsubmit mencegah reload halaman lalu memanggil addTodo(). Di TodoItem, onchange dan onclick adalah event DOM biasa yang meneruskan kerja ke callback induk.
Kalau kamu datang dari React, pola $state + $derived kira-kira sepadan dengan useState + useMemo, tapi tanpa aturan dependency array. Kalau dari Vue, $state mirip ref dan $derived mirip computed. Saya sudah menulis perbandingan model komponen di tutorial React dan tutorial Vue 3 kalau kamu ingin membandingkan langsung.
Build untuk production
Selesai bereksperimen, jalankan build. Svelte akan mengompilasi komponen menjadi JavaScript statis dan mengeluarkannya ke folder dist/.
npm run build
npm run preview
npm run preview menyajikan hasil build secara lokal supaya kamu bisa memastikan semuanya jalan sebelum deploy. Folder dist/ isinya file statis biasa — tinggal di-upload ke hosting mana pun.
Apakah Svelte 5 masih mendukung sintaks lama seperti $: dan on:event?
Masih, tapi statusnya deprecated. Svelte 5 tetap menjalankan $: dan on: supaya proyek lama tidak langsung rusak, namun kamu akan melihat peringatan. Untuk kode baru, pakai runes dan callback props.
Kapan sebaiknya pakai Svelte dibanding React atau Vue?
Ketika ukuran bundle dan kecepatan update jadi prioritas — misalnya komponen interaktif kecil yang ditempel ke situs statis. Karena Svelte mengompilasi di build time, tidak ada runtime yang ikut terkirim ke browser. Untuk tim besar dengan ekosistem library yang sudah matang, React masih lebih aman.
Apakah saya perlu belajar SvelteKit sekalian?
SvelteKit adalah framework penuh (routing, SSR, data loading) di atas Svelte, mirip Next.js untuk React. Kalau targetmu aplikasi multi-halaman dengan server, ya — mulai dari SvelteKit. Fondasi runes yang kamu pelajari di sini tetap berlaku di SvelteKit.
Fondasi dari tutorial ini — runes, $props, dan event handling — berlaku sama di SvelteKit, langkah berikutnya kalau kamu butuh routing dan server. Sebelum naik, utak-atik dulu: tambah tombol filter (semua / aktif / selesai) memakai $derived, atau bikin tugas bisa diedit inline. Dua-duanya cuma menambah sedikit kode di atas apa yang sudah kamu tulis.