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
440 lines
8.3 KiB
Markdown
440 lines
8.3 KiB
Markdown
# 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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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`
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<script lang="ts">
|
||
$effect(() => {
|
||
console.log($effect.tracking()); // true
|
||
});
|
||
|
||
function regularFunction() {
|
||
console.log($effect.tracking()); // false
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### `$effect.root` - Untracked Effect Scope
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<script lang="ts">
|
||
let { name, age }: { name: string; age: number } = $props();
|
||
</script>
|
||
|
||
<p>{name} is {age} years old</p>
|
||
```
|
||
|
||
### Optional Props with Defaults
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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)
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<!-- 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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<script lang="ts">
|
||
let buggyValue = $state(0);
|
||
|
||
// Pauses debugger when value changes
|
||
$inspect(buggyValue).with(() => {
|
||
debugger;
|
||
});
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 🏪 Class-Based State with `$state`
|
||
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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
|
||
```svelte
|
||
<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 |
|