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

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!