svelte5-runes-static

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Svelte 5 Runes with adapter-static (SvelteKit)

Svelte 5 Runes 结合 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.
使用 Svelte 5 runes 构建静态优先的 SvelteKit 应用,同时避免 hydration 问题。当使用
adapter-static
(预渲染)并将全局存储与组件本地 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)
  • svelte
    (Svelte 5 runes 核心模式)
  • sveltekit
    (适配器、部署、SSR/SSG 模式)
  • typescript-core
    (TypeScript 模式与验证)
  • vitest
    (单元测试模式)

Core Expertise

核心专长

Building static-first Svelte 5 applications using runes mode with proper state management patterns that survive prerendering and hydration.
使用 runes 模式构建静态优先的 Svelte 5 应用,采用合适的状态管理模式,确保在预渲染和 hydration 后仍能正常工作。

Critical Compatibility Rules

关键兼容性规则

❌ NEVER: Runes in Module Scope with adapter-static

❌ 禁止:在模块作用域中使用 Runes 搭配 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)
  };
}
问题:Runes 在静态预渲染后无法正确完成 hydration
typescript
// ❌ 失效 - SSG 后状态会被冻结
export function createStore() {
  let state = $state({ count: 0 });
  return {
    get count() { return state.count; },
    increment: () => { state.count++; }
  };
}
失败原因:
  • adapter-static
    将组件预渲染为 HTML
  • 模块作用域中的 Runes 无法序列化/反序列化
  • Hydration 后状态变为惰性/冻结
  • 响应性完全失效
解决方案:使用传统的
writable()
存储管理全局状态
typescript
// ✅ 有效 - 传统存储可正确完成 hydration
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

❌ 禁止:在 $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'));
问题:Runes 模式禁用了
$
自动订阅语法
typescript
// ❌ 失效 - 无法在 $derived 中使用 $
let filtered = $derived($events.filter(e => e.type === 'info'));
//                      ^^^^^^^ 错误:Runes 模式中不支持 $
解决方案:在
$effect()
中订阅 → 更新
$state()
→ 在
$derived()
中使用
typescript
// ✅ 有效 - 手动订阅模式
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

❌ 禁止:使用带 getter 的存储工厂

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: () => { /* ... */ }
  };
}
问题:Getter 无法建立响应式连接
typescript
// ❌ 失效 - Getter 模式破坏响应性
export function createSocketStore() {
  const socket = writable<Socket | null>(null);
  return {
    get socket() { return socket; }, // ❌ 无响应性
    connect: () => { /* ... */ }
  };
}
解决方案:直接导出存储
typescript
// ✅ 有效 - 直接导出存储
export function createSocketStore() {
  const socket = writable<Socket | null>(null);
  const isConnected = derived(socket, $s => $s?.connected ?? false);

  return {
    socket,          // ✅ 直接引用存储
    isConnected,     // ✅ 直接引用派生存储
    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);
使用
writable()
/
derived()
管理需要在 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

组件状态: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>
使用 Runes 管理组件本地状态和逻辑:
typescript
<script lang="ts">
import { user } from '$lib/stores/globalState';

// 使用 Runes 定义 props
let {
  initialCount = 0,
  onUpdate = () => {}
}: {
  initialCount?: number;
  onUpdate?: (count: number) => void;
} = $props();

// 桥接:存储 → Rune 状态
let currentUser = $state<User | null>(null);
$effect(() => {
  const unsub = user.subscribe(u => {
    currentUser = u;
  });
  return unsub;
});

// 组件本地状态
let count = $state(initialCount);
let doubled = $derived(count * 2);

// 副作用
$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

存储 → Rune → 派生链

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}
typescript
<script lang="ts">
import { type Writable } from 'svelte/store';

// 1. 导入全局存储(传统方式)
const { events: eventsStore, filters: filtersStore } = myGlobalStore;

// 2. 桥接到 Rune 状态
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. 派生计算(纯 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>找到 {count} 个事件</p>
  {#each filtered as event}
    <EventCard {event} />
  {/each}
{:else}
  <p>没有匹配筛选条件的事件</p>
{/if}

SSG/SSR Considerations

SSG/SSR 注意事项

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>
typescript
// ✅ 可安全用于预渲染
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();

// ✅ 仅客户端初始化
$effect(() => {
  if (browser) {
    // WebSocket、localStorage 等
    initializeClientOnlyFeatures();
  }
});
</script>

Hydration Mismatch Prevention

避免 Hydration 不匹配

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
// ✅ 避免 Hydration 不匹配
let timestamp = $state<number | null>(null);

$effect(() => {
  if (browser) {
    timestamp = Date.now(); // 仅在客户端设置
  }
});
svelte
<!-- ✅ 仅客户端内容的条件渲染 -->
{#if browser}
  <LiveClock />
{:else}
  <p>加载时钟中...</p>
{/if}

TypeScript Integration

TypeScript 集成

Typed Props with Runes

使用 Runes 的类型化 Props

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?.()}
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>
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>
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 Management

表单状态管理

typescript
<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>
typescript
<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 = '无效邮箱';
  } else if (field === 'password' && formData.password.length < 8) {
    errors.password = '密码过短';
  } else {
    delete errors[field];
  }
}

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

  // 提交逻辑
  const result = await submitForm(formData);

  if (result.ok) {
    // 成功处理
  } 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}>
    提交
  </button>
</form>

Debounced Search

防抖搜索

typescript
<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}
typescript
<script lang="ts">
import { writable, derived } from 'svelte/store';

const searchQuery = writable('');

// 带防抖的传统派生存储
const debouncedQuery = derived(
  searchQuery,
  ($query, set) => {
    const timeout = setTimeout(() => set($query), 300);
    return () => clearTimeout(timeout);
  },
  '' // 初始值
);

// 桥接到 Rune 状态
let query = $state('');
let debouncedValue = $state('');

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

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

// 在派生中使用
let results = $derived(
  debouncedValue.length >= 3
    ? performSearch(debouncedValue)
    : []
);
</script>

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

{#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
从 Svelte 4 迁移到结合 adapter-static 的 Svelte 5 时:
  • 将组件级
    $:
    替换为
    $derived()
  • export let prop
    替换为
    let { prop } = $props()
  • 保留全局存储为
    writable()
    /
    derived()
  • 添加存储 → Rune 状态的桥接模式
  • $store
    语法替换为
    $effect()
    中的手动订阅
  • 使用
    npm run build
    测试预渲染
  • 验证 hydration 是否正常工作
  • 检查控制台中的 hydration 不匹配警告
  • 确保仅客户端代码使用
    browser
    检查进行防护

Testing Patterns

测试模式

Unit Testing Runes

Runes 单元测试

typescript
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');
  });
});
typescript
import { mount } from 'svelte';
import { tick } from 'svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';

describe('Counter', () => {
  it('增加计数', 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

存储桥接测试

typescript
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']);
  });
});
typescript
import { get } from 'svelte/store';
import { tick } from 'svelte';
import { describe, it, expect } from 'vitest';
import { createMyStore } from './myStore';

describe('存储桥接', () => {
  it('同步存储到 Rune 状态', async () => {
    const store = createMyStore();

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

    await tick();

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

Performance Considerations

性能考虑

Avoid Unnecessary Reactivity

避免不必要的响应性

typescript
// ❌ 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)
);
typescript
// ❌ 过度响应
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));

// ✅ 仅计算所需内容
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

缓存昂贵的计算

typescript
// 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;
});
typescript
// 用于昂贵计算的传统派生存储
const expensiveComputation = derived(
  [source1, source2],
  ([$s1, $s2]) => {
    // 昂贵计算
    return complexAlgorithm($s1, $s2);
  }
);

// 桥接到 Rune
let result = $state(null);
$effect(() => {
  const unsub = expensiveComputation.subscribe(v => { result = v; });
  return unsub;
});

Troubleshooting

故障排除

Symptom: State doesn't update after hydration

症状:Hydration 后状态不更新

Cause: Runes in module scope with adapter-static
Fix: Use traditional
writable()
stores for global state
原因:结合 adapter-static 使用模块作用域中的 Runes
修复:使用传统的
writable()
存储管理全局状态

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

症状:$derived 中出现“$ is not defined”错误

Cause: Trying to use
$store
syntax in runes mode
Fix: Use bridge pattern with
$effect()
subscription
原因:尝试在 Runes 模式中使用
$store
语法
修复:使用
$effect()
订阅的桥接模式

Symptom: "Cannot read property of undefined" after SSG

症状:SSG 后出现“Cannot read property of undefined”错误

Cause: Store factory with getters instead of direct exports
Fix: Export stores directly, not wrapped in getters
原因:使用带 getter 的存储工厂而非直接导出
修复:直接导出存储,而非用 getter 包裹

Symptom: Hydration mismatch warnings

症状:Hydration 不匹配警告

Cause: Client-only state rendered during SSR
Fix: Guard with
browser
check or use
{#if browser}
原因:SSR 期间渲染了仅客户端的状态
修复:使用
browser
检查或
{#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
使用传统存储的场景:
  • 状态需要在 SSG/SSR 预渲染后保留
  • 状态是全局/跨组件共享的
  • 状态需要序列化/反序列化
  • 使用 adapter-static 时
使用 Runes 的场景:
  • 状态是组件本地的
  • 构建响应式 UI 逻辑
  • 处理 props 和组件生命周期
  • 从本地状态创建派生计算
使用桥接模式的场景:
  • 需要结合全局存储与组件 Runes
  • 希望从存储值创建派生计算
  • 构建复杂的响应式链

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
  • toolchains-javascript-frameworks-svelte: Svelte 基础模式
  • toolchains-typescript-core: TypeScript 集成
  • toolchains-ui-styling-tailwind: Svelte 组件样式
  • toolchains-javascript-testing-vitest: Svelte 5 测试

References

参考资料