Svelte 5 Complete Guide | Runes· Snippets
이 글의 핵심
Svelte 5 replaces the compiler-magic reactivity of Svelte 4 with explicit Runes ??$state, $derived, $effect, $props. The result is more predictable reactivity, better TypeScript support, and a simpler mental model for complex state.
What Changed in Svelte 5
Svelte 5 replaces implicit compiler magic with explicit Runes ??special function calls the compiler recognizes.
<!-- Svelte 4 -->
<script>
let count = 0; // Reactive by default (compiler magic)
$: doubled = count * 2; // Reactive declaration ($ label)
</script>
<!-- Svelte 5 -->
<script>
let count = $state(0); // Explicit reactive state
const doubled = $derived(count * 2); // Explicit derived value
</script>
Why Runes?
- Work outside
.sveltefiles (in.js/.tsfor shared logic) - Explicit ??no “which variables are reactive?” confusion
- Better TypeScript inference
- Consistent with how frameworks like Solid and Angular handle reactivity
$state ??Reactive State
<script lang="ts">
// Primitive state
let count = $state(0);
let name = $state('Alice');
let visible = $state(true);
// Object state ??deep reactive (nested properties are also reactive)
let user = $state({
name: 'Alice',
settings: { theme: 'dark', lang: 'en' }
});
function updateTheme() {
user.settings.theme = 'light'; // ??Reactive ??UI updates automatically
}
// Array state
let items = $state<string[]>([]);
function addItem(item: string) {
items.push(item); // ??Reactive push
}
</script>
<button onclick={() => count++}>Count: {count}</button>
<p>Doubled: {count * 2}</p>
$state.raw ??Shallow Reactive
<script>
// Only the reference is reactive, not the contents
let items = $state.raw(['a', 'b', 'c']);
function replace() {
items = [...items, 'd']; // Must reassign to trigger update
}
</script>
$derived ??Computed Values
<script lang="ts">
let price = $state(100);
let quantity = $state(3);
let taxRate = $state(0.1);
// Recomputes automatically when dependencies change
const subtotal = $derived(price * quantity);
const tax = $derived(subtotal * taxRate);
const total = $derived(subtotal + tax);
// Complex derived with $derived.by
const sortedItems = $derived.by(() => {
// Use .by for multi-line derived logic
return items
.filter(item => item.active)
.sort((a, b) => a.name.localeCompare(b.name));
});
</script>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax: ${tax.toFixed(2)}</p>
<p>Total: ${total.toFixed(2)}</p>
$effect ??Side Effects
<script lang="ts">
let query = $state('');
let results = $state<string[]>([]);
// Runs after DOM updates, re-runs when dependencies change
$effect(() => {
if (query.length < 2) {
results = [];
return;
}
// Fetch results when query changes
fetchResults(query).then(data => results = data);
// Return cleanup function
return () => {
// Cancels previous fetch if query changes before it completes
};
});
// $effect.pre ??runs before DOM updates (like useLayoutEffect)
$effect.pre(() => {
// Measure DOM before render
});
</script>
<input bind:value={query} placeholder="Search..." />
$props ??Component Props
<!-- Button.svelte -->
<script lang="ts">
interface Props {
label: string;
variant?: 'primary' | 'secondary';
disabled?: boolean;
onclick?: () => void;
}
// $props() destructures all props at once
let { label, variant = 'primary', disabled = false, onclick }: Props = $props();
</script>
<button
class={`btn btn-${variant}`}
{disabled}
{onclick}
>
{label}
</button>
<!-- Usage -->
<Button label="Submit" variant="primary" onclick={() => handleSubmit()} />
Spread Props
<script lang="ts">
let { class: className, ...rest } = $props();
</script>
<!-- Pass remaining props to underlying element -->
<button class={`base-btn ${className}`} {...rest} />
Bindable Props
<!-- Input.svelte -->
<script lang="ts">
let { value = $bindable('') } = $props();
</script>
<input bind:value />
<!-- Usage ??two-way binding -->
<Input bind:value={myValue} />
Snippets ??Replace Slots
Snippets replace the <slot> system with explicit, composable render functions.
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
title: string;
children: Snippet; // Default snippet (replaces default slot)
footer?: Snippet; // Optional named snippet
header?: Snippet<[string]>; // Snippet with parameters
}
let { title, children, footer, header } = $props();
</script>
<div class="card">
<div class="card-header">
{#if header}
{@render header(title)}
{:else}
<h2>{title}</h2>
{/if}
</div>
<div class="card-body">
{@render children()}
</div>
{#if footer}
<div class="card-footer">
{@render footer()}
</div>
{/if}
</div>
<!-- Usage -->
<Card title="My Card">
<!-- Default children snippet -->
<p>Card content goes here.</p>
{#snippet footer()}
<button>Close</button>
{/snippet}
{#snippet header(title)}
<h2 class="custom-title">{title}</h2>
<span class="badge">New</span>
{/snippet}
</Card>
Event Handling
Svelte 5 uses standard DOM event attributes (no more on: directive):
<!-- Svelte 4 -->
<button on:click={handleClick}>Click</button>
<button on:click|preventDefault={handleSubmit}>Submit</button>
<!-- Svelte 5 -->
<button onclick={handleClick}>Click</button>
<!-- Modifiers via inline handler -->
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<button type="submit">Submit</button>
</form>
<!-- Inline handlers -->
<button onclick={() => count++}>+1</button>
<input oninput={(e) => (value = e.currentTarget.value)} />
Shared Reactive State (Runes in .svelte.ts)
One of the biggest Svelte 5 improvements ??reactive logic in plain .svelte.ts files:
// stores/counter.svelte.ts
// This is a .svelte.ts file ??runes work here!
function createCounter(initial = 0) {
let count = $state(initial);
const doubled = $derived(count * 2);
return {
get count() { return count; },
get doubled() { return doubled; },
increment() { count++; },
reset() { count = initial; },
};
}
export const counter = createCounter(0);
<!-- Any component can import and use it -->
<script>
import { counter } from '../stores/counter.svelte.ts';
</script>
<button onclick={counter.increment}>
Count: {counter.count} (doubled: {counter.doubled})
</button>
Lifecycle
<script>
import { onMount, onDestroy } from 'svelte';
let element: HTMLElement;
onMount(() => {
// Runs after component mounts to DOM
const observer = new ResizeObserver(() => { /* ... */ });
observer.observe(element);
return () => observer.disconnect(); // Cleanup on destroy
});
// $effect replaces most onMount use cases in Svelte 5
$effect(() => {
const timer = setInterval(() => tick(), 1000);
return () => clearInterval(timer);
});
</script>
Migration: Svelte 4 ??5
# Automated migration tool
npx sv migrate svelte-5
Key changes:
| Svelte 4 | Svelte 5 |
|---|---|
let count = 0 | let count = $state(0) |
$: doubled = count * 2 | const doubled = $derived(count * 2) |
$: { sideEffect() } | $effect(() => { sideEffect(); }) |
export let prop | let { prop } = $props() |
<slot /> | {@render children()} |
<slot name="x" /> | {@render x()} |
on:click={handler} | onclick={handler} |
Svelte stores (writable) | .svelte.ts files with $state |
SvelteKit Integration
Svelte 5 works seamlessly with SvelteKit ??the routing and SSR layer:
// src/routes/posts/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => {
const posts = await fetch('/api/posts').then(r => r.json());
return { posts };
};
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
// data.posts is fully typed from the load function
</script>
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
</article>
{/each}
Related posts:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Svelte 5 replaces the compiler-magic reactivity of Svelte 4 with explicit Runes ??$state, $derived, $effect, $props. The result is more predictable reactivity, better TypeScript support, and a simpler mental model for complex state.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Vite Complete Guide | Fast Dev Server· Build Optimization
- TypeScript 5 Complete Guide | Decorators· satisfies
- Vitest Complete Guide | Unit Testing· Mocking
Keywords Covered in This Article (Related Search Terms)
This article covers Svelte, Svelte 5, SvelteKit, JavaScript, TypeScript, Frontend, Runes.