Web Development

Svelte: Framework Reaktif untuk Web Modern

Tutorial lengkap belajar Svelte dari nol — components, reactivity, stores, transitions, actions, lifecycle, dan SvelteKit dengan contoh kode praktis

1. Pengenalan Svelte

Svelte adalah framework JavaScript compiler-based yang mengambil pendekatan berbeda dari React atau Vue. Alih-alih melakukan pekerjaan di browser (runtime), Svelte mengubah kode Anda menjadi vanilla JavaScript yang sangat optimal saat build time. Hasilnya? Bundle yang lebih kecil dan performa yang lebih cepat — tanpa virtual DOM.

Svelte dibuat oleh Rich Harris pada tahun 2016 dan sejak itu tumbuh menjadi salah satu framework yang paling dicintai oleh developer. Dalam survei State of JS, Svelte secara konsisten menduduki peringkat tertinggi untuk kepuasan pengembang.

Mengapa Svelte Berbeda?

Keunggulan Penjelasan
Compile-timeTidak ada runtime framework — kode langsung jadi vanilla JS yang optimal
Tanpa Virtual DOMUpdate DOM langsung tanpa diffing, lebih cepat dari React/Vue
Bundle KecilAplikasi Svelte biasanya 10-30x lebih kecil dari React
Sintaks SederhanaMenulis lebih sedikit kode, lebih dekat ke HTML/CSS/JS standar
Reactivity Built-inTidak perlu hooks atau decorator — reaktivitas otomatis
Built-in FeaturesTransitions, animations, dan scoped styles tanpa library tambahan

Svelte vs React vs Vue

Aspek Svelte React Vue
PendekatanCompile-timeRuntime (Virtual DOM)Runtime (Proxy + VDOM)
Bundle Size~2 KB (gzip)~42 KB (gzip)~33 KB (gzip)
Syntax.svelte (HTML-based)JSX.vue (template)
StateReactive variablesuseState/useReducerref/reactive
Learning Curve🟢 Mudah🟡 Sedang🟢 Mudah
Full-stackSvelteKitNext.jsNuxt.js
Diagram: Cara Kerja Svelte (Compile-time)
┌───────────────────────────────────────────────────────┐
│              SVELTE COMPILE PROCESS                   │
│                                                       │
│  SOURCE CODE (.svelte)                                │
│  ┌─────────────────────────────────────────────────┐  │
│  │ <script>                                        │  │
│  │   let count = 0;                                │  │
│  │   function increment() { count += 1; }          │  │
│  │ </script>                                       │  │
│  │                                                 │  │
│  │ <button on:click={increment}>                   │  │
│  │   Klik: {count} kali                            │  │
│  │ </button>                                       │  │
│  │                                                 │  │
│  │ <style>                                         │  │
│  │   button { background: #6366f1; color: white; } │  │
│  │ </style>                                        │  │
│  └─────────────────────────────────────────────────┘  │
│                     │                                 │
│                     ▼                                 │
│  ┌─────────────────────────────────────────────────┐  │
│  │            Svelte Compiler                      │  │
│  │   • Parse template → AST                        │  │
│  │   • Analyze reactivity                          │  │
│  │   • Generate vanilla JS                         │  │
│  │   • Extract & scope CSS                         │  │
│  └─────────────────────────────────────────────────┘  │
│                     │                                 │
│                     ▼                                 │
│  COMPILED OUTPUT (vanilla JS)                         │
│  ┌─────────────────────────────────────────────────┐  │
│  │ // Tanpa runtime framework!                     │  │
│  │ const button = element("button");               │  │
│  │ let count = 0;                                  │  │
│  │ button.$$on("click", () => {                    │  │
│  │   count += 1;                                   │  │
│  │   set_text(button, `Klik: ${count} kali`);      │  │
│  │ });                                             │  │
│  └─────────────────────────────────────────────────┘  │
│                                                       │
│  Bundle: ~2-5 KB vs React ~42 KB                      │
└───────────────────────────────────────────────────────┘

2. Setup Proyek Svelte

Svelte dapat di-setup dengan sangat cepat menggunakan Vite dan SvelteKit (framework full-stack untuk Svelte). Untuk belajar dasar, kita bisa mulai dengan proyek Svelte standalone.

Bash — Setup Proyek
# Opsi 1: Svelte standalone (belajar dasar)
npm create vite@latest belajar-svelte -- --template svelte

# Opsi 2: SvelteKit (full-stack, production-ready)
npm create svelte@latest belajar-sveltekit

# Masuk ke direktori
cd belajar-svelte

# Instal dependencies
npm install

# Jalankan development server
npm run dev

# Output:
#   VITE v5.x  ready in 200 ms
#   ➜  Local:   http://localhost:5173/

# Build untuk production
npm run build

# Preview build
npm run preview

Struktur Proyek Svelte

File Structure
belajar-svelte/
├── node_modules/
├── public/
│   └── favicon.png
├── src/
│   ├── lib/                  ← Komponen & utility
│   │   ├── Counter.svelte    ← Komponen contoh
│   │   └── stores.js         ← Svelte stores
│   ├── routes/               ← (SvelteKit) routing
│   │   ├── +page.svelte      ← Halaman utama
│   │   ├── +layout.svelte    ← Layout wrapper
│   │   └── about/
│   │       └── +page.svelte  ← /about
│   ├── App.svelte            ← Root component (standalone)
│   ├── app.css               ← Global styles
│   └── main.js               ← Entry point
├── index.html
├── package.json
├── svelte.config.js          ← Svelte config
└── vite.config.js            ← Vite config
💡 SvelteKit vs Svelte Standalone

Svelte adalah compiler/UI framework. SvelteKit adalah framework full-stack yang dibangun di atas Svelte — menyediakan routing, SSR, API endpoints, dan banyak lagi. Untuk produksi, gunakan SvelteKit. Untuk belajar dasar-dasar Svelte, standalone sudah cukup.

3. Komponen Svelte

Setiap file .svelte adalah satu komponen. Komponen Svelte terdiri dari tiga bagian opsional: <script> (logika), markup (template HTML), dan <style> (CSS yang di-scope otomatis).

Struktur Komponen

Svelte — Component Structure
<!-- Greeting.svelte -- Komponen Svelte pertama -->

<!-- 1. SCRIPT: Logika JavaScript -->
<script>
  // Variabel yang dideklarasi di sini langsung reaktif
  let nama = 'Budi';
  let waktu = new Date().toLocaleTimeString('id-ID');

  // Fungsi biasa
  function gantiNama() {
    const namaBaru = prompt('Masukkan nama:');
    if (namaBaru) nama = namaBaru;
  }

  // Update waktu setiap detik
  setInterval(() => {
    waktu = new Date().toLocaleTimeString('id-ID');
  }, 1000);
</script>

<!-- 2. MARKUP: Template HTML dengan Svelte syntax -->
<div class="greeting">
  <h1>Halo, {nama}! 👋</h1>
  <p>Waktu sekarang: {waktu}</p>
  <button on:click={gantiNama}>
    Ganti Nama
  </button>
</div>

<!-- 3. STYLE: CSS yang otomatis di-scope ke komponen ini -->
<style>
  /* Style ini HANYA berlaku untuk komponen ini */
  /* Tidak perlu BEM, CSS Modules, atau styled-components */
  .greeting {
    padding: 24px;
    background: #1e1e2e;
    border-radius: 12px;
    text-align: center;
  }

  h1 {
    color: #cdd6f4;
    font-size: 28px;
  }

  button {
    margin-top: 12px;
    padding: 10px 20px;
    background: #6366f1;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
  }
</style>

<!-- 
  Svelte compiler mengubah class names menjadi unique identifiers.
  Contoh: .greeting → .greeting.svelte-abc123
  Sehingga style TIDAK bocor ke komponen lain!
-->

Komposisi Komponen

Svelte — Component Composition
<!-- Badge.svelte -- Komponen kecil -->
<script>
  export let text;
  export let color = '#6366f1';
</script>

<span class="badge" style="background: {color}">
  {text}
</span>

<style>
  .badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 12px;
    font-size: 12px;
    color: white;
  }
</style>

<!-- UserCard.svelte -- Komponen yang menggunakan Badge -->
<script>
  import Badge from './Badge.svelte';

  export let nama;
  export let role = 'Member';
  export let avatar = `https://ui-avatars.com/api/?name=${encodeURIComponent(nama)}`;
</script>

<div class="card">
  <img src={avatar} alt={nama} />
  <div class="info">
    <h3>{nama}</h3>
    <Badge text={role} />
  </div>
</div>

<style>
  .card {
    background: #1e1e2e;
    border: 1px solid #313244;
    border-radius: 12px;
    padding: 20px;
    display: flex;
    gap: 16px;
    align-items: center;
  }
  img {
    width: 56px;
    height: 56px;
    border-radius: 50%;
  }
  h3 { margin: 0 0 8px; color: #cdd6f4; }
</style>

<!-- App.svelte -- Menggunakan UserCard -->
<script>
  import UserCard from './UserCard.svelte';
</script>

<div class="grid">
  <UserCard nama="Budi Santoso" role="Admin" />
  <UserCard nama="Sari Dewi" role="Developer" />
  <UserCard nama="Andi Pratama" role="Designer" />
</div>

4. Reactivity & State

Fitur paling menarik dari Svelte adalah reaktivitas built-in. Di Svelte, cukup dengan mengubah nilai variabel, UI akan otomatis diperbarui. Tidak perlu hooks, tidak perlu setState — compiler Svelte yang menangani semuanya.

Reactive Variables

Svelte — Reactivity
<script>
  // ====== Reactive variables: cukup deklarasi dan ubah ======
  let count = 0;

  function increment() {
    count += 1; // UI otomatis update!
  }

  function decrement() {
    count -= 1;
  }

  function reset() {
    count = 0;
  }

  // ====== Reactive declarations: $: ======
  // Dijalankan ulang setiap kali dependensi berubah
  $: doubled = count * 2;
  $: tripled = count * 3;
  $: isPositive = count > 0;
  $: isNegative = count < 0;

  // Reactive statement dengan block
  $: {
    console.log(`Count berubah menjadi: ${count}`);
    console.log(`Doubled: ${doubled}, Tripled: ${tripled}`);
  }

  // Reactive if
  $: if (count > 10) {
    console.log('Count sudah lebih dari 10!');
  }

  // ====== Derived values ======
  $: color = count > 0 ? '#a6e3a1' : count < 0 ? '#f38ba8' : '#cdd6f4';
  $: label = count === 0 ? 'Nol' : count > 0 ? 'Positif' : 'Negatif';
</script>

<div class="counter" style="border-color: {color}">
  <h2 style="color: {color}">{count}</h2>
  <p>Doubled: {doubled} | Tripled: {tripled} | Status: {label}</p>

  <div class="buttons">
    <button on:click={decrement}>− Kurangi</button>
    <button on:click={reset}>↺ Reset</button>
    <button on:click={increment}>+ Tambah</button>
  </div>
</div>

<style>
  .counter {
    text-align: center;
    padding: 32px;
    background: #1e1e2e;
    border: 3px solid #313244;
    border-radius: 16px;
    transition: border-color 0.3s;
  }
  h2 { font-size: 64px; margin: 0; }
  p { color: #a6adc8; margin: 8px 0 24px; }
  .buttons { display: flex; gap: 8px; justify-content: center; }
  button {
    padding: 10px 20px;
    border: 2px solid #45475a;
    background: transparent;
    color: #cdd6f4;
    border-radius: 8px;
    cursor: pointer;
    font-size: 16px;
  }
</style>

Reactive dengan Array & Object

Svelte — Reactive Arrays & Objects
<script>
  // ====== Array reactivity ======
  // Di Svelte, reassignment = reaktif
  let items = [
    { id: 1, nama: 'Roti', harga: 15000 },
    { id: 2, nama: 'Susu', harga: 12000 },
    { id: 3, nama: 'Telur', harga: 25000 },
  ];

  let namaBaru = '';
  let hargaBaru = 0;

  function tambahItem() {
    if (!namaBaru.trim()) return;
    // Push langsung reaktif di Svelte!
    items = [...items, {
      id: Date.now(),
      nama: namaBaru,
      harga: Number(hargaBaru)
    }];
    namaBaru = '';
    hargaBaru = 0;
  }

  function hapusItem(id) {
    // Filter membuat array baru → reaktif!
    items = items.filter(item => item.id !== id);
  }

  // ====== Reactive derived ======
  $: totalHarga = items.reduce((sum, item) => sum + item.harga, 0);
  $: jumlahItem = items.length;
  $: rataRata = jumlahItem > 0 ? totalHarga / jumlahItem : 0;

  // Format rupiah
  function formatRupiah(angka) {
    return new Intl.NumberFormat('id-ID', {
      style: 'currency',
      currency: 'IDR',
      minimumFractionDigits: 0
    }).format(angka);
  }
</script>

<div class="belanja">
  <h2>🛒 Keranjang Belanja</h2>

  <!-- Input form -->
  <form on:submit|preventDefault={tambahItem}>
    <input bind:value={namaBaru} placeholder="Nama barang" />
    <input bind:value={hargaBaru} type="number" placeholder="Harga" />
    <button type="submit">Tambah</button>
  </form>

  <!-- Daftar items -->
  {#if items.length > 0}
    <table>
      <thead>
        <tr><th>No</th><th>Nama</th><th>Harga</th><th>Aksi</th></tr>
      </thead>
      <tbody>
        {#each items as item, i (item.id)}
          <tr>
            <td>{i + 1}</td>
            <td>{item.nama}</td>
            <td>{formatRupiah(item.harga)}</td>
            <td>
              <button class="hapus" on:click={() => hapusItem(item.id)}>
                🗑️
              </button>
            </td>
          </tr>
        {/each}
      </tbody>
    </table>

    <div class="summary">
      <p>Total: <strong>{formatRupiah(totalHarga)}</strong></p>
      <p>Jumlah: {jumlahItem} item | Rata-rata: {formatRupiah(rataRata)}</p>
    </div>
  {:else}
    <p class="empty">Keranjang kosong</p>
  {/if}
</div>

5. Props & Events

Di Svelte, props dideklarasikan dengan export let dan events dikirim menggunakan createEventDispatcher atau on: event forwarding.

Svelte — Props & Events
<!-- ====== TodoItem.svelte — Komponen dengan props & events ====== -->
<script>
  import { createEventDispatcher } from 'svelte';

  // Props dengan nilai default
  export let todo;
  export let showDate = false;

  // Event dispatcher
  const dispatch = createEventDispatcher();

  function toggleComplete() {
    dispatch('toggle', { id: todo.id });
  }

  function remove() {
    dispatch('remove', { id: todo.id });
  }
</script>

<div class="todo-item" class:completed={todo.selesai}>
  <input
    type="checkbox"
    checked={todo.selesai}
    on:change={toggleComplete}
  />
  <span class="text">{todo.teks}</span>
  {#if showDate}
    <span class="date">{todo.tanggal}</span>
  {/if}
  <button class="delete" on:click={remove}>✕</button>
</div>

<style>
  .todo-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    background: #1e1e2e;
    border: 1px solid #313244;
    border-radius: 8px;
    transition: opacity 0.3s;
  }
  .completed { opacity: 0.5; }
  .completed .text { text-decoration: line-through; }
  .text { flex: 1; color: #cdd6f4; }
  .date { font-size: 12px; color: #6c7086; }
  .delete {
    background: none;
    border: none;
    color: #f38ba8;
    cursor: pointer;
    font-size: 16px;
  }
</style>

<!-- ====== TodoApp.svelte — Komponen induk ====== -->
<script>
  import TodoItem from './TodoItem.svelte';

  let todos = [
    { id: 1, teks: 'Belajar Svelte', selesai: false, tanggal: '26/06/2026' },
    { id: 2, teks: 'Buat proyek', selesai: false, tanggal: '26/06/2026' },
    { id: 3, teks: 'Baca dokumentasi', selesai: true, tanggal: '25/06/2026' },
  ];

  let teksBaru = '';

  function tambahTodo() {
    if (!teksBaru.trim()) return;
    todos = [...todos, {
      id: Date.now(),
      teks: teksBaru,
      selesai: false,
      tanggal: new Date().toLocaleDateString('id-ID'),
    }];
    teksBaru = '';
  }

  // Handler untuk event dari TodoItem
  function handleToggle(e) {
    todos = todos.map(t =>
      t.id === e.detail.id ? { ...t, selesai: !t.selesai } : t
    );
  }

  function handleRemove(e) {
    todos = todos.filter(t => t.id !== e.detail.id);
  }

  $: sisaTugas = todos.filter(t => !t.selesai).length;
</script>

<div class="todo-app">
  <h2>📝 Daftar Tugas ({sisaTugas} belum selesai)</h2>

  <form on:submit|preventDefault={tambahTodo}>
    <input bind:value={teksBaru} placeholder="Tambah tugas baru..." />
    <button type="submit">Tambah</button>
  </form>

  <div class="todo-list">
    {#each todos as todo (todo.id)}
      <TodoItem
        {todo}
        showDate={true}
        on:toggle={handleToggle}
        on:remove={handleRemove}
      />
    {/each}
  </div>
</div>

6. Svelte Stores

Svelte Stores menyediakan cara untuk menyimpan data yang bisa diakses dari banyak komponen tanpa prop drilling. Svelte menyediakan tiga jenis store: writable, readable, dan derived.

JavaScript — Svelte Stores
// ====== stores.js — Definisikan stores ======
import { writable, readable, derived } from 'svelte/store';

// ====== 1. WRITABLE STORE — bisa dibaca dan ditulis ======
export const count = writable(0);

export const user = writable({
  nama: '',
  email: '',
  isLoggedIn: false,
});

export const cartItems = writable([]);

export const theme = writable('dark'); // 'dark' atau 'light'

// Writable store dengan custom logic
function createTodoStore() {
  const { subscribe, set, update } = writable([]);

  return {
    subscribe,
    tambah: (teks) => update(items => [
      ...items,
      { id: Date.now(), teks, selesai: false }
    ]),
    toggle: (id) => update(items =>
      items.map(item =>
        item.id === id ? { ...item, selesai: !item.selesai } : item
      )
    ),
    hapus: (id) => update(items =>
      items.filter(item => item.id !== id)
    ),
    reset: () => set([]),
  };
}

export const todos = createTodoStore();

// ====== 2. READABLE STORE — hanya bisa dibaca ======
export const waktu = readable(new Date(), function start(set) {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return function stop() {
    clearInterval(interval);
  };
});

export const windowWidth = readable(window.innerWidth, function start(set) {
  function handleResize() {
    set(window.innerWidth);
  }
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
});

// ====== 3. DERIVED STORE — dihitung dari store lain ======
export const waktuFormatted = derived(waktu, ($waktu) =>
  $waktu.toLocaleTimeString('id-ID')
);

export const tanggalFormatted = derived(waktu, ($waktu) =>
  $waktu.toLocaleDateString('id-ID', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
);

export const jumlahBelanja = derived(cartItems, ($items) =>
  $items.reduce((total, item) => total + item.jumlah, 0)
);

export const totalBelanja = derived(cartItems, ($items) =>
  $items.reduce((total, item) => total + (item.harga * item.jumlah), 0)
);

export const isMobile = derived(windowWidth, ($width) => $width < 768);

export const activeTodos = derived(todos, ($todos) =>
  $todos.filter(t => !t.selesai)
);

export const completedTodos = derived(todos, ($todos) =>
  $todos.filter(t => t.selesai)
);

Menggunakan Stores di Komponen

Svelte — Menggunakan Stores
<!-- ====== Menggunakan stores di komponen ====== -->
<script>
  import { count, theme, waktuFormatted, activeTodos, todos } from './stores.js';

  // ====== Cara 1: Auto-subscription dengan $ prefix ======
  // Svelte otomatis subscribe/unsubscribe saat komponen mount/unmount
  // $count langsung reactive!

  // ====== Cara 2: Manual subscription ======
  // import { get } from 'svelte/store';
  // let currentValue = get(count); // sekali baca tanpa subscribe
</script>

<!-- Gunakan $ prefix untuk auto-subscribe -->
<div class="dashboard" class:dark={$theme === 'dark'}>
  <h2>{$waktuFormatted}</h2>

  <p>Counter: {$count}</p>

  <!-- Tulis ke store langsung -->
  <button on:click={() => $count++}>+</button>
  <button on:click={() => $count--}>−</button>

  <!-- Toggle theme -->
  <button on:click={() => $theme = $theme === 'dark' ? 'light' : 'dark'}>
    {$theme === 'dark' ? '☀️' : '🌙'} Ganti Tema
  </button>

  <!-- Derived store -->
  <p>Tugas aktif: {$activeTodos.length}</p>

  <!-- Custom store methods -->
  <button on:click={() => todos.tambah('Tugas baru')}>
    Tambah Todo
  </button>
</div>

7. Transitions & Animations

Svelte memiliki sistem transitions built-in yang powerful. Anda bisa menambahkan animasi masuk/keluar pada elemen tanpa library animasi tambahan. Transition diterapkan menggunakan directive transition:, in:, dan out:.

Svelte — Built-in Transitions
<script>
  import { fade, fly, slide, scale, blur, draw } from 'svelte/transition';
  import { quintOut, elasticOut, bounceOut } from 'svelte/easing';
  import { flip } from 'svelte/animate';

  let visible = true;
  let items = [
    { id: 1, teks: 'Belajar Svelte' },
    { id: 2, teks: 'Buat transisi' },
    { id: 3, teks: 'Tambah animasi' },
  ];
  let nextId = 4;

  function toggle() {
    visible = !visible;
  }

  function addItem() {
    items = [...items, { id: nextId++, teks: `Item baru #${nextId - 1}` }];
  }

  function removeItem(id) {
    items = items.filter(i => i.id !== id);
  }
</script>

<!-- Toggle visibility dengan fade -->
<button on:click={toggle}>
  {visible ? 'Sembunyikan' : 'Tampilkan'}
</button>

{#if visible}
  <!-- fade: transisi opacity -->
  <div transition:fade={{ duration: 300 }}>
    <p>Ini menghilang dengan fade!</p>
  </div>
{/if}

{#if visible}
  <!-- fly: masuk dari arah tertentu -->
  <div transition:fly={{ y: 200, duration: 500 }}>
    <p>Terbang dari bawah!</p>
  </div>
{/if}

{#if visible}
  <!-- slide: geser masuk/keluar -->
  <div transition:slide={{ duration: 400 }}>
    <p>Slide masuk dari atas!</p>
  </div>
{/if}

{#if visible}
  <!-- scale: membesar/mengecil -->
  <div transition:scale={{ start: 0.5, duration: 300, easing: elasticOut }}>
    <p>Scale dengan efek elastic!</p>
  </div>
{/if}

{#if visible}
  <!-- blur: efek blur -->
  <div transition:blur={{ amount: 10, duration: 400 }}>
    <p>Masuk dengan efek blur!</p>
  </div>
{/if}

<!-- ====== Transitions dengan {#each} dan FLIP animation ====== -->
<button on:click={addItem}>+ Tambah Item</button>

{#each items as item (item.id)}
  <div
    animate:flip={{ duration: 300 }}
    in:fly={{ x: -50, duration: 300 }}
    out:fade={{ duration: 200 }}
    class="list-item"
  >
    <span>{item.teks}</span>
    <button on:click={() => removeItem(item.id)}>✕</button>
  </div>
{/each}

<!-- ====== Custom CSS Transition ====== -->
<script>
  function typewriter(node, { speed = 50 }) {
    const text = node.textContent;
    const duration = text.length / (speed * 0.001);

    return {
      duration,
      tick: (t) => {
        const i = Math.trunc(text.length * t);
        node.textContent = text.slice(0, i);
      }
    };
  }
</script>

{#if visible}
  <p transition:typewriter={{ speed: 40 }}>
    Efek mengetik dengan custom transition Svelte!
  </p>
{/if}

8. Actions & Lifecycle

Actions adalah cara Svelte untuk menambahkan perilaku pada elemen DOM yang di-render. Mereka sangat berguna untuk interop dengan library pihak ketiga, tooltip, click-outside, lazy loading, dan banyak lagi. Lifecycle callbacks memungkinkan Anda menjalankan kode pada momen penting komponen.

Svelte — Actions & Lifecycle
<script>
  import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte';

  // ====== ACTIONS ======
  // Action: click outside detection
  function clickOutside(node, callback) {
    function handleClick(event) {
      if (!node.contains(event.target)) {
        callback();
      }
    }
    document.addEventListener('click', handleClick, true);
    return {
      destroy() {
        document.removeEventListener('click', handleClick, true);
      }
    };
  }

  // Action: tooltip
  function tooltip(node, text) {
    let tooltipEl;

    function showTooltip() {
      tooltipEl = document.createElement('div');
      tooltipEl.className = 'tooltip';
      tooltipEl.textContent = text;
      document.body.appendChild(tooltipEl);

      const rect = node.getBoundingClientRect();
      tooltipEl.style.left = `${rect.left + rect.width / 2}px`;
      tooltipEl.style.top = `${rect.top - 8}px`;
    }

    function hideTooltip() {
      tooltipEl?.remove();
    }

    node.addEventListener('mouseenter', showTooltip);
    node.addEventListener('mouseleave', hideTooltip);

    return {
      update(newText) {
        text = newText;
        if (tooltipEl) tooltipEl.textContent = newText;
      },
      destroy() {
        node.removeEventListener('mouseenter', showTooltip);
        node.removeEventListener('mouseleave', hideTooltip);
        tooltipEl?.remove();
      }
    };
  }

  // Action: long press
  function longpress(node, { duration = 500, callback }) {
    let timer;

    function start() {
      timer = setTimeout(() => {
        callback();
      }, duration);
    }

    function cancel() {
      clearTimeout(timer);
    }

    node.addEventListener('mousedown', start);
    node.addEventListener('mouseup', cancel);
    node.addEventListener('mouseleave', cancel);

    return {
      destroy() {
        clearTimeout(timer);
        node.removeEventListener('mousedown', start);
        node.removeEventListener('mouseup', cancel);
        node.removeEventListener('mouseleave', cancel);
      }
    };
  }

  // ====== LIFECYCLE ======
  let data = [];
  let containerEl;
  let isLoading = true;
  let isDropdownOpen = false;

  // Dipanggil saat komponen pertama kali mount di DOM
  onMount(async () => {
    console.log('Komponen mounted!');

    // Fetch data
    const response = await fetch('/api/items');
    data = await response.json();
    isLoading = false;

    // Mengembalikan fungsi cleanup (sama seperti onDestroy)
    return () => {
      console.log('Cleanup on unmount');
    };
  });

  // Dipanggil saat komponen di-unmount
  onDestroy(() => {
    console.log('Komponen destroyed!');
    // Cleanup: timers, subscriptions, event listeners, dll.
  });

  // Dipanggil SEBELUM DOM update
  beforeUpdate(() => {
    console.log('DOM akan di-update...');
    // Berguna untuk menyimpan scroll position
  });

  // Dipanggil SETELAH DOM update
  afterUpdate(() => {
    console.log('DOM sudah di-update!');
    // Berguna untuk scroll ke bottom, focus, dll.
  });

  // Tunggu sampai DOM selesai update
  async function scrollToBottom() {
    await tick(); // Tunggu DOM update selesai
    containerEl.scrollTop = containerEl.scrollHeight;
  }
</script>

<!-- Menggunakan actions dengan use: directive -->
<div
  class="dropdown"
  use:clickOutside={() => isDropdownOpen = false}
>
  <button on:click={() => isDropdownOpen = !isDropdownOpen}>
    Menu ▾
  </button>
  {#if isDropdownOpen}
    <div class="dropdown-menu">
      <a href="#">Profil</a>
      <a href="#">Pengaturan</a>
      <a href="#">Logout</a>
    </div>
  {/if}
</div>

<!-- Tooltip action -->
<button use:tooltip={'Klik tombol ini untuk aksi'}>
  Hover saya
</button>

<!-- Long press action -->
<div
  use:longpress={{ duration: 800, callback: () => alert('Long press!') }}
  class="longpress-area"
>
  Tekan dan tahan di sini selama 0.8 detik
</div>

9. SvelteKit Overview

SvelteKit adalah framework full-stack resmi untuk Svelte. Ia menyediakan file-based routing, server-side rendering (SSR), API endpoints, prerendering, dan banyak fitur production-ready lainnya. SvelteKit adalah cara yang direkomendasikan untuk membangun aplikasi web dengan Svelte.

Svelte — SvelteKit Routes
// ====== Struktur Routes SvelteKit ======
src/routes/
├── +page.svelte              → /
├── +layout.svelte            → Layout wrapper untuk semua halaman
├── +error.svelte             → Error page
├── +page.server.js           → Server-side load function untuk /
├── about/
│   └── +page.svelte          → /about
├── blog/
│   ├── +page.svelte          → /blog
│   ├── +page.server.js       → Load data blog dari server
│   └── [slug]/
│       ├── +page.svelte      → /blog/:slug (dynamic route)
│       └── +page.server.js   → Load artikel per slug
├── api/
│   └── users/
│       ├── +server.js        → GET /api/users
│       └── [id]/
│           └── +server.js    → GET/PUT/DELETE /api/users/:id
└── (auth)/
    ├── login/
    │   └── +page.svelte      → /login (group tanpa URL segment)
    └── register/
        └── +page.svelte      → /register
Svelte — SvelteKit Code Examples
<!-- ====== src/routes/+layout.svelte — Layout Utama ====== -->
<script>
  import { page } from '$app/stores';
  import Navbar from '$lib/components/Navbar.svelte';
</script>

<Navbar currentPath={$page.url.pathname} />

<main>
  <!-- Slot: konten halaman di-render di sini -->
  <slot />
</main>

<footer>
  <p>&copy; 2026 My App</p>
</footer>

<!-- ====== src/routes/blog/+page.svelte — Halaman Blog ====== -->
<script>
  // Data dari +page.server.js di-props secara otomatis
  export let data;
</script>

<h1>Blog</h1>

{#each data.posts as post}
  <article>
    <h2><a href="/blog/{post.slug}">{post.title}</a></h2>
    <p>{post.excerpt}</p>
    <time>{post.date}</time>
  </article>
{/each}

// ====== src/routes/blog/+page.server.js — Server Load ======
import { db } from '$lib/server/database';

export async function load() {
  const posts = await db.posts.findMany({
    orderBy: { date: 'desc' },
    take: 10,
  });

  return { posts };
}

// ====== src/routes/api/users/+server.js — API Endpoint ======
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/database';

// GET /api/users
export async function GET({ url }) {
  const limit = Number(url.searchParams.get('limit') || 10);
  const users = await db.users.findMany({ take: limit });
  return json(users);
}

// POST /api/users
export async function POST({ request }) {
  const body = await request.json();
  const user = await db.users.create({ data: body });
  return json(user, { status: 201 });
}

// ====== src/routes/blog/[slug]/+page.server.js ======
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/database';

export async function load({ params }) {
  const post = await db.posts.findUnique({
    where: { slug: params.slug }
  });

  if (!post) {
    throw error(404, 'Artikel tidak ditemukan');
  }

  return { post };
}

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Svelte:

Pertanyaan 1: Apa pendekatan utama Svelte yang membedakannya dari React dan Vue?

a) Menggunakan Virtual DOM yang lebih cepat
b) Mengcompile kode menjadi vanilla JavaScript saat build time
c) Menggunakan Web Workers untuk semua rendering
d) Menggunakan WebAssembly untuk performa tinggi

Pertanyaan 2: Bagaimana cara membuat variabel reaktif di Svelte?

a) Dengan menggunakan useState() hook
b) Dengan menggunakan ref() decorator
c) Cukup deklarasi variabel dengan let — otomatis reaktif
d) Dengan membungkus variabel dalam reactive() function

Pertanyaan 3: Apa fungsi dari $: di Svelte?

a) Mengimpor library pihak ketiga
b) Mendeklarasikan reactive statement yang berjalan ulang saat dependensi berubah
c) Membuat komponen baru
d) Mengakses store global

Pertanyaan 4: Jenis store apa yang hanya bisa dibaca tetapi tidak bisa ditulis oleh komponen luar?

a) writable
b) readable
c) derived
d) readonly

Pertanyaan 5: Apa nama framework full-stack resmi yang dibangun di atas Svelte?

a) SveltePress
b) SvelteKit
c) SvelteApp
d) SvelteCore
🔍 Zoom
100%
🎨 Tema