svelte5-runes-static
Svelte 5 runes + SvelteKit adapter-static (SSG/SSR) patterns for hydration-safe state, store bridges, and reactivity that survives prerendering
What this skill does
# 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
```typescript
// ❌ 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
```typescript
// ✅ 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
```typescript
// ❌ 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()`
```typescript
// ✅ 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
```typescript
// ❌ 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
```typescript
// ✅ 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:
```typescript
// 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:
```typescript
<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
```typescript
<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
```typescript
// ✅ Safe for prerendering
export const load = async ({ fetch }) => {
const data = await fetch('/api/data').then(r => r.json());
return { data };
};
```
```svelte
<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
```typescript
// ✅ Avoid hydration mismatches
let timestamp = $state<number | null>(null);
$effect(() => {
if (browser) {
timestamp = Date.now(); // Only set on client
}
});
```
```svelte
<!-- ✅ Conditional rendering for client-only content -->
{#if browser}
<LiveClock />
{:else}
<p>Loading clock...</p>
{/if}
```
## TypeScript Integration
### Typed Props with Runes
```typescript
<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
```typescript
<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
```typescript
<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 ManagemRelated in toolchain
nextjs-core
IncludedCore Next.js patterns for App Router development including Server Components, Server Actions, route handlers, data fetching, and caching strategies
nextjs-v16
IncludedNext.js 16 migration guide (async request APIs, "use cache", Turbopack)
vitest
IncludedVitest - Modern TypeScript testing framework with Vite-native performance, ESM support, and TypeScript-first design
mcp-protocol-builder
IncludedMCP (Model Context Protocol) - Build AI-native servers with tools, resources, and prompts. TypeScript/Python SDKs for Claude Desktop integration.
golang-database-patterns
IncludedGo database integration patterns using sqlx, pgx, and migration tools like golang-migrate
sveltekit
IncludedSvelteKit - Full-stack Svelte framework with file-based routing, SSR/SSG, form actions, and adapters for deployment