Files
Job-Info/svelte-reference/11-MIGRATION_GUIDE.md
T
aewing 0b76a4b119 Cleanup: Remove old modes, add Svelte 5 reference library, update orchestrator
- 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
2026-01-21 03:56:01 +00:00

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!