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
12 KiB
12 KiB
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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:
# 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!