auto-subscription syntax\n```typescript\n// ❌ BROKEN - Can't use $ inside $derived\nlet filtered = $derived($events.filter(e => e.type === 'info'));\n// ^^^^^^^ Error: $ not available in runes mode\n```\n\n**Solution**: Subscribe in `$effect()` → update `$state()` → use in `$derived()`\n```typescript\n// ✅ WORKS - Manual subscription pattern\nimport { type Writable } from 'svelte/store';\n\nlet events = $state\u003cEvent[]>([]);\n\n$effect(() => {\n const unsub = eventsStore.subscribe(value => {\n events = value;\n });\n return unsub;\n});\n\nlet filtered = $derived(events.filter(e => e.type === 'info'));\n```\n\n### ❌ NEVER: Store Factory with Getters\n\n**Problem**: Getters don't establish reactive connections\n```typescript\n// ❌ BROKEN - Getter pattern breaks reactivity\nexport function createSocketStore() {\n const socket = writable\u003cSocket | null>(null);\n return {\n get socket() { return socket; }, // ❌ Not reactive\n connect: () => { /* ... */ }\n };\n}\n```\n\n**Solution**: Export stores directly\n```typescript\n// ✅ WORKS - Direct store exports\nexport function createSocketStore() {\n const socket = writable\u003cSocket | null>(null);\n const isConnected = derived(socket, $s => $s?.connected ?? false);\n\n return {\n socket, // ✅ Direct store reference\n isConnected, // ✅ Direct derived reference\n connect: () => { /* ... */ }\n };\n}\n```\n\n## Recommended Hybrid Pattern\n\n### Global State: Traditional Stores\n\nUse `writable()`/`derived()` for state that needs to survive SSG/SSR:\n\n```typescript\n// stores/globalState.ts\nimport { writable, derived } from 'svelte/store';\n\nexport const user = writable\u003cUser | null>(null);\nexport const theme = writable\u003c'light' | 'dark'>('light');\nexport const isAuthenticated = derived(user, $u => $u !== null);\n```\n\n### Component State: Svelte 5 Runes\n\nUse runes for component-local state and logic:\n\n```typescript\n\u003cscript lang=\"ts\">\nimport { user } from '$lib/stores/globalState';\n\n// Props with runes\nlet {\n initialCount = 0,\n onUpdate = () => {}\n}: {\n initialCount?: number;\n onUpdate?: (count: number) => void;\n} = $props();\n\n// Bridge: Store → Rune State\nlet currentUser = $state\u003cUser | null>(null);\n$effect(() => {\n const unsub = user.subscribe(u => {\n currentUser = u;\n });\n return unsub;\n});\n\n// Component-local state\nlet count = $state(initialCount);\nlet doubled = $derived(count * 2);\n\n// Effects\n$effect(() => {\n if (count > 10) {\n onUpdate(count);\n }\n});\n\nfunction increment() {\n count++;\n}\n\u003c/script>\n\n\u003cbutton onclick={increment}>\n {currentUser?.name ?? 'Guest'}: {count} (×2 = {doubled})\n\u003c/button>\n```\n\n## Complete Bridge Pattern\n\n### Store → Rune → Derived Chain\n\n```typescript\n\u003cscript lang=\"ts\">\nimport { type Writable } from 'svelte/store';\n\n// 1. Import global stores (traditional)\nconst { events: eventsStore, filters: filtersStore } = myGlobalStore;\n\n// 2. Bridge to rune state\nlet events = $state\u003cEvent[]>([]);\nlet activeFilters = $state\u003cstring[]>([]);\n\n$effect(() => {\n const unsubEvents = eventsStore.subscribe(v => { events = v; });\n const unsubFilters = filtersStore.subscribe(v => { activeFilters = v; });\n\n return () => {\n unsubEvents();\n unsubFilters();\n };\n});\n\n// 3. Derived computations (pure runes)\nlet filtered = $derived(\n events.filter(e =>\n activeFilters.length === 0 ||\n activeFilters.includes(e.category)\n )\n);\n\nlet count = $derived(filtered.length);\nlet hasEvents = $derived(count > 0);\n\u003c/script>\n\n{#if hasEvents}\n \u003cp>Found {count} events\u003c/p>\n {#each filtered as event}\n \u003cEventCard {event} />\n {/each}\n{:else}\n \u003cp>No events match filters\u003c/p>\n{/if}\n```\n\n## SSG/SSR Considerations\n\n### Prerender-Safe Patterns\n\n```typescript\n// ✅ Safe for prerendering\nexport const load = async ({ fetch }) => {\n const data = await fetch('/api/data').then(r => r.json());\n return { data };\n};\n```\n\n```svelte\n\u003cscript lang=\"ts\">\nimport { browser } from '$app/environment';\n\nlet { data } = $props();\n\n// ✅ Client-only initialization\n$effect(() => {\n if (browser) {\n // WebSocket, localStorage, etc.\n initializeClientOnlyFeatures();\n }\n});\n\u003c/script>\n```\n\n### Hydration Mismatch Prevention\n\n```typescript\n// ✅ Avoid hydration mismatches\nlet timestamp = $state\u003cnumber | null>(null);\n\n$effect(() => {\n if (browser) {\n timestamp = Date.now(); // Only set on client\n }\n});\n```\n\n```svelte\n\u003c!-- ✅ Conditional rendering for client-only content -->\n{#if browser}\n \u003cLiveClock />\n{:else}\n \u003cp>Loading clock...\u003c/p>\n{/if}\n```\n\n## TypeScript Integration\n\n### Typed Props with Runes\n\n```typescript\n\u003cscript lang=\"ts\">\nimport type { Snippet } from 'svelte';\n\ninterface Props {\n title: string;\n count?: number;\n items: Array\u003c{ id: string; name: string }>;\n onSelect?: (id: string) => void;\n children?: Snippet;\n}\n\nlet {\n title,\n count = 0,\n items,\n onSelect = () => {},\n children\n}: Props = $props();\n\nlet selected = $state\u003cstring | null>(null);\nlet filteredItems = $derived(\n items.filter(item =>\n selected === null || item.id === selected\n )\n);\n\u003c/script>\n\n\u003ch2>{title} ({count})\u003c/h2>\n\n{#each filteredItems as item}\n \u003cbutton onclick={() => onSelect(item.id)}>\n {item.name}\n \u003c/button>\n{/each}\n\n{@render children?.()}\n```\n\n### Typed Store Bridges\n\n```typescript\n\u003cscript lang=\"ts\">\nimport type { Writable, Readable } from 'svelte/store';\n\ninterface StoreShape {\n data: Writable\u003cstring[]>;\n status: Readable\u003c'loading' | 'ready' | 'error'>;\n}\n\nconst stores: StoreShape = getMyStores();\n\nlet data = $state\u003cstring[]>([]);\nlet status = $state\u003c'loading' | 'ready' | 'error'>('loading');\n\n$effect(() => {\n const unsubData = stores.data.subscribe(v => { data = v; });\n const unsubStatus = stores.status.subscribe(v => { status = v; });\n return () => {\n unsubData();\n unsubStatus();\n };\n});\n\nlet isEmpty = $derived(data.length === 0);\nlet isReady = $derived(status === 'ready');\n\u003c/script>\n```\n\n## Common Patterns\n\n### Bindable Component State\n\n```typescript\n\u003cscript lang=\"ts\">\nlet {\n value = $bindable(''),\n disabled = false\n}: {\n value?: string;\n disabled?: boolean;\n} = $props();\n\nlet focused = $state(false);\nlet charCount = $derived(value.length);\nlet isValid = $derived(charCount >= 3 && charCount \u003c= 100);\n\u003c/script>\n\n\u003cinput\n bind:value\n {disabled}\n onfocus={() => { focused = true; }}\n onblur={() => { focused = false; }}\n class:focused\n class:invalid={!isValid}\n/>\n\u003cp>{charCount}/100\u003c/p>\n```\n\n### Form State Management\n\n```typescript\n\u003cscript lang=\"ts\">\ninterface FormData {\n email: string;\n password: string;\n}\n\nlet formData = $state\u003cFormData>({\n email: '',\n password: ''\n});\n\nlet errors = $state\u003cPartial\u003cRecord\u003ckeyof FormData, string>>>({});\n\nlet isValid = $derived(\n formData.email.includes('@') &&\n formData.password.length >= 8\n);\n\nlet canSubmit = $derived(\n isValid && Object.keys(errors).length === 0\n);\n\nfunction validate(field: keyof FormData) {\n if (field === 'email' && !formData.email.includes('@')) {\n errors.email = 'Invalid email';\n } else if (field === 'password' && formData.password.length \u003c 8) {\n errors.password = 'Password too short';\n } else {\n delete errors[field];\n }\n}\n\nasync function handleSubmit() {\n if (!canSubmit) return;\n\n // Submit logic\n const result = await submitForm(formData);\n\n if (result.ok) {\n // Success\n } else {\n errors = result.errors;\n }\n}\n\u003c/script>\n\n\u003cform onsubmit={handleSubmit}>\n \u003cinput\n type=\"email\"\n bind:value={formData.email}\n onblur={() => validate('email')}\n />\n {#if errors.email}\n \u003cspan class=\"error\">{errors.email}\u003c/span>\n {/if}\n\n \u003cinput\n type=\"password\"\n bind:value={formData.password}\n onblur={() => validate('password')}\n />\n {#if errors.password}\n \u003cspan class=\"error\">{errors.password}\u003c/span>\n {/if}\n\n \u003cbutton type=\"submit\" disabled={!canSubmit}>\n Submit\n \u003c/button>\n\u003c/form>\n```\n\n### Debounced Search\n\n```typescript\n\u003cscript lang=\"ts\">\nimport { writable, derived } from 'svelte/store';\n\nconst searchQuery = writable('');\n\n// Traditional derived store with debounce\nconst debouncedQuery = derived(\n searchQuery,\n ($query, set) => {\n const timeout = setTimeout(() => set($query), 300);\n return () => clearTimeout(timeout);\n },\n '' // initial value\n);\n\n// Bridge to rune state\nlet query = $state('');\nlet debouncedValue = $state('');\n\n$effect(() => {\n searchQuery.set(query);\n});\n\n$effect(() => {\n const unsub = debouncedQuery.subscribe(v => {\n debouncedValue = v;\n });\n return unsub;\n});\n\n// Use in derived\nlet results = $derived(\n debouncedValue.length >= 3\n ? performSearch(debouncedValue)\n : []\n);\n\u003c/script>\n\n\u003cinput\n type=\"search\"\n bind:value={query}\n placeholder=\"Search...\"\n/>\n\n{#each results as result}\n \u003cSearchResult {result} />\n{/each}\n```\n\n## Migration Checklist\n\nWhen migrating from Svelte 4 to Svelte 5 with adapter-static:\n\n- [ ] Replace component-level `$:` with `$derived()`\n- [ ] Replace `export let prop` with `let { prop } = $props()`\n- [ ] Keep global stores as `writable()`/`derived()`\n- [ ] Add bridge pattern for store → rune state\n- [ ] Replace `$store` syntax with manual subscription in `$effect()`\n- [ ] Test prerendering with `npm run build`\n- [ ] Verify hydration works correctly\n- [ ] Check for hydration mismatches in console\n- [ ] Ensure client-only code is guarded with `browser` check\n\n## Testing Patterns\n\n### Unit Testing Runes\n\n```typescript\nimport { mount } from 'svelte';\nimport { tick } from 'svelte';\nimport { describe, it, expect } from 'vitest';\nimport Counter from './Counter.svelte';\n\ndescribe('Counter', () => {\n it('increments count', async () => {\n const { component } = mount(Counter, {\n target: document.body,\n props: { initialCount: 0 }\n });\n\n const button = document.querySelector('button');\n button?.click();\n\n await tick();\n\n expect(button?.textContent).toContain('1');\n });\n});\n```\n\n### Testing Store Bridges\n\n```typescript\nimport { get } from 'svelte/store';\nimport { tick } from 'svelte';\nimport { describe, it, expect } from 'vitest';\nimport { createMyStore } from './myStore';\n\ndescribe('Store Bridge', () => {\n it('syncs store to rune state', async () => {\n const store = createMyStore();\n\n store.data.set(['item1', 'item2']);\n\n await tick();\n\n expect(get(store.data)).toEqual(['item1', 'item2']);\n });\n});\n```\n\n## Performance Considerations\n\n### Avoid Unnecessary Reactivity\n\n```typescript\n// ❌ Over-reactive\nlet items = $state([1, 2, 3, 4, 5]);\nlet doubled = $derived(items.map(x => x * 2));\nlet tripled = $derived(items.map(x => x * 3));\nlet quadrupled = $derived(items.map(x => x * 4));\n\n// ✅ Compute only what's needed\nlet items = $state([1, 2, 3, 4, 5]);\nlet transformedItems = $derived(\n mode === 'double' ? items.map(x => x * 2) :\n mode === 'triple' ? items.map(x => x * 3) :\n items.map(x => x * 4)\n);\n```\n\n### Memoize Expensive Computations\n\n```typescript\n// Traditional derived store for expensive computations\nconst expensiveComputation = derived(\n [source1, source2],\n ([$s1, $s2]) => {\n // Expensive calculation\n return complexAlgorithm($s1, $s2);\n }\n);\n\n// Bridge to rune\nlet result = $state(null);\n$effect(() => {\n const unsub = expensiveComputation.subscribe(v => { result = v; });\n return unsub;\n});\n```\n\n## Troubleshooting\n\n### Symptom: State doesn't update after hydration\n\n**Cause**: Runes in module scope with adapter-static\n\n**Fix**: Use traditional `writable()` stores for global state\n\n### Symptom: \"$ is not defined\" error in $derived\n\n**Cause**: Trying to use `$store` syntax in runes mode\n\n**Fix**: Use bridge pattern with `$effect()` subscription\n\n### Symptom: \"Cannot read property of undefined\" after SSG\n\n**Cause**: Store factory with getters instead of direct exports\n\n**Fix**: Export stores directly, not wrapped in getters\n\n### Symptom: Hydration mismatch warnings\n\n**Cause**: Client-only state rendered during SSR\n\n**Fix**: Guard with `browser` check or use `{#if browser}`\n\n## Decision Framework\n\n**Use Traditional Stores When:**\n- State needs to survive SSG/SSR prerendering\n- State is global/shared across components\n- State needs to be serialized/deserialized\n- Working with adapter-static\n\n**Use Runes When:**\n- State is component-local\n- Building reactive UI logic\n- Working with props and component lifecycle\n- Creating derived computations from local state\n\n**Use Bridge Pattern When:**\n- Need to combine global stores with component runes\n- Want derived computations from store values\n- Building complex reactive chains\n\n## Related Skills\n\n- **toolchains-javascript-frameworks-svelte**: Base Svelte patterns\n- **toolchains-typescript-core**: TypeScript integration\n- **toolchains-ui-styling-tailwind**: Styling Svelte components\n- **toolchains-javascript-testing-vitest**: Testing Svelte 5\n\n## References\n\n- [Svelte 5 Runes Documentation](https://svelte.dev/docs/svelte/what-are-runes)\n- [SvelteKit adapter-static](https://kit.svelte.dev/docs/adapter-static)\n- [Svelte Stores](https://svelte.dev/docs/svelte-store)\n","licenseDocuments":[]}}
L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Productivité · SKILL.md

svelte5-runes-static

svelte5-runes-static

Svelte 5 runes + SvelteKit adapter-static (SSG/SSR) patterns for hydration-safe state, store bridges, and reactivity that survives prerendering

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: svelte5-runes-static description: Svelte 5 runes + SvelteKit adapter-static (SSG/SSR) patterns for hydration-safe state, store bridges, and reactivity that survives prerendering user-invocable: false disable-model-invocation: true version: 1.0.0 category: toolchain author: Claude MPM Team license: MIT progressive_disclosure: entry_point: summary: "Hydration-safe Svelte 5 runes patterns for SvelteKit adapter-static (SSG/SSR)" when_to_use: "When using Svelte 5 runes with SvelteKit prerendering/adapter-static and encountering frozen state, hydration mismatches, or store bridge issues" quick_start: "1. Keep global state in writable()/derived() 2. Bridge store → $state in $effect() 3. Use $derived() for computations 4. Guard client-only code with browser" context_limit: 150 tags:

  • svelte
  • svelte5
  • sveltekit
  • runes
  • adapter-static
  • ssr
  • ssg
  • hydration
  • stores
  • reactivity
  • prerendering
  • typescript requires_tools: []

Svelte 5 Runes with adapter-static (SvelteKit)

Overview

Build static-first SvelteKit applications with Svelte 5 runes without breaking hydration. Apply these patterns when using adapter-static (prerendering) and combining global stores with component-local runes.

Related Skills

  • svelte (Svelte 5 runes core patterns)
  • sveltekit (adapters, deployment, SSR/SSG patterns)
  • typescript-core (TypeScript patterns and validation)
  • vitest (unit testing patterns)

Core Expertise

Building static-first Svelte 5 applications using runes mode with proper state management patterns that survive prerendering and hydration.

Critical Compatibility Rules

❌ NEVER: Runes in Module Scope with adapter-static

Problem: Runes don't hydrate properly after static prerendering

// ❌ BROKEN - State becomes frozen after SSG
export function createStore() {
  let state = $state({ count: 0 });
  return {
    get count() { return state.count; },
    increment: () => { state.count++; }
  };
}

Why it fails:

  • adapter-static prerenders components to HTML
  • Runes in module scope don't serialize/deserialize
  • State becomes inert/frozen after hydration
  • Reactivity completely breaks

Solution: Use traditional writable() stores for global state

// ✅ WORKS - Traditional stores hydrate correctly
import { writable } from 'svelte/store';

export function createStore() {
  const count = writable(0);
  return {
    count,
    increment: () => count.update(n => n + 1)
  };
}

❌ NEVER: $ Auto-subscription Inside $derived

Problem: Runes mode disables $ auto-subscription syntax

// ❌ BROKEN - Can't use $ inside $derived
let filtered = $derived($events.filter(e => e.type === 'info'));
//                      ^^^^^^^ Error: $ not available in runes mode

Solution: Subscribe in $effect() → update $state() → use in $derived()

// ✅ WORKS - Manual subscription pattern
import { type Writable } from 'svelte/store';

let events = $state<Event[]>([]);

$effect(() => {
  const unsub = eventsStore.subscribe(value => {
    events = value;
  });
  return unsub;
});

let filtered = $derived(events.filter(e => e.type === 'info'));

❌ NEVER: Store Factory with Getters

Problem: Getters don't establish reactive connections

// ❌ BROKEN - Getter pattern breaks reactivity
export function createSocketStore() {
  const socket = writable<Socket | null>(null);
  return {
    get socket() { return socket; }, // ❌ Not reactive
    connect: () => { /* ... */ }
  };
}

Solution: Export stores directly

// ✅ WORKS - Direct store exports
export function createSocketStore() {
  const socket = writable<Socket | null>(null);
  const isConnected = derived(socket, $s => $s?.connected ?? false);

  return {
    socket,          // ✅ Direct store reference
    isConnected,     // ✅ Direct derived reference
    connect: () => { /* ... */ }
  };
}

Recommended Hybrid Pattern

Global State: Traditional Stores

Use writable()/derived() for state that needs to survive SSG/SSR:

// stores/globalState.ts
import { writable, derived } from 'svelte/store';

export const user = writable<User | null>(null);
export const theme = writable<'light' | 'dark'>('light');
export const isAuthenticated = derived(user, $u => $u !== null);

Component State: Svelte 5 Runes

Use runes for component-local state and logic:

<script lang="ts">
import { user } from '$lib/stores/globalState';

// Props with runes
let {
  initialCount = 0,
  onUpdate = () => {}
}: {
  initialCount?: number;
  onUpdate?: (count: number) => void;
} = $props();

// Bridge: Store → Rune State
let currentUser = $state<User | null>(null);
$effect(() => {
  const unsub = user.subscribe(u => {
    currentUser = u;
  });
  return unsub;
});

// Component-local state
let count = $state(initialCount);
let doubled = $derived(count * 2);

// Effects
$effect(() => {
  if (count > 10) {
    onUpdate(count);
  }
});

function increment() {
  count++;
}
</script>

<button onclick={increment}>
  {currentUser?.name ?? 'Guest'}: {count} (×2 = {doubled})
</button>

Complete Bridge Pattern

Store → Rune → Derived Chain

<script lang="ts">
import { type Writable } from 'svelte/store';

// 1. Import global stores (traditional)
const { events: eventsStore, filters: filtersStore } = myGlobalStore;

// 2. Bridge to rune state
let events = $state<Event[]>([]);
let activeFilters = $state<string[]>([]);

$effect(() => {
  const unsubEvents = eventsStore.subscribe(v => { events = v; });
  const unsubFilters = filtersStore.subscribe(v => { activeFilters = v; });

  return () => {
    unsubEvents();
    unsubFilters();
  };
});

// 3. Derived computations (pure runes)
let filtered = $derived(
  events.filter(e =>
    activeFilters.length === 0 ||
    activeFilters.includes(e.category)
  )
);

let count = $derived(filtered.length);
let hasEvents = $derived(count > 0);
</script>

{#if hasEvents}
  <p>Found {count} events</p>
  {#each filtered as event}
    <EventCard {event} />
  {/each}
{:else}
  <p>No events match filters</p>
{/if}

SSG/SSR Considerations

Prerender-Safe Patterns

// ✅ Safe for prerendering
export const load = async ({ fetch }) => {
  const data = await fetch('/api/data').then(r => r.json());
  return { data };
};
<script lang="ts">
import { browser } from '$app/environment';

let { data } = $props();

// ✅ Client-only initialization
$effect(() => {
  if (browser) {
    // WebSocket, localStorage, etc.
    initializeClientOnlyFeatures();
  }
});
</script>

Hydration Mismatch Prevention

// ✅ Avoid hydration mismatches
let timestamp = $state<number | null>(null);

$effect(() => {
  if (browser) {
    timestamp = Date.now(); // Only set on client
  }
});
<!-- ✅ Conditional rendering for client-only content -->
{#if browser}
  <LiveClock />
{:else}
  <p>Loading clock...</p>
{/if}

TypeScript Integration

Typed Props with Runes

<script lang="ts">
import type { Snippet } from 'svelte';

interface Props {
  title: string;
  count?: number;
  items: Array<{ id: string; name: string }>;
  onSelect?: (id: string) => void;
  children?: Snippet;
}

let {
  title,
  count = 0,
  items,
  onSelect = () => {},
  children
}: Props = $props();

let selected = $state<string | null>(null);
let filteredItems = $derived(
  items.filter(item =>
    selected === null || item.id === selected
  )
);
</script>

<h2>{title} ({count})</h2>

{#each filteredItems as item}
  <button onclick={() => onSelect(item.id)}>
    {item.name}
  </button>
{/each}

{@render children?.()}

Typed Store Bridges

<script lang="ts">
import type { Writable, Readable } from 'svelte/store';

interface StoreShape {
  data: Writable<string[]>;
  status: Readable<'loading' | 'ready' | 'error'>;
}

const stores: StoreShape = getMyStores();

let data = $state<string[]>([]);
let status = $state<'loading' | 'ready' | 'error'>('loading');

$effect(() => {
  const unsubData = stores.data.subscribe(v => { data = v; });
  const unsubStatus = stores.status.subscribe(v => { status = v; });
  return () => {
    unsubData();
    unsubStatus();
  };
});

let isEmpty = $derived(data.length === 0);
let isReady = $derived(status === 'ready');
</script>

Common Patterns

Bindable Component State

<script lang="ts">
let {
  value = $bindable(''),
  disabled = false
}: {
  value?: string;
  disabled?: boolean;
} = $props();

let focused = $state(false);
let charCount = $derived(value.length);
let isValid = $derived(charCount >= 3 && charCount <= 100);
</script>

<input
  bind:value
  {disabled}
  onfocus={() => { focused = true; }}
  onblur={() => { focused = false; }}
  class:focused
  class:invalid={!isValid}
/>
<p>{charCount}/100</p>

Form State Management

<script lang="ts">
interface FormData {
  email: string;
  password: string;
}

let formData = $state<FormData>({
  email: '',
  password: ''
});

let errors = $state<Partial<Record<keyof FormData, string>>>({});

let isValid = $derived(
  formData.email.includes('@') &&
  formData.password.length >= 8
);

let canSubmit = $derived(
  isValid && Object.keys(errors).length === 0
);

function validate(field: keyof FormData) {
  if (field === 'email' && !formData.email.includes('@')) {
    errors.email = 'Invalid email';
  } else if (field === 'password' && formData.password.length < 8) {
    errors.password = 'Password too short';
  } else {
    delete errors[field];
  }
}

async function handleSubmit() {
  if (!canSubmit) return;

  // Submit logic
  const result = await submitForm(formData);

  if (result.ok) {
    // Success
  } else {
    errors = result.errors;
  }
}
</script>

<form onsubmit={handleSubmit}>
  <input
    type="email"
    bind:value={formData.email}
    onblur={() => validate('email')}
  />
  {#if errors.email}
    <span class="error">{errors.email}</span>
  {/if}

  <input
    type="password"
    bind:value={formData.password}
    onblur={() => validate('password')}
  />
  {#if errors.password}
    <span class="error">{errors.password}</span>
  {/if}

  <button type="submit" disabled={!canSubmit}>
    Submit
  </button>
</form>

Debounced Search

<script lang="ts">
import { writable, derived } from 'svelte/store';

const searchQuery = writable('');

// Traditional derived store with debounce
const debouncedQuery = derived(
  searchQuery,
  ($query, set) => {
    const timeout = setTimeout(() => set($query), 300);
    return () => clearTimeout(timeout);
  },
  '' // initial value
);

// Bridge to rune state
let query = $state('');
let debouncedValue = $state('');

$effect(() => {
  searchQuery.set(query);
});

$effect(() => {
  const unsub = debouncedQuery.subscribe(v => {
    debouncedValue = v;
  });
  return unsub;
});

// Use in derived
let results = $derived(
  debouncedValue.length >= 3
    ? performSearch(debouncedValue)
    : []
);
</script>

<input
  type="search"
  bind:value={query}
  placeholder="Search..."
/>

{#each results as result}
  <SearchResult {result} />
{/each}

Migration Checklist

When migrating from Svelte 4 to Svelte 5 with adapter-static:

  • Replace component-level $: with $derived()
  • Replace export let prop with let { prop } = $props()
  • Keep global stores as writable()/derived()
  • Add bridge pattern for store → rune state
  • Replace $store syntax with manual subscription in $effect()
  • Test prerendering with npm run build
  • Verify hydration works correctly
  • Check for hydration mismatches in console
  • Ensure client-only code is guarded with browser check

Testing Patterns

Unit Testing Runes

import { mount } from 'svelte';
import { tick } from 'svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';

describe('Counter', () => {
  it('increments count', async () => {
    const { component } = mount(Counter, {
      target: document.body,
      props: { initialCount: 0 }
    });

    const button = document.querySelector('button');
    button?.click();

    await tick();

    expect(button?.textContent).toContain('1');
  });
});

Testing Store Bridges

import { get } from 'svelte/store';
import { tick } from 'svelte';
import { describe, it, expect } from 'vitest';
import { createMyStore } from './myStore';

describe('Store Bridge', () => {
  it('syncs store to rune state', async () => {
    const store = createMyStore();

    store.data.set(['item1', 'item2']);

    await tick();

    expect(get(store.data)).toEqual(['item1', 'item2']);
  });
});

Performance Considerations

Avoid Unnecessary Reactivity

// ❌ Over-reactive
let items = $state([1, 2, 3, 4, 5]);
let doubled = $derived(items.map(x => x * 2));
let tripled = $derived(items.map(x => x * 3));
let quadrupled = $derived(items.map(x => x * 4));

// ✅ Compute only what's needed
let items = $state([1, 2, 3, 4, 5]);
let transformedItems = $derived(
  mode === 'double' ? items.map(x => x * 2) :
  mode === 'triple' ? items.map(x => x * 3) :
  items.map(x => x * 4)
);

Memoize Expensive Computations

// Traditional derived store for expensive computations
const expensiveComputation = derived(
  [source1, source2],
  ([$s1, $s2]) => {
    // Expensive calculation
    return complexAlgorithm($s1, $s2);
  }
);

// Bridge to rune
let result = $state(null);
$effect(() => {
  const unsub = expensiveComputation.subscribe(v => { result = v; });
  return unsub;
});

Troubleshooting

Symptom: State doesn't update after hydration

Cause: Runes in module scope with adapter-static

Fix: Use traditional writable() stores for global state

Symptom: "$ is not defined" error in $derived

Cause: Trying to use $store syntax in runes mode

Fix: Use bridge pattern with $effect() subscription

Symptom: "Cannot read property of undefined" after SSG

Cause: Store factory with getters instead of direct exports

Fix: Export stores directly, not wrapped in getters

Symptom: Hydration mismatch warnings

Cause: Client-only state rendered during SSR

Fix: Guard with browser check or use {#if browser}

Decision Framework

Use Traditional Stores When:

  • State needs to survive SSG/SSR prerendering
  • State is global/shared across components
  • State needs to be serialized/deserialized
  • Working with adapter-static

Use Runes When:

  • State is component-local
  • Building reactive UI logic
  • Working with props and component lifecycle
  • Creating derived computations from local state

Use Bridge Pattern When:

  • Need to combine global stores with component runes
  • Want derived computations from store values
  • Building complex reactive chains

Related Skills

  • toolchains-javascript-frameworks-svelte: Base Svelte patterns
  • toolchains-typescript-core: TypeScript integration
  • toolchains-ui-styling-tailwind: Styling Svelte components
  • toolchains-javascript-testing-vitest: Testing Svelte 5

References

Skills enregistrées