npx skills add .
full-stack-skills/svelte-skills
svelte-runes
Svelte 5 Runes 响应式系统技能。当用户需要使用 $state/$derived/$effect/$props/$bindable/$inspect/$host 等符文,或理解 Svelte 5 显式响应式与 Svelte 4 隐式响应式的区别时使用。
Installation
npx skills add full-stack-skills/svelte-skills --skill svelte-runes
Similar popular skills
Related neighbors and high-traction skills in the same topics — useful to compare before installing.
CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, …
9.7K installsGuidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte proj…
6.9K installsFull Sentry SDK setup for Svelte and SvelteKit. Use when asked to "add Sentry to Svelte", "add …
1.9K installsSvelte 5 renderer for json-render that turns JSON specs into Svelte component trees. Use when w…
1.0K installsPre-built shadcn-svelte components for json-render Svelte apps. Use when working with @json-ren…
938 installsSvelte 5 runes, snippets, SvelteKit patterns, and modern best practices for TypeScript and comp…
8.9K installsAlso in this package
Other skills from full-stack-skills/svelte-skills · top by installs.
npx skills add full-stack-skills/svelte-skills
More details
Agent compatibility
Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.
Also listed on
Alternate registries and mirrors of this skill.
Repository health
main
Package contents
Files included with this skill beyond the listing page.
-
skill md
SKILL.md21,531 B -
docs
SUMMARY.md232 B
History
- First seen on skills.sh
- First recorded snapshot · 51 installs
SKILL.md
Svelte Runes Reference (Svelte 5)
本技能覆盖 Svelte 5 的 Runes(符文)系统。Runes 是 Svelte 5 引入的显式响应式语法,取代了 Svelte 4 的隐式 let 声明和 $: 语句。
When to use this skill
当用户需要理解或使用 $state、$derived、$effect、$props、$bindable、$inspect、$host 等符文,或需要将 Svelte 4 代码迁移到 Svelte 5 时使用本技能。
Critical: Runes Overview
Runes 是以 $ 为前缀的符号,类似于函数调用语法:
let message = $state('hello');
关键区别:
- 无需导入 — Runes 是语言内置关键字
- 不是值 — 不能赋值给变量或作为函数参数
- 位置敏感 — 仅在特定位置有效(编译器会报错)
| Rune | 用途 |
|---|---|
$state |
创建响应式状态 |
$derived |
声明派生计算值 |
$effect |
声明副作用 |
$props |
声明组件属性 |
$bindable |
可绑定 prop |
$inspect |
开发调试 |
$host |
自定义元素访问 |
Critical: $state
创建响应式状态,UI 在状态变化时自动更新。
let count = $state(0);
let user = $state({ name: 'Ada', age: 30 });
深层响应式代理
$state 对数组和简单对象自动创建深层 Proxy,属性变更自动触发更新:
let todos = $state([
{ done: false, text: 'task' }
]);
todos[0].done = true; // ✅ 触发更新
todos.push({ done: false }); // ✅ 触发更新
$state.raw(避免深层代理)
适用于大数组和无需深层变更的场景:
let list = $state.raw([]);
// 只能重新赋值,不能 .push()
list = [...list, newItem]; // ✅
list.push(newItem); // ❌ 无效
$state.raw内部仍可包含响应式状态(如原始数组里放代理对象)。
$state.snapshot(静态快照)
获取深层代理的只读快照(用于传给外部 API):
console.log($state.snapshot(proxyObject));
若值有
toJSON()方法,snapshot 会克隆toJSON()的返回值。
$state.eager(即时 UI 更新)
用于 await 表达式中立即更新 UI:
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
</nav>
类字段中的 $state
class Counter {
count = $state(0); // 公共字段
#value = $state(0); // 私有字段
constructor(start = 0) {
this.value = $state(start); // constructor 中初始化
}
// 箭头函数字段 → this 自动绑定
reset = () => { this.count = 0; };
}
编译器将
$state字段转换为原型上的get/set方法,指向私有字段。这些属性不可枚举。注意this绑定:方法直接传给事件处理器会丢失this,用箭头函数字段或内联() => todo.reset()。
内置响应式类(svelte/reactivity)
普通 Map/Set/Date/URL 不响应 —— 改用响应式版本:
import { SvelteSet, SvelteMap, SvelteDate, SvelteURL } from 'svelte/reactivity';
const tags = new SvelteSet(['a', 'b']);
const cache = new SvelteMap();
tags.add('c'); // ✅ 触发更新
cache.set('a', 1); // ✅ 触发更新
解构陷阱
解构后丢失响应式(与普通 JS 行为一致):
let { name, age } = $state({ name: 'Ada', age: 30 });
name = 'Bob'; // ❌ 不会触发更新,原对象不变
跨模块传递状态
.svelte.js/.svelte.ts 文件中可使用 Runes,但不能直接 export let 重新赋值的 $state:
// ❌ 不可行:另一文件读到的会是 Signal 对象
export let count = $state(0);
// ✅ 方案1:不重新赋值整个对象
export const counter = $state({ count: 0 });
export function increment() { counter.count += 1; }
// ✅ 方案2:模块内私有 + 函数导出
let _count = $state(0);
export function getCount() { return _count; }
export function setCount(n) { _count = n; }
将 state 传入函数 —— getter 模式
JavaScript 是按值传递,$state 同理。若函数需读取最新值,传入 getter:
/** @param {() => number} getA @param {() => number} getB */
function add(getA, getB) {
return () => getA() + getB();
}
let a = $state(1);
let b = $state(2);
const total = add(() => a, () => b);
console.log(total()); // 3
a = 3; b = 4;
console.log(total()); // 7
也可借助 proxy 属性或 get/set 属性实现"实时读取"。文档:"Note that 'functions' is broad — it encompasses properties of proxies and get/set properties."
Critical: $derived
声明派生值——基于已有状态的只读计算值。
let count = $state(0);
let doubled = $derived(count * 2);
表达式必须纯净 —— 内部不能修改
$state(编译器报错)。
$derived.by(复杂派生)
let total = $derived.by(() => {
let sum = 0;
for (const item of items) sum += item.price;
return sum;
});
派生值覆盖(Svelte 5.25+,乐观 UI)
let likes = $derived(post.likes);
async function onclick() {
likes += 1; // 即时乐观更新
try {
await like();
} catch {
likes -= 1; // 回滚
}
}
const派生只读;用let才能重新赋值。文档:"Prior to Svelte 5.25, deriveds were read-only."
理解依赖追踪
$derived 内部同步读取的所有 $state/$derived 都是依赖:
let a = Promise.resolve(1);
let b = 2;
let sum = $derived(await a + b);
// a 和 b 都是依赖(await 之后的同步读取也会追踪)
仅表达式自身的
await后的同步读才算依赖;调用函数内部的await不计入。
用 untrack 排除非依赖值。
Push-pull 响应
派生值只在被读取时重新计算(pull),但状态变化时立即通知所有依赖(push)。若派生返回的引用未变,下游不更新:
let count = $state(0);
let large = $derived(count > 10); // 布尔
// 大文本节点只在 large 变化时重渲染,而非 count
派生内避免
() => ({})、[...].map(...)这类返回新引用的写法 —— 会让下游始终重算。
派生不解构代理
$derived 不会把返回值包成 Proxy —— 修改派生返回对象的属性会影响到底层 $state:
let items = $state([...]);
let selected = $derived(items[0]);
selected.name = 'new'; // ✅ 影响 items[0].name
派生解构
let { first, last } = $derived(user);
// 等价于:
// let first = $derived(user.first);
// let last = $derived(user.last);
Critical: $effect
声明副作用——DOM 操作、第三方库调用、网络请求等。不应用 $effect 同步状态。
$effect(() => {
document.title = `count: ${count}`;
return () => { /* 清理函数 */ };
});
生命周期
- 挂载后首次执行
- 状态变化时 microtask 调度(批量)
- DOM 更新之后执行
- 仅浏览器执行(SSR 自动跳过)
- 可在组件任意位置调用,只要在父 effect 运行期间
依赖追踪
自动追踪 $state/$derived 的同步读取:
$effect(() => {
// color 和 size 是依赖
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
});
异步不追踪
await 之后和 setTimeout 内部的读取不追踪:
$effect(() => {
ctx.fillStyle = color; // ✅ 追踪
setTimeout(() => {
ctx.fillRect(0, 0, size, size); // ❌ size 不追踪
}, 0);
});
条件分支决定依赖
effect 只追踪上次运行时实际读取的状态:
$effect(() => {
if (condition) {
confetti({ colors: [color] });
} else {
confetti();
}
});
// condition=false 时 color 不再是依赖
$effect.pre(DOM 更新前运行)
$effect.pre(() => {
if (!div) return; // 挂载前跳过
messages.length; // 显式依赖追踪
// DOM 更新前执行(如滚动位置计算)
});
$effect.tracking(判断追踪上下文)
console.log($effect.tracking()); // false(组件初始化)
$effect(() => {
console.log($effect.tracking()); // true(effect 内)
});
用于
createSubscriber这类工具:仅在状态被追踪时订阅,事件处理器中不订阅。
$effect.pending()(待定 Promise 数量)
返回当前 boundary(不含子 boundary)中待定的 Promise 数量:
<script>
let a = $state(1);
let b = $state(2);
async function add(a, b) {
await new Promise(f => setTimeout(f, 500));
return a + b;
}
</script>
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
<p>{a} + {b} = {await add(a, b)}</p>
{#if $effect.pending()}
<span>pending: {$effect.pending()}</span>
{/if}
$effect.root()(独立作用域)
创建非追踪作用域,不随组件销毁自动清理:
import { flushSync } from 'svelte';
const destroy = $effect.root(() => {
let count = $state(0);
$effect(() => console.log(count));
return () => { /* cleanup */ };
});
count = 1;
flushSync(); // 立即跑 pending effects
// later...
destroy();
适用:测试工具、组件外创建 effect、模块级 effect。文档:"This rune also allows for the creation of effects outside of the component initialisation phase."
清理函数(Teardown)
$effect(() => {
const interval = setInterval(() => count += 1, 1000);
return () => clearInterval(interval); // 清理
});
teardown 在 (a) effect 重跑前、(b) 组件销毁时执行。
何时不用 $effect
| 需求 | 正确方案 |
|---|---|
| 派生计算值 | $derived |
| 双向同步 | 函数绑定 bind:value={() => v, setter} |
| 无限循环 | untrack 包裹读取 |
| 模板内 await | 直接 await |
文档:"In general,
$effectis best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently."
Critical: $props
声明组件属性(props):
let { name, age = 18, ...rest } = $props();
类型安全
<script lang="ts">
// 方式 1:内联注解
let { title }: { title: string } = $props();
// 方式 2:接口分离
interface Props { adjective: string; }
let { adjective }: Props = $props();
</script>
JSDoc 风格:
<script>
/** @type {{ adjective: string }} */
let { adjective } = $props();
</script>
原生 DOM 元素的属性类型在
svelte/elements中(如HTMLButtonAttributes)。
prop 默认值
let { adjective = 'happy', count = 0 } = $props();
默认值不是响应式代理 —— 父组件未传时修改其属性不触发更新。文档:"Fallback values are not turned into reactive state proxies."
prop 重命名
let { super: hero = 'default' } = $props();
let { 'class': className, 'for': htmlFor } = $props();
更新 props
Props 在父组件变化时自动更新,但子组件不应直接修改 prop(除 $bindable 外):
// ❌ 不要修改
let { object } = $props();
object.count += 1; // 警告:ownership_invalid_mutation
// ✅ 用 callback props 或 $bindable
子组件可以临时重新赋值 prop(unsaved 状态),父级不受影响。
Rest Props
let { variant = 'primary', size = 'md', ...rest } = $props();
<button class="btn btn-{variant} btn-{size}" {...rest}>
<slot />
</button>
类型:
{ variant?: 'primary' | 'secondary'; [key: string]: any }或HTMLButtonAttributes。
$props.id()(Svelte 5.20+)
生成当前组件实例唯一的 ID,SSR 时 server/client 一致:
<script>
const uid = $props.id();
</script>
<form>
<label for="{uid}-firstname">First Name:</label>
<input id="{uid}-firstname" type="text" />
</form>
适用:
for/aria-labelledby/aria-describedby等需要 ID 配对的场景。
泛型组件
<script lang="ts" generics="T extends { id: number }">
import type { Snippet } from 'svelte';
let {
items,
renderItem
}: {
items: T[];
renderItem: Snippet<[T]>;
} = $props();
</script>
{#each items as item (item.id)}
{@render renderItem(item)}
{/each}
Critical: $bindable
允许子组件修改父组件状态的 prop 类型——双向数据流:
<!-- FancyInput.svelte -->
<script>
let { value = $bindable(), ...props } = $props();
</script>
<input bind:value={value} {...props} />
<!-- 父组件 -->
<script>
import FancyInput from './FancyInput.svelte';
let message = $state('hello');
</script>
<FancyInput bind:value={message} />
<p>{message}</p>
用法
// 必须放在解构的默认值位置
let {
value = $bindable(), // 必填
other = $bindable('hello'), // 带 fallback
} = $props();
父组件两种用法
<!-- 双向 -->
<FancyInput bind:value={message} />
<!-- 单向(父不监听子) -->
<FancyInput value={message} />
文档:"The parent component doesn't have to use
bind:— it can just pass a normal prop. Some parents don't want to listen to what their children have to say."
何时使用
| 场景 | 推荐 |
|---|---|
| 受控表单组件 | $bindable |
| 子→父事件通知 | callback props |
| 跨组件共享 | Context / store |
文档原话:"This isn't something you should do often — overuse can make your data flow unpredictable and your components harder to maintain."
与函数绑定组合
<script>
let { value = $bindable(0), min = 0, max = 100 } = $props();
function clamp(v: number) { return Math.max(min, Math.min(max, v)); }
</script>
<input
type="range"
bind:value={() => value, (v) => (value = clamp(v))}
{min} {max}
/>
Critical: $inspect / $inspect.trace
开发时打印状态变化(生产环境为 noop):
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // 变化时自动 console.log
</script>
深度追踪 —— 修改
user.name或arr.push(x)都触发。Stack trace 同步打印(playground 除外)。
$inspect(...).with() —— 自定义处理
$inspect(count).with((type, count) => {
if (type === 'update') {
debugger; // 或 console.trace、埋点等
}
});
签名:(type: 'init' | 'update', ...values) => void。
$inspect.trace()(Svelte 5.14+)
让所在函数被追踪,重跑时打印触发的状态:
<script>
$effect(() => {
// ⚠️ 必须是函数体的第一条语句
$inspect.trace();
doSomeWork();
});
</script>
可选 label:
$effect(() => {
$inspect.trace('resize-handler');
layout();
});
文档:"Any time the function re-runs as part of an effect or a derived, information will be printed to the console about which pieces of reactive state caused the effect to fire."
Critical: $host
仅在编译为自定义元素时使用,访问宿主元素:
<svelte:options customElement="my-stepper" />
<script>
function dispatch(type) {
$host().dispatchEvent(new CustomEvent(type));
}
</script>
<button onclick={() => dispatch('decrement')}>-</button>
<button onclick={() => dispatch('increment')}>+</button>
<!-- App.svelte -->
<script>
import './Stepper.svelte';
let count = $state(0);
</script>
<my-stepper
ondecrement={() => (count -= 1)}
onincrement={() => (count += 1)}
></my-stepper>
典型用例
- 派发自定义事件:
$host().dispatchEvent(new CustomEvent('x', { detail, bubbles: true, composed: true })) - 访问宿主属性:
$host().getAttribute('foo')/setAttribute(...) - 调用宿主方法:
$host().focus()等 - 暴露 imperative API:用
export function
自定义元素配置
<svelte:options
customElement="my-tag"
customElementShadow="open" <!-- 或 "closed" / "none" -->
customElementEvents={['change']} <!-- 声明可冒泡事件 -->
customElementObservers={{ attr: ['data-foo'] }} <!-- 观察的 attribute -->
/>
错误用法
<!-- ❌ 普通组件(非 custom element)中调用 -->
<script>
$host(); // 编译错误
</script>
<!-- ❌ 用 $host().querySelector 找内部元素 -->
<script>
$host().querySelector('.child'); // 改用 bind:this
</script>
Quick Fixes
| 问题 | 解决方案 |
|---|---|
| 状态变化不更新 UI | 确认用了 $state(不是普通 let) |
$derived 不生效 |
依赖必须同步读取(不在 await 后) |
$effect 无限循环 |
不要在其中直接修改 $state,改用 $derived |
类方法中 this 丢失 |
用箭头函数字段或内联函数 |
| prop 变异警告 | 用 $bindable 或 callback props |
| 解构后响应式丢失 | 访问原对象属性而非解构变量 |
| Map/Set 修改不触发更新 | 改用 SvelteMap / SvelteSet |
跨文件 $state 读到 Signal 对象 |
用不可重新赋值的对象 + 导出函数 |
Gotchas
$state是深层代理 — 解构后丢失响应式,访问原始对象属性$effect不追踪异步读取 —await/setTimeout后的读取不在依赖中- 类中
$effect字段 — 方法内读取的状态不作为依赖追踪 $effect不应在 SSR 运行 — 浏览器专用,SSR 时自动跳过$props默认值不是代理 — 非$bindable的 prop fallback 值不是响应式对象- 派生返回新引用 — 即便内容相同也会触发下游更新
$inspect是开发工具 — 生产编译为 noop,不可用于业务逻辑$host仅 custom element — 普通组件中使用会编译错误- Map/Set 必须用响应式版本 — 原生 API 不会触发更新
- 跨文件 export
$state—export let重新赋值的$state不可行
FAQ
Q: $derived 和 $state 的区别? A: $state 创建可变状态;$derived 创建只读派生值,自动从依赖推导。5.25+ 可用 let 覆盖派生实现乐观 UI。
Q: 什么时候用 $effect? A: 仅用于副作用:DOM 操作、第三方库调用、网络请求。派生值同步状态永远不用 $effect。
Q: $state.raw 和普通 $state 的区别? A: $state.raw 不对数组/对象创建深层代理,性能更好,但只能通过重新赋值来更新。
Q: $props 能解构吗? A: 可以,且支持默认值、重命名、rest 解构、$bindable、$props.id()。
Q: $bindable 与函数绑定区别? A: $bindable 是声明 prop 可双向;函数绑定 bind:value={() => v, setter} 是自定义读写逻辑。两者可组合。
Q: 何时用 $bindable vs callback props? A: 受控表单用 $bindable;事件通知用 callback props。避免滥用 $bindable 造成数据流混乱。
Q: $inspect 在生产会运行吗? A: 不会,生产构建编译为 noop。
Q: $effect.pending() 有什么用途? A: 显示当前 boundary 中待定的 Promise 数量,可用于全局 loading 状态。
Q: $effect.root() 何时用? A: 测试工具、组件外创建 effect、模块级 effect。需手动调用返回的 destroy() 清理。
Q: 跨模块共享状态用 .svelte.js 还是 Context? A: 真正全局单例用 .svelte.js;请求级隔离/组件树共享优先 Context。
Examples
可执行的代码示例,见 examples/ 目录:
| 文件 | 内容 |
|---|---|
state-patterns.md |
$state 基础、深层代理、$state.raw/snapshot/eager、类字段、内置响应式类、跨模块 |
derived-patterns.md |
$derived 基础、$derived.by、乐观 UI、解构派生、update propagation、async |
effect-patterns.md |
$effect 基础、cleanup、$effect.pre/tracking/pending/root、禁忌、untrack |
props-patterns.md |
$props 解构、类型安全、Rest Props、$bindable、$props.id()、泛型组件 |
bindable-patterns.md |
$bindable 全部用法:双向绑定、fallback、函数绑定、数组共享 |
inspect-patterns.md |
$inspect/$inspect.trace/$inspect.with 调试模式 |
References
深入技术参考,见 references/ 目录:
| 文件 | 内容 |
|---|---|
runes-overview.md |
Runes 整体架构、vs Legacy 对比、生命周期 |
$state-deep.md |
Proxy 行为、$state.raw/snapshot/eager、类中 $state、内置响应式类、跨模块 |
$derived-deep.md |
表达式 vs $derived.by、依赖追踪、可写派生、Push-pull、解构 |
$effect-deep.md |
pre/tracking/cleanup/pending/root、追踪规则、常见错误 |
$props-deep.md |
解构、Rest Props、Type safety、$props.id()、泛型 |
$bindable-deep.md |
完整 $bindable 参考:双向绑定、fallback、函数绑定组合 |
$inspect-deep.md |
$inspect、$inspect.with、$inspect.trace 完整参考 |
$host-deep.md |
自定义元素 $host 用法、<svelte:options> 配置、典型模式 |
context-deep.md |
createContext vs setContext、类型安全、SSR |