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
706 lines
12 KiB
Markdown
706 lines
12 KiB
Markdown
# Svelte 5 Migration Guide
|
|
|
|
## Overview
|
|
|
|
This guide covers migrating from Svelte 4 to Svelte 5, with focus on the new Runes system which replaces the old reactive syntax.
|
|
|
|
---
|
|
|
|
## 🔄 Key Changes at a Glance
|
|
|
|
| Svelte 4 | Svelte 5 | Description |
|
|
|----------|----------|-------------|
|
|
| `let count = 0` | `let count = $state(0)` | Reactive state |
|
|
| `$: doubled = count * 2` | `let doubled = $derived(count * 2)` | Derived values |
|
|
| `$: { sideEffect() }` | `$effect(() => { sideEffect() })` | Side effects |
|
|
| `export let prop` | `let { prop } = $props()` | Component props |
|
|
| `<slot />` | `{@render children()}` | Rendering children |
|
|
| `<slot name="x" />` | `{@render x?.()}` | Named slots → Snippets |
|
|
| `$$props`, `$$restProps` | `$props()` with rest | All props access |
|
|
| `createEventDispatcher()` | Callback props | Event handling |
|
|
|
|
---
|
|
|
|
## 📦 State Migration
|
|
|
|
### Basic State
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
let count = 0;
|
|
|
|
function increment() {
|
|
count += 1;
|
|
}
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
<script>
|
|
let count = $state(0);
|
|
|
|
function increment() {
|
|
count += 1;
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### Object/Array State
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 - Needed reassignment for reactivity -->
|
|
<script>
|
|
let items = [];
|
|
|
|
function addItem(item) {
|
|
items = [...items, item]; // Had to reassign
|
|
}
|
|
|
|
let user = { name: '', age: 0 };
|
|
|
|
function updateName(name) {
|
|
user = { ...user, name }; // Had to spread
|
|
}
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 - Deep reactivity -->
|
|
<script>
|
|
let items = $state([]);
|
|
|
|
function addItem(item) {
|
|
items.push(item); // Direct mutation works!
|
|
}
|
|
|
|
let user = $state({ name: '', age: 0 });
|
|
|
|
function updateName(name) {
|
|
user.name = name; // Direct property assignment works!
|
|
}
|
|
</script>
|
|
```
|
|
|
|
---
|
|
|
|
## 🧮 Derived Values Migration
|
|
|
|
### Simple Derived
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
let count = 0;
|
|
$: doubled = count * 2;
|
|
$: tripled = count * 3;
|
|
$: total = doubled + tripled;
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
<script>
|
|
let count = $state(0);
|
|
let doubled = $derived(count * 2);
|
|
let tripled = $derived(count * 3);
|
|
let total = $derived(doubled + tripled);
|
|
</script>
|
|
```
|
|
|
|
### Complex Derived
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
let items = [];
|
|
let filter = '';
|
|
|
|
$: filteredItems = items.filter(item =>
|
|
item.name.toLowerCase().includes(filter.toLowerCase())
|
|
);
|
|
|
|
$: {
|
|
// Complex computation
|
|
let result = [];
|
|
for (const item of items) {
|
|
if (item.active) {
|
|
result.push(processItem(item));
|
|
}
|
|
}
|
|
processedItems = result;
|
|
}
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
<script>
|
|
let items = $state([]);
|
|
let filter = $state('');
|
|
|
|
let filteredItems = $derived(
|
|
items.filter(item =>
|
|
item.name.toLowerCase().includes(filter.toLowerCase())
|
|
)
|
|
);
|
|
|
|
// For complex computations, use $derived.by()
|
|
let processedItems = $derived.by(() => {
|
|
let result = [];
|
|
for (const item of items) {
|
|
if (item.active) {
|
|
result.push(processItem(item));
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
</script>
|
|
```
|
|
|
|
---
|
|
|
|
## ⚡ Effects Migration
|
|
|
|
### Basic Effects
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
let count = 0;
|
|
|
|
// Reactive statement as effect
|
|
$: console.log('Count changed:', count);
|
|
|
|
// Effect with cleanup wasn't possible inline
|
|
import { onMount, onDestroy } from 'svelte';
|
|
|
|
let interval;
|
|
onMount(() => {
|
|
interval = setInterval(() => count++, 1000);
|
|
});
|
|
onDestroy(() => clearInterval(interval));
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
<script>
|
|
let count = $state(0);
|
|
|
|
// Effect automatically tracks dependencies
|
|
$effect(() => {
|
|
console.log('Count changed:', count);
|
|
});
|
|
|
|
// Effect with cleanup
|
|
$effect(() => {
|
|
const interval = setInterval(() => count++, 1000);
|
|
|
|
return () => clearInterval(interval); // Cleanup function
|
|
});
|
|
</script>
|
|
```
|
|
|
|
### Pre-Effects (beforeUpdate replacement)
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
import { beforeUpdate, afterUpdate } from 'svelte';
|
|
|
|
let messages = [];
|
|
let container;
|
|
let autoscroll = false;
|
|
|
|
beforeUpdate(() => {
|
|
autoscroll = container &&
|
|
container.scrollTop + container.clientHeight >= container.scrollHeight - 20;
|
|
});
|
|
|
|
afterUpdate(() => {
|
|
if (autoscroll) {
|
|
container.scrollTo(0, container.scrollHeight);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
<script>
|
|
let messages = $state([]);
|
|
let container = $state();
|
|
|
|
$effect.pre(() => {
|
|
// Runs before DOM updates
|
|
if (!container) return;
|
|
// ... pre-update logic
|
|
});
|
|
|
|
$effect(() => {
|
|
// Runs after DOM updates
|
|
if (!container) return;
|
|
container.scrollTo(0, container.scrollHeight);
|
|
});
|
|
</script>
|
|
```
|
|
|
|
---
|
|
|
|
## 🎁 Props Migration
|
|
|
|
### Basic Props
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
export let name;
|
|
export let count = 0; // With default
|
|
export let optional = undefined;
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
<script>
|
|
let {
|
|
name,
|
|
count = 0,
|
|
optional
|
|
} = $props();
|
|
</script>
|
|
```
|
|
|
|
### Rest Props
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
export let variant = 'primary';
|
|
// $$restProps used in template
|
|
</script>
|
|
|
|
<button class={variant} {...$$restProps}>
|
|
<slot />
|
|
</button>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
<script>
|
|
let {
|
|
variant = 'primary',
|
|
children,
|
|
...restProps
|
|
} = $props();
|
|
</script>
|
|
|
|
<button class={variant} {...restProps}>
|
|
{@render children?.()}
|
|
</button>
|
|
```
|
|
|
|
### Bindable Props (Two-way Binding)
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 - Parent -->
|
|
<script>
|
|
let value = '';
|
|
</script>
|
|
<Input bind:value />
|
|
|
|
<!-- ❌ Svelte 4 - Child (Input.svelte) -->
|
|
<script>
|
|
export let value;
|
|
</script>
|
|
<input bind:value />
|
|
|
|
<!-- ✅ Svelte 5 - Parent (same) -->
|
|
<script>
|
|
let value = $state('');
|
|
</script>
|
|
<Input bind:value />
|
|
|
|
<!-- ✅ Svelte 5 - Child (Input.svelte) -->
|
|
<script>
|
|
let { value = $bindable() } = $props();
|
|
</script>
|
|
<input bind:value />
|
|
```
|
|
|
|
---
|
|
|
|
## 🎰 Slots → Snippets Migration
|
|
|
|
### Default Slot
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 - Card.svelte -->
|
|
<div class="card">
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- ❌ Svelte 4 - Usage -->
|
|
<Card>
|
|
<p>Card content</p>
|
|
</Card>
|
|
|
|
<!-- ✅ Svelte 5 - Card.svelte -->
|
|
<script>
|
|
let { children } = $props();
|
|
</script>
|
|
|
|
<div class="card">
|
|
{@render children?.()}
|
|
</div>
|
|
|
|
<!-- ✅ Svelte 5 - Usage -->
|
|
<Card>
|
|
<p>Card content</p>
|
|
</Card>
|
|
```
|
|
|
|
### Named Slots
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 - Layout.svelte -->
|
|
<header>
|
|
<slot name="header" />
|
|
</header>
|
|
<main>
|
|
<slot />
|
|
</main>
|
|
<footer>
|
|
<slot name="footer" />
|
|
</footer>
|
|
|
|
<!-- ❌ Svelte 4 - Usage -->
|
|
<Layout>
|
|
<h1 slot="header">Title</h1>
|
|
<p>Main content</p>
|
|
<span slot="footer">Footer</span>
|
|
</Layout>
|
|
|
|
<!-- ✅ Svelte 5 - Layout.svelte -->
|
|
<script>
|
|
import type { Snippet } from 'svelte';
|
|
|
|
let {
|
|
header,
|
|
children,
|
|
footer
|
|
}: {
|
|
header?: Snippet;
|
|
children?: Snippet;
|
|
footer?: Snippet;
|
|
} = $props();
|
|
</script>
|
|
|
|
<header>
|
|
{@render header?.()}
|
|
</header>
|
|
<main>
|
|
{@render children?.()}
|
|
</main>
|
|
<footer>
|
|
{@render footer?.()}
|
|
</footer>
|
|
|
|
<!-- ✅ Svelte 5 - Usage -->
|
|
<Layout>
|
|
{#snippet header()}
|
|
<h1>Title</h1>
|
|
{/snippet}
|
|
|
|
<p>Main content</p>
|
|
|
|
{#snippet footer()}
|
|
<span>Footer</span>
|
|
{/snippet}
|
|
</Layout>
|
|
```
|
|
|
|
### Slot Props → Snippet Parameters
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 - List.svelte -->
|
|
<script>
|
|
export let items = [];
|
|
</script>
|
|
|
|
<ul>
|
|
{#each items as item}
|
|
<li>
|
|
<slot {item} index={items.indexOf(item)} />
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<!-- ❌ Svelte 4 - Usage -->
|
|
<List {items} let:item let:index>
|
|
<span>{index}: {item.name}</span>
|
|
</List>
|
|
|
|
<!-- ✅ Svelte 5 - List.svelte -->
|
|
<script>
|
|
import type { Snippet } from 'svelte';
|
|
|
|
let {
|
|
items,
|
|
children
|
|
}: {
|
|
items: Item[];
|
|
children: Snippet<[{ item: Item; index: number }]>;
|
|
} = $props();
|
|
</script>
|
|
|
|
<ul>
|
|
{#each items as item, index}
|
|
<li>
|
|
{@render children({ item, index })}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<!-- ✅ Svelte 5 - Usage -->
|
|
<List {items}>
|
|
{#snippet children({ item, index })}
|
|
<span>{index}: {item.name}</span>
|
|
{/snippet}
|
|
</List>
|
|
```
|
|
|
|
---
|
|
|
|
## 📡 Events Migration
|
|
|
|
### Custom Events
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 - Button.svelte -->
|
|
<script>
|
|
import { createEventDispatcher } from 'svelte';
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
function handleClick() {
|
|
dispatch('click', { timestamp: Date.now() });
|
|
}
|
|
</script>
|
|
|
|
<button on:click={handleClick}>
|
|
<slot />
|
|
</button>
|
|
|
|
<!-- ❌ Svelte 4 - Usage -->
|
|
<Button on:click={(e) => console.log(e.detail.timestamp)}>
|
|
Click me
|
|
</Button>
|
|
|
|
<!-- ✅ Svelte 5 - Button.svelte -->
|
|
<script>
|
|
let {
|
|
onclick,
|
|
children
|
|
}: {
|
|
onclick?: (data: { timestamp: number }) => void;
|
|
children: Snippet;
|
|
} = $props();
|
|
|
|
function handleClick() {
|
|
onclick?.({ timestamp: Date.now() });
|
|
}
|
|
</script>
|
|
|
|
<button onclick={handleClick}>
|
|
{@render children()}
|
|
</button>
|
|
|
|
<!-- ✅ Svelte 5 - Usage -->
|
|
<Button onclick={(data) => console.log(data.timestamp)}>
|
|
Click me
|
|
</Button>
|
|
```
|
|
|
|
### Event Forwarding
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 - Wrapper.svelte -->
|
|
<button on:click on:mouseenter on:mouseleave>
|
|
<slot />
|
|
</button>
|
|
|
|
<!-- ✅ Svelte 5 - Wrapper.svelte -->
|
|
<script>
|
|
let {
|
|
onclick,
|
|
onmouseenter,
|
|
onmouseleave,
|
|
children,
|
|
...restProps
|
|
} = $props();
|
|
</script>
|
|
|
|
<!-- Option 1: Explicit forwarding -->
|
|
<button {onclick} {onmouseenter} {onmouseleave}>
|
|
{@render children?.()}
|
|
</button>
|
|
|
|
<!-- Option 2: Spread all props -->
|
|
<button {...restProps}>
|
|
{@render children?.()}
|
|
</button>
|
|
```
|
|
|
|
---
|
|
|
|
## 🏪 Stores Migration
|
|
|
|
### Writable Stores
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
import { writable } from 'svelte/store';
|
|
|
|
const count = writable(0);
|
|
|
|
function increment() {
|
|
count.update(n => n + 1);
|
|
}
|
|
</script>
|
|
|
|
<button on:click={increment}>
|
|
Count: {$count}
|
|
</button>
|
|
|
|
<!-- ✅ Svelte 5 - Using class-based state -->
|
|
// stores/counter.svelte.ts
|
|
export class Counter {
|
|
count = $state(0);
|
|
|
|
increment() {
|
|
this.count++;
|
|
}
|
|
}
|
|
|
|
export const counter = new Counter();
|
|
|
|
// Component
|
|
<script>
|
|
import { counter } from './stores/counter.svelte';
|
|
</script>
|
|
|
|
<button onclick={() => counter.increment()}>
|
|
Count: {counter.count}
|
|
</button>
|
|
```
|
|
|
|
### Derived Stores
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
import { writable, derived } from 'svelte/store';
|
|
|
|
const count = writable(0);
|
|
const doubled = derived(count, $count => $count * 2);
|
|
</script>
|
|
|
|
<p>Doubled: {$doubled}</p>
|
|
|
|
<!-- ✅ Svelte 5 -->
|
|
// stores/counter.svelte.ts
|
|
export class Counter {
|
|
count = $state(0);
|
|
doubled = $derived(this.count * 2);
|
|
}
|
|
|
|
// Component
|
|
<script>
|
|
import { counter } from './stores/counter.svelte';
|
|
</script>
|
|
|
|
<p>Doubled: {counter.doubled}</p>
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 Component Lifecycle
|
|
|
|
### onMount / onDestroy
|
|
|
|
```svelte
|
|
<!-- ❌ Svelte 4 -->
|
|
<script>
|
|
import { onMount, onDestroy } from 'svelte';
|
|
|
|
let data;
|
|
let subscription;
|
|
|
|
onMount(async () => {
|
|
data = await fetchData();
|
|
subscription = subscribe();
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
};
|
|
});
|
|
|
|
onDestroy(() => {
|
|
cleanup();
|
|
});
|
|
</script>
|
|
|
|
<!-- ✅ Svelte 5 - onMount still works! -->
|
|
<script>
|
|
import { onMount } from 'svelte';
|
|
|
|
let data = $state(null);
|
|
|
|
onMount(async () => {
|
|
data = await fetchData();
|
|
});
|
|
|
|
// For subscriptions, use $effect
|
|
$effect(() => {
|
|
const subscription = subscribe();
|
|
return () => subscription.unsubscribe();
|
|
});
|
|
</script>
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 Migration Checklist
|
|
|
|
### Phase 1: Preparation
|
|
- [ ] Update to latest Svelte 4.x
|
|
- [ ] Fix all deprecation warnings
|
|
- [ ] Ensure tests are passing
|
|
- [ ] Review component inventory
|
|
|
|
### Phase 2: Upgrade
|
|
- [ ] Update to Svelte 5
|
|
- [ ] Update SvelteKit (if using)
|
|
- [ ] Update dependencies
|
|
|
|
### Phase 3: Migrate Components
|
|
- [ ] Convert `export let` → `$props()`
|
|
- [ ] Convert reactive declarations → `$derived()`
|
|
- [ ] Convert reactive statements → `$effect()`
|
|
- [ ] Convert slots → snippets
|
|
- [ ] Convert events → callback props
|
|
- [ ] Convert stores → class-based state
|
|
|
|
### Phase 4: Validation
|
|
- [ ] Run all tests
|
|
- [ ] Check for runtime errors
|
|
- [ ] Performance testing
|
|
- [ ] Accessibility audit
|
|
|
|
---
|
|
|
|
## 🛠️ Automated Migration
|
|
|
|
Svelte 5 includes a migration script:
|
|
|
|
```bash
|
|
# Run the migration script
|
|
npx sv migrate svelte-5
|
|
|
|
# This will:
|
|
# - Update syntax to Svelte 5
|
|
# - Convert reactive statements
|
|
# - Update event handlers
|
|
# - Flag manual review items
|
|
```
|
|
|
|
**Note:** Always review automated changes and test thoroughly!
|