This skill covers Svelte 5's runtime APIs, including cross-component state (Stores/Context), lifecycle, imperative APIs, SSR, and testing.
When to use this skill
Use this skill when the user needs to share state across components, handle component lifecycles, use mount/render and other imperative APIs, or write Vitest component tests.
Critical: Stores (svelte/store)
A Store is an older way to share reactive state across components (Svelte 5 recommends using the .svelte.js object in $state instead).
writable
import { writable } from 'svelte/store';
const count = writable(0);
count.subscribe(v => console.log(v)); // subscribe count.set(1); // set value count.update(n => n + 1); // update
### readable(不可从外部设置)
```js
import { readable } from 'svelte/store';
const time = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000);
return () => clearInterval(interval);
});
derived
import { derived } from 'svelte/store';
const doubled = derived(count, $c => $c * 2);
const combined = derived([a, b], ([$a, $b]) => $a + $b);
readonly
import { readonly, writable } from 'svelte/store';
const readOnlyStore = readonly(writableStore);
Using in components
<script>
import { count } from './store.js';
console.log($count); // $ 前缀订阅
$count = 2; // 自动调用 .set()
</script>
A Store must be declared at the top level of a component; do not add a
$prefix before the variable (that is$count, notcount)。
Svelte 5 recommended alternatives:
// state.svelte.js
export const userState = $state({ name: '', count: 0 });
// 其他组件直接 import
Critical: Context (svelte)
Pass data across component levels without prop-drilling through layers of props:
setContext / getContext
// Parent.svelte
<script>
import { setContext } from 'svelte';
setContext('key', 'hello');
</script>
// Child.svelte
<script>
import { getContext } from 'svelte';
const msg = getContext('key');
</script>
createContext (recommended, type-safe)
// context.ts
import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();
// Parent.svelte
<script> setUserContext({ name: 'Ada' }); </script>
// Child.svelte
<script> const user = getUserContext(); </script>
Reactive Context
Store the $state object in Context:
setContext('counter', counterState);
// ❌ 不要重新赋值整个对象
// counterState = { count: 0 }
// ✅ 直接修改属性
counterState.count = 0;
Critical: Lifecycle Hooks
Mental model: Svelte 5's lifecycle has only two phases — creation and destruction. The "state update" in between is handled by effect , because the smallest unit of reactivity is the effect, not the component. So there is no
beforeUpdate/afterUpdatehook.
onMount
Runs after the component is mounted to the DOM (does not run during SSR):
<script>
import { onMount } from 'svelte';
onMount(() => {
console.log('mounted');
return () => console.log('unmounted'); // 返回清理函数
});
</script>
The returned function is called when the component is unmounted; if an async function is passed in, the cleanup function will not be called (async functions always return
Promise). When you need an async task plus cleanup, wrap it in an IIFE, and the outer arrow function synchronously returns the cleanup function.
onDestroy
Runs before the component is unmounted (also runs during SSR):
<script>
import { onDestroy } from 'svelte';
onDestroy(() => cleanup());
</script>
tick
Wait for DOM updates to complete before continuing:
<script>
import { tick } from 'svelte';
$effect.pre(() => {
// DOM 更新前
tick().then(() => {
// DOM 已更新完成
});
});
</script>
Lifecycle coordination with mount/unmount/hydrate
| Phase | mount() | hydrate() | render() (SSR) | unmount() |
|---|---|---|---|---|
| Synchronous: component script, state, template | ✓ | ✓ | ✓ | — |
| Synchronous: insert into DOM / output string | ✓ replace target | ✓ reuse SSR DOM | ✓ return head+body | — |
Microtask:$effect / onMount trigger |
✓ | ✓ | ✗ | — |
| Synchronous: cleanup effects + onDestroy | — | — | ✓ (after render completes) | ✓ |
| Asynchronous: transitions (outro) | — | — | — | outro:true wait during |
mount()When returns, effects andonMounthave not run yet; to make them run immediately,flushSync()。hydrate()Same for.tick()andflushSync()are no-ops on the server (there is no reactive scheduling).
Critical: Imperative Component API
mount (client-side mount)
import { mount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {
target: document.querySelector('#app'),
props: { name: 'world' }
});
unmount (unmount)
import { mount, unmount } from 'svelte';
unmount(app, { outro: true }); // 播放过渡后卸载
render (server-side rendering)
import { render } from 'svelte/server';
const { head, body } = await render(App, { props: { data } });
hydrate (client-side hydration)
Reuse SSR HTML and activate it into interactive:
import { hydrate } from 'svelte';
const app = hydrate(App, { target: document.querySelector('#app') });
Critical: Hydratable Data
Serialize data during SSR and reuse it during client hydration (avoid duplicate requests):
<script>
import { hydratable } from 'svelte';
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
CSP support
hydratable() will embed an inline render() in the head returned by <script>. CSP blocks it by default; you need to allow it with a nonce or hash:
// 动态 SSR:每请求一个 nonce
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce }
});
// 响应头同步加上:
// Content-Security-Policy: script-src 'self' 'nonce-${nonce}'
// 静态 SSG:使用 hash
const { head, body, hashes } = await render(App, {
csp: { hash: true }
});
// Content-Security-Policy: script-src ${hashes.script.map(h => `'${h}'`).join(' ')}
Important:
- Not used
hydratable()whenrender()does not output inline<script>, CSP strict mode will not block anything. - inline event handlers (
<button onclick={...}>) are not<script>tags and do not need a nonce. - The nonce mode is recommended; hash mode will conflict with streaming SSR in the future.
- See references/csp-reference.md。
Critical: Best Practices
$state
// ✅ 只对需要响应式的变量用 $state
let count = $state(0);
// ✅ 大数组/不可变数据用 $state.raw
let apiData = $state.raw([]);
apiData = await fetchData(); // 重新赋值触发更新
$derived
// ✅ 派生值用 $derived
let doubled = $derived(count * 2);
// ❌ 不要用 $effect 同步派生
let square;
$effect(() => { square = num * num; }); // ❌
let square = $derived(num * num); // ✅
$effect
// ✅ 仅用于副作用
$effect(() => { myChart.resize(); });
// ❌ 不要同步状态
$effect(() => { left = total - spent; }); // ❌
Props
// ✅ 依赖 props 的值用 $derived
let { type } = $props();
let color = $derived(type === 'danger' ? 'red' : 'green');
Events
<!-- ✅ 现代事件属性 -->
<button onclick={handleClick}>click</button>
<!-- ❌ 旧式 on:click -->
<button on:click={handleClick}>click</button>
<!-- ✅ 监听 window 事件 -->
<svelte:window onkeydown={handleKey} />
Snippets
<!-- ✅ 推荐:使用 snippet -->
{#snippet footer()}
<p>footer</p>
{/snippet}
Each blocks
<!-- ✅ 始终使用 key -->
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
Context
// ✅ 用 createContext 提供类型安全
import { createContext } from 'svelte';
const [getCtx, setCtx] = createContext<MyCtx>();
Avoid Legacy Features
// ✅ Runes mode 全是新项目的默认
let count = $state(0); // ✅
$: count++; // ❌ legacy
export let name; // ❌
let { name } = $props(); // ✅
on:click={fn} // ❌
onclick={fn} // ✅
<slot /> // ❌
{@render children()} // ✅
<svelte:component this={x} /> // ❌
<svelte:element this={x} /> // ✅
Critical: Testing (Vitest)
Installation and configuration
npm install -D vitest
// vite.config.js
export default defineConfig({
resolve: process.env.VITEST
? { conditions: ['browser'] }
: undefined
});
Unit testing
// multiplier.svelte.js
export function multiplier(initial, k) {
let count = $state(initial);
return {
get value() { return count * k; },
set: (c) => { count = c; }
};
}
// test
import { multiplier } from './multiplier.svelte.js';
test('multiplier', () => {
let double = multiplier(0, 2);
expect(double.value).toEqual(0);
double.set(5);
expect(double.value).toEqual(10);
});
Component testing
import { mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
test('MyComponent', () => {
const comp = mount(MyComponent, { target: document.body, props: { name: 'Ada' } });
expect(document.body.innerHTML).toContain('Ada');
unmount(comp);
});
flushSync
Force all pending effects to run synchronously (commonly used in tests):
import { flushSync } from 'svelte';
flushSync();
Quick Fixes
| Problem | Solution |
|---|---|
| State is not synchronized between components | Use Context (across levels) or .svelte.js(global) |
| onMount cleanup does not run | Make sure to pass a synchronous function (not async); when async is needed, wrap it in an IIFE |
| Components are not interactive after SSR | Use hydrate instead of mount |
| State does not update in tests | Use flushSync() to force synchronous execution |
| Context link is broken | Do not reassign the entire Context object |
| effect does not run after mount | mount() returns synchronously, but the effect only runs in the next microtask; if you need it to run immediately, flushSync() |
| CSP blocks hydratable scripts | Use csp: { nonce }(dynamic) or csp: { hash: true }(static), and declare it synchronously in the response headers |
| async onMount cannot get cleanup | outer arrow synchronous return () => controller.abort(), put the async logic inside the IIFE |
Gotchas
- Store
$prefix is subscription syntax —$storeis a subscription in both templates and scripts, not$variables without the prefix are plain values - Context link is broken — reassigning the entire object breaks the reactive link; use
.count = 0instead ofobj = {count:0} mountdoes not run$effect— effects are triggered by a microtask after the component mounts- In tests
$effect.root— tests involving effects need$effect.root()wrapping renderis only available in the SSR environment — on the client usemount- async
onMountcleanup is ignored — an async function always returns a Promise, so Svelte cannot get the cleanup function; use the IIFE pattern hydrate()does not run effects synchronously — likemount(), after hydration you needflushSync()to make effects run- CSP nonce is one-time — a new nonce per request; hash mode conflicts with future streaming SSR
- hydration random values will not match —
Math.random()/Date.now()differs between SSR and CSR; usehydratable()wrapping
FAQ
Q: When should you use Store vs Context vs .svelte.js?
A: Store: simple values shared across the whole app; Context: shared locally within a component tree;.svelte.js $state: the recommended new approach, can replace Store.
Q: tick and await Promise.resolve() what is the difference?
A: tick() guarantees resolution after the DOM updates,Promise.resolve() Does not wait for Svelte's update cycle.
Q: tick() and flushSync() how to choose?
A: await tick() is suitable for asynchronous user code;flushSync() is suitable for testing and external library synchronization scenarios that need immediate DOM synchronization.
Q: How to test code with $effect ?
A: Use flushSync() to force execution, use $effect.root() to wrap the test code.
Q: Hydration error after SSR?
A: Ensure the HTML structure of SSR and CSR is consistent; use hydrate instead of mount。
Q: When to use onMount vs $effect?
A: onMount is a side effect that "runs once after mounting";$effect is a reactive side effect that "reruns when dependencies change".
Q: hydrate 's inline script is blocked by CSP, what should I do?
A: Use csp: { nonce } to add a nonce to the script at render time, and declare it synchronously in the response headers; for static sites, use csp: { hash: true } to get hashes.script。
Examples
Practical examples demonstrating Svelte 5's runtime APIs.
| File | Description |
|---|---|
| examples/README.md | Entry point with quick reference |
| examples/mount-hydrate.md | 9 examples: mount, unmount, hydrate, render, flushSync, tick |
| examples/ssr-patterns.md | SSR patterns: async render, streaming, hydration, CSP |
| examples/onmount-tick.md | 15 lifecycle examples: onMount (sync/async/cleanup), onDestroy, tick for focus/measure/scroll, $effect.pre pattern |
| examples/csp-examples.md | 8 CSP examples: nonce (Express/SvelteKit/Cloudflare), hash (SSG), diagnostic tips |
References
In-depth technical references for Svelte 5's runtime APIs.
| File | Description |
|---|---|
| references/README.md | Entry point with quick reference |
| references/mount-unmount-reference.md | Deep dive: mount/unmount/hydrate lifecycle, flushSync, tick |
| references/ssr-reference.md | SSR deep dive: render, async SSR, CSP, hydration |
| references/lifecycle-runtime-reference.md | Lifecycle in mount/hydrate runtime: when effects run, onMount timing, tick vs flushSync, SSR phase matrix |
| references/csp-reference.md | CSP nonce vs hash, runtime configuration, streaming compatibility |