svelte5-runes-static

安装量: 86
排名: #9264

安装

npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill svelte5-runes-static

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(null); return { get socket() { return socket; }, // ❌ Not reactive connect: () => { / ... / } }; }

Solution: Export stores directly

// ✅ WORKS - Direct store exports export function createSocketStore() { const socket = writable(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(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(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>

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([]); let activeFilters = $state([]); $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}

Found {count} events

{#each filtered as event} {/each} {:else}

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(null);

$effect(() => { if (browser) { timestamp = Date.now(); // Only set on client } });

{#if browser} {:else}

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(null); let filteredItems = $derived( items.filter(item => selected === null || item.id === selected ) ); </script>

{title} ({count})

{#each filteredItems as item}

{@render children?.()}

Typed Store Bridges

<script lang="ts"> import type { Writable, Readable } from 'svelte/store'; interface StoreShape { data: Writable; status: Readable<'loading' | 'ready' | 'error'>; } const stores: StoreShape = getMyStores(); let data = $state([]); 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>

{ 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 = $state({ email: '', password: '' }); let errors = $state>>({}); 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>
validate('email')} /> {#if errors.email} {errors.email} {/if} validate('password')} /> {#if errors.password} {errors.password} {/if}

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>

{#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

返回排行榜