0b76a4b119
- Delete Mode1Test, Mode2Test, Mode3Test, Mode5Test (keep only Mode6Test) - Delete old documentation, Docker files, and k8s configs - Add comprehensive Svelte 5 reference library (svelte-reference/) - Update ModeSwitch orchestrator: - Fix port to 3005 (never use 3000/3001) - Add API endpoints for mode switching - Add bun-types and proper tsconfig - Clean up mode folder mappings
8.3 KiB
8.3 KiB
Svelte 5 Runes Complete Guide
Overview
Runes are Svelte 5's new reactivity system. They replace the old $: reactive statements and export let declarations with explicit, predictable primitives.
📦 $state - Reactive State
Basic Usage
<script lang="ts">
let count = $state(0);
let user = $state({ name: 'John', age: 30 });
let items = $state<string[]>([]);
</script>
<button onclick={() => count++}>
Clicked {count} times
</button>
Deep Reactivity
<script lang="ts">
// Objects and arrays are deeply reactive
let todos = $state([
{ id: 1, text: 'Learn Svelte', done: false }
]);
function toggle(id: number) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done; // This triggers updates!
}
</script>
$state.raw - Shallow Reactivity
<script lang="ts">
// Use for large objects where you replace, not mutate
let data = $state.raw<BigObject | null>(null);
async function load() {
data = await fetchData(); // Must reassign, not mutate
}
</script>
$state.snapshot - Get Plain Object
<script lang="ts">
let user = $state({ name: 'John' });
function save() {
// Get a plain, non-reactive copy
const plain = $state.snapshot(user);
localStorage.setItem('user', JSON.stringify(plain));
}
</script>
🔗 $derived - Computed Values
Basic Derived
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
</script>
<p>{count} × 2 = {doubled}</p>
<p>{doubled} × 2 = {quadrupled}</p>
Complex Derived with $derived.by
<script lang="ts">
let items = $state([1, 2, 3, 4, 5]);
let filter = $state('all');
// Use $derived.by for complex computations
let filtered = $derived.by(() => {
console.log('Recomputing filtered list...');
if (filter === 'even') {
return items.filter(n => n % 2 === 0);
}
if (filter === 'odd') {
return items.filter(n => n % 2 !== 0);
}
return items;
});
</script>
Derived from Multiple Sources
<script lang="ts">
let firstName = $state('John');
let lastName = $state('Doe');
let fullName = $derived(`${firstName} ${lastName}`);
let initials = $derived(`${firstName[0]}${lastName[0]}`);
</script>
⚡ $effect - Side Effects
Basic Effect
<script lang="ts">
let count = $state(0);
$effect(() => {
// Runs when count changes
console.log(`Count is now ${count}`);
document.title = `Count: ${count}`;
});
</script>
Effect with Cleanup
<script lang="ts">
let running = $state(false);
$effect(() => {
if (!running) return;
const interval = setInterval(() => {
console.log('tick');
}, 1000);
// Cleanup function
return () => {
clearInterval(interval);
};
});
</script>
$effect.pre - Before DOM Update
<script lang="ts">
let messages = $state<string[]>([]);
let container: HTMLDivElement;
$effect.pre(() => {
// Runs BEFORE DOM updates
// Useful for preserving scroll position
messages; // Track dependency
if (container) {
// Save scroll position before new messages render
}
});
</script>
$effect.tracking - Check if Reactive
<script lang="ts">
$effect(() => {
console.log($effect.tracking()); // true
});
function regularFunction() {
console.log($effect.tracking()); // false
}
</script>
$effect.root - Untracked Effect Scope
<script lang="ts">
// Create effects outside component lifecycle
const cleanup = $effect.root(() => {
$effect(() => {
// This effect lives until cleanup() is called
});
return () => {
// Custom cleanup logic
};
});
// Later: cleanup();
</script>
🎯 $props - Component Props
Basic Props
<script lang="ts">
let { name, age }: { name: string; age: number } = $props();
</script>
<p>{name} is {age} years old</p>
Optional Props with Defaults
<script lang="ts">
interface Props {
name: string;
age?: number;
greeting?: string;
}
let {
name,
age = 18,
greeting = 'Hello'
}: Props = $props();
</script>
<p>{greeting}, {name}! You are {age}.</p>
Rest Props
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
variant?: 'primary' | 'secondary';
}
let { variant = 'primary', ...rest }: Props = $props();
</script>
<button class={variant} {...rest}>
<slot />
</button>
Children as Props (Snippets)
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
header?: Snippet;
children: Snippet;
footer?: Snippet;
}
let { header, children, footer }: Props = $props();
</script>
<div class="card">
{#if header}
<header>{@render header()}</header>
{/if}
<main>{@render children()}</main>
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</div>
🔄 $bindable - Two-Way Binding Props
Basic Bindable
<!-- TextInput.svelte -->
<script lang="ts">
let { value = $bindable('') }: { value?: string } = $props();
</script>
<input bind:value />
<!-- Parent.svelte -->
<script lang="ts">
import TextInput from './TextInput.svelte';
let name = $state('');
</script>
<TextInput bind:value={name} />
<p>Name: {name}</p>
Bindable with Fallback
<script lang="ts">
// Provide a fallback for when parent doesn't bind
let { checked = $bindable(false) }: { checked?: boolean } = $props();
</script>
<input type="checkbox" bind:checked />
🎨 $inspect - Development Debugging
Basic Inspect
<script lang="ts">
let count = $state(0);
let user = $state({ name: 'John' });
// Logs whenever count or user changes
$inspect(count, user);
</script>
Custom Inspect Handler
<script lang="ts">
let data = $state({ x: 0, y: 0 });
$inspect(data).with((type, ...values) => {
if (type === 'update') {
console.log('Data updated:', values);
}
});
</script>
Breakpoint on Change
<script lang="ts">
let buggyValue = $state(0);
// Pauses debugger when value changes
$inspect(buggyValue).with(() => {
debugger;
});
</script>
🏪 Class-Based State with $state
<script lang="ts" module>
// Can be in a separate .svelte.ts file
export class Counter {
count = $state(0);
doubled = $derived(this.count * 2);
increment() {
this.count++;
}
decrement() {
this.count--;
}
reset() {
this.count = 0;
}
}
</script>
<script lang="ts">
const counter = new Counter();
</script>
<button onclick={() => counter.decrement()}>-</button>
<span>{counter.count} (doubled: {counter.doubled})</span>
<button onclick={() => counter.increment()}>+</button>
⚠️ Common Mistakes
❌ Wrong: Destructuring Loses Reactivity
<script lang="ts">
let user = $state({ name: 'John', age: 30 });
// ❌ This breaks reactivity!
let { name, age } = user;
// ✅ Access properties directly
// user.name, user.age
</script>
❌ Wrong: Effect Without Dependencies
<script lang="ts">
let count = $state(0);
// ❌ This runs only once (no reactive reads inside)
$effect(() => {
console.log('This only runs once');
});
// ✅ Read reactive values to create dependencies
$effect(() => {
console.log(`Count is ${count}`);
});
</script>
❌ Wrong: Mutating Derived Values
<script lang="ts">
let items = $state([1, 2, 3]);
let doubled = $derived(items.map(n => n * 2));
// ❌ Cannot mutate derived values!
// doubled.push(8);
// ✅ Mutate the source
items.push(4);
</script>
📊 Runes Comparison Table
| Svelte 4 | Svelte 5 | Purpose |
|---|---|---|
export let x |
let { x } = $props() |
Props |
let x = 0 |
let x = $state(0) |
Reactive state |
$: y = x * 2 |
let y = $derived(x * 2) |
Computed |
$: { ... } |
$effect(() => { ... }) |
Side effects |
$$props |
$props() with rest |
All props |
bind:value |
$bindable() |
Two-way binding |