svelte5-runes-static
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSvelte 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 (prerendering) and combining global stores with component-local runes.
adapter-static使用 Svelte 5 runes 构建静态优先的 SvelteKit 应用,同时避免 hydration 问题。当使用 (预渲染)并将全局存储与组件本地 runes 结合时,可应用这些模式。
adapter-staticRelated Skills
相关技能
- (Svelte 5 runes core patterns)
svelte - (adapters, deployment, SSR/SSG patterns)
sveltekit - (TypeScript patterns and validation)
typescript-core - (unit testing patterns)
vitest
- (Svelte 5 runes 核心模式)
svelte - (适配器、部署、SSR/SSG 模式)
sveltekit - (TypeScript 模式与验证)
typescript-core - (单元测试模式)
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:
- prerenders components to HTML
adapter-static - Runes in module scope don't serialize/deserialize
- State becomes inert/frozen after hydration
- Reactivity completely breaks
Solution: Use traditional stores for global state
writable()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++; }
};
}失败原因:
- 将组件预渲染为 HTML
adapter-static - 模块作用域中的 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 modeSolution: Subscribe in → update → use in
$effect()$state()$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 / for state that needs to survive SSG/SSR:
writable()derived()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);使用 / 管理需要在 SSG/SSR 后保留的状态:
writable()derived()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 with
export let proplet { prop } = $props() - Keep global stores as /
writable()derived() - Add bridge pattern for store → rune state
- Replace syntax with manual subscription in
$store$effect() - Test prerendering with
npm run build - Verify hydration works correctly
- Check for hydration mismatches in console
- Ensure client-only code is guarded with check
browser
从 Svelte 4 迁移到结合 adapter-static 的 Svelte 5 时:
- 将组件级 替换为
$:$derived() - 将 替换为
export let proplet { 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 stores for global state
writable()原因:结合 adapter-static 使用模块作用域中的 Runes
修复:使用传统的 存储管理全局状态
writable()Symptom: "$ is not defined" error in $derived
症状:$derived 中出现“$ is not defined”错误
Cause: Trying to use syntax in runes mode
$storeFix: Use bridge pattern with subscription
$effect()原因:尝试在 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 check or use
browser{#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 测试