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
$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
Solution: Export stores directly
// ✅ WORKS - Direct store exports
export function createSocketStore() {
const socket = writable
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
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 = $stateComplete 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{#if hasEvents}
Found {count} events
{#each filtered as event}
No events match filters
{/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
$effect(() => { if (browser) { timestamp = Date.now(); // Only set on client } });
{#if browser}
Loading clock...
{/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{title} ({count})
{#each filteredItems as item}
{@render children?.()}
Typed Store Bridges
<script lang="ts"> import type { Writable, Readable } from 'svelte/store'; interface StoreShape { data: WritableCommon 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>{ focused = true; }} onblur={() => { focused = false; }} class:focused class:invalid={!isValid} />
{charCount}/100
Form State Management
<script lang="ts"> interface FormData { email: string; password: string; } let formData = $stateDebounced 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>{#each results as result}
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 Svelte 5 Runes Documentation SvelteKit adapter-static Svelte Stores