svelte
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSvelte Guidelines
Svelte 开发指南
Mutation Pattern Preference
突变模式偏好
In Svelte Files (.svelte)
在 Svelte 文件(.svelte)中
Always prefer from TanStack Query for mutations. This provides:
createMutation- Loading states ()
isPending - Error states ()
isError - Success states ()
isSuccess - Better UX with automatic state management
始终优先使用 TanStack Query 的 来处理突变。它提供:
createMutation- 加载状态()
isPending - 错误状态()
isError - 成功状态()
isSuccess - 自动状态管理带来的更优用户体验
The Preferred Pattern
推荐模式
Pass and as the second argument to to get maximum context:
onSuccessonError.mutate()svelte
<script lang="ts">
import { createMutation } from '@tanstack/svelte-query';
import * as rpc from '$lib/query';
// Wrap .options in accessor function, no parentheses on .options
// Name it after what it does, NOT with a "Mutation" suffix (redundant)
const deleteSession = createMutation(
() => rpc.sessions.deleteSession.options,
);
// Local state that we can access in callbacks
let isDialogOpen = $state(false);
</script>
<Button
onclick={() => {
// Pass callbacks as second argument to .mutate()
deleteSession.mutate(
{ sessionId },
{
onSuccess: () => {
// Access local state and context
isDialogOpen = false;
toast.success('Session deleted');
goto('/sessions');
},
onError: (error) => {
toast.error(error.title, { description: error.description });
},
},
);
}}
disabled={deleteSession.isPending}
>
{#if deleteSession.isPending}
Deleting...
{:else}
Delete
{/if}
</Button>将 和 作为第二个参数传递给 ,以获取最大上下文:
onSuccessonError.mutate()svelte
<script lang="ts">
import { createMutation } from '@tanstack/svelte-query';
import * as rpc from '$lib/query';
// Wrap .options in accessor function, no parentheses on .options
// Name it after what it does, NOT with a "Mutation" suffix (redundant)
const deleteSession = createMutation(
() => rpc.sessions.deleteSession.options,
);
// Local state that we can access in callbacks
let isDialogOpen = $state(false);
</script>
<Button
onclick={() => {
// Pass callbacks as second argument to .mutate()
deleteSession.mutate(
{ sessionId },
{
onSuccess: () => {
// Access local state and context
isDialogOpen = false;
toast.success('Session deleted');
goto('/sessions');
},
onError: (error) => {
toast.error(error.title, { description: error.description });
},
},
);
}}
disabled={deleteSession.isPending}
>
{#if deleteSession.isPending}
Deleting...
{:else}
Delete
{/if}
</Button>Why This Pattern?
为什么选择这种模式?
- More context: Access to local variables and state at the call site
- Better organization: Success/error handling is co-located with the action
- Flexibility: Different calls can have different success/error behaviors
- 更多上下文:在调用位置可访问局部变量和状态
- 更优组织:成功/错误处理与操作代码放在一起
- 灵活性:不同的调用可以有不同的成功/错误行为
In TypeScript Files (.ts)
在 TypeScript 文件(.ts)中
Always use since createMutation requires component context:
.execute()typescript
// In a .ts file (e.g., load function, utility)
const result = await rpc.sessions.createSession.execute({
body: { title: 'New Session' },
});
const { data, error } = result;
if (error) {
// Handle error
} else if (data) {
// Handle success
}由于 createMutation 需要组件上下文,因此在.ts文件中始终使用 :
.execute()typescript
// In a .ts file (e.g., load function, utility)
const result = await rpc.sessions.createSession.execute({
body: { title: 'New Session' },
});
const { data, error } = result;
if (error) {
// Handle error
} else if (data) {
// Handle success
}Exception: When to Use .execute() in Svelte Files
例外情况:在 Svelte 文件中何时使用 .execute()
Only use in Svelte files when:
.execute()- You don't need loading states
- You're performing a one-off operation
- You need fine-grained control over async flow
仅在以下情况时,才在 Svelte 文件中使用 :
.execute()- 不需要加载状态
- 执行一次性操作
- 需要对异步流程进行细粒度控制
No handle*
Functions - Always Inline
handle*禁止使用handle*
函数 - 始终内联
handle*Never create functions prefixed with in the script tag. If the function is used only once and the logic isn't deeply nested, inline it directly in the template:
handlesvelte
<!-- BAD: Unnecessary wrapper function -->
<script>
function handleShare() {
share.mutate({ id });
}
function handleSelectItem(itemId: string) {
goto(`/items/${itemId}`);
}
</script>
<Button onclick={handleShare}>Share</Button>
<Item onclick={() => handleSelectItem(item.id)} />
<!-- GOOD: Inline the logic directly -->
<Button onclick={() => share.mutate({ id })}>Share</Button>
<Item onclick={() => goto(`/items/${item.id}`)} />This keeps related logic co-located with the UI element that triggers it, making the code easier to follow.
永远不要在脚本标签中创建以为前缀的函数。如果函数仅使用一次且逻辑不复杂,请直接在模板中内联:
handlesvelte
<!-- BAD: Unnecessary wrapper function -->
<script>
function handleShare() {
share.mutate({ id });
}
function handleSelectItem(itemId: string) {
goto(`/items/${itemId}`);
}
</script>
<Button onclick={handleShare}>Share</Button>
<Item onclick={() => handleSelectItem(item.id)} />
<!-- GOOD: Inline the logic directly -->
<Button onclick={() => share.mutate({ id })}>Share</Button>
<Item onclick={() => goto(`/items/${item.id}`)} />这样可以将相关逻辑与触发它的UI元素放在一起,让代码更易于理解。
Styling
样式
For general CSS and Tailwind guidelines, see the skill.
styling有关通用 CSS 和 Tailwind 指南,请参考技能文档。
stylingshadcn-svelte Best Practices
shadcn-svelte 最佳实践
Component Organization
组件组织
- Use the CLI:
bunx shadcn-svelte@latest add [component] - Each component in its own folder under with an
$lib/components/ui/exportindex.ts - Follow kebab-case for folder names (e.g., ,
dialog/)toggle-group/ - Group related sub-components in the same folder
- When using $state, $derived, or functions only referenced once in markup, inline them directly
- 使用 CLI:
bunx shadcn-svelte@latest add [component] - 每个组件放在下的独立文件夹中,并通过
$lib/components/ui/导出index.ts - 文件夹名称遵循短横线命名法(如、
dialog/)toggle-group/ - 将相关子组件放在同一文件夹中
- 当使用$state、$derived或仅在标记中引用一次的函数时,直接内联它们
Import Patterns
导入模式
Namespace imports (preferred for multi-part components):
typescript
import * as Dialog from '$lib/components/ui/dialog';
import * as ToggleGroup from '$lib/components/ui/toggle-group';Named imports (for single components):
typescript
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';Lucide icons (always use individual imports from ):
@lucide/sveltetypescript
// Good: Individual icon imports
import Database from '@lucide/svelte/icons/database';
import MinusIcon from '@lucide/svelte/icons/minus';
import MoreVerticalIcon from '@lucide/svelte/icons/more-vertical';
// Bad: Don't import multiple icons from lucide-svelte
import { Database, MinusIcon, MoreVerticalIcon } from 'lucide-svelte';The path uses kebab-case (e.g., , ), and you can name the import whatever you want (typically PascalCase with optional Icon suffix).
more-verticalminimize-2命名空间导入(推荐用于多部分组件):
typescript
import * as Dialog from '$lib/components/ui/dialog';
import * as ToggleGroup from '$lib/components/ui/toggle-group';命名导入(用于单个组件):
typescript
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';Lucide 图标(始终从单独导入):
@lucide/sveltetypescript
// Good: Individual icon imports
import Database from '@lucide/svelte/icons/database';
import MinusIcon from '@lucide/svelte/icons/minus';
import MoreVerticalIcon from '@lucide/svelte/icons/more-vertical';
// Bad: Don't import multiple icons from lucide-svelte
import { Database, MinusIcon, MoreVerticalIcon } from 'lucide-svelte';路径使用短横线命名法(如、),你可以随意命名导入(通常使用大驼峰命名,可选择性添加Icon后缀)。
more-verticalminimize-2Styling and Customization
样式与自定义
- Always use the utility from
cn()for combining Tailwind classes$lib/utils - Modify component code directly rather than overriding styles with complex CSS
- Use for component variant systems
tailwind-variants - Follow the /
backgroundconvention for colorsforeground - Leverage CSS variables for theme consistency
- 始终使用中的
$lib/utils工具类来组合Tailwind类cn() - 直接修改组件代码,而不是用复杂的CSS覆盖样式
- 使用实现组件变体系统
tailwind-variants - 遵循颜色的/
background约定foreground - 利用CSS变量保证主题一致性
Component Usage Patterns
组件使用模式
Use proper component composition following shadcn-svelte patterns:
svelte
<Dialog.Root bind:open={isOpen}>
<Dialog.Trigger>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
</Dialog.Header>
</Dialog.Content>
</Dialog.Root>遵循shadcn-svelte模式进行正确的组件组合:
svelte
<Dialog.Root bind:open={isOpen}>
<Dialog.Trigger>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
</Dialog.Header>
</Dialog.Content>
</Dialog.Root>Custom Components
自定义组件
- When extending shadcn components, create wrapper components that maintain the design system
- Add JSDoc comments for complex component props
- Ensure custom components follow the same organizational patterns
- Consider semantic appropriateness (e.g., use section headers instead of cards for page sections)
- 扩展shadcn组件时,创建包装组件以保持设计系统一致性
- 为复杂组件属性添加JSDoc注释
- 确保自定义组件遵循相同的组织模式
- 考虑语义恰当性(例如,页面部分使用节标题而非卡片)
Props Pattern
属性模式
Always Inline Props Types
始终内联属性类型
Never create a separate declaration. Always inline the type directly in :
type Props = {...}$props()svelte
<!-- BAD: Separate Props type -->
<script lang="ts">
type Props = {
selectedWorkspaceId: string | undefined;
onSelect: (id: string) => void;
};
let { selectedWorkspaceId, onSelect }: Props = $props();
</script>
<!-- GOOD: Inline props type -->
<script lang="ts">
let { selectedWorkspaceId, onSelect }: {
selectedWorkspaceId: string | undefined;
onSelect: (id: string) => void;
} = $props();
</script>永远不要创建单独的声明。始终在中直接内联类型:
type Props = {...}$props()svelte
<!-- BAD: Separate Props type -->
<script lang="ts">
type Props = {
selectedWorkspaceId: string | undefined;
onSelect: (id: string) => void;
};
let { selectedWorkspaceId, onSelect }: Props = $props();
</script>
<!-- GOOD: Inline props type -->
<script lang="ts">
let { selectedWorkspaceId, onSelect }: {
selectedWorkspaceId: string | undefined;
onSelect: (id: string) => void;
} = $props();
</script>Children Prop Never Needs Type Annotation
Children 属性无需类型注解
The prop is implicitly typed in Svelte. Never annotate it:
childrensvelte
<!-- BAD: Annotating children -->
<script lang="ts">
let { children }: { children: Snippet } = $props();
</script>
<!-- GOOD: children is implicitly typed -->
<script lang="ts">
let { children } = $props();
</script>
<!-- GOOD: Other props need types, but children does not -->
<script lang="ts">
let { children, title, onClose }: {
title: string;
onClose: () => void;
} = $props();
</script>在Svelte中,属性是隐式类型的。永远不要为它添加注解:
childrensvelte
<!-- BAD: Annotating children -->
<script lang="ts">
let { children }: { children: Snippet } = $props();
</script>
<!-- GOOD: children is implicitly typed -->
<script lang="ts">
let { children } = $props();
</script>
<!-- GOOD: Other props need types, but children does not -->
<script lang="ts">
let { children, title, onClose }: {
title: string;
onClose: () => void;
} = $props();
</script>Self-Contained Component Pattern
独立组件模式
Prefer Component Composition Over Parent State Management
优先选择组件组合而非父组件状态管理
When building interactive components (especially with dialogs/modals), create self-contained components rather than managing state at the parent level.
构建交互式组件(尤其是带对话框/模态框的组件)时,创建独立组件而非在父组件层面管理状态。
The Anti-Pattern (Parent State Management)
反模式(父组件状态管理)
svelte
<!-- Parent component -->
<script>
let deletingItem = $state(null);
</script>
{#each items as item}
<Button onclick={() => (deletingItem = item)}>Delete</Button>
{/each}
<AlertDialog open={!!deletingItem}>
<!-- Single dialog for all items -->
</AlertDialog>svelte
<!-- Parent component -->
<script>
let deletingItem = $state(null);
</script>
{#each items as item}
<Button onclick={() => (deletingItem = item)}>Delete</Button>
{/each}
<AlertDialog open={!!deletingItem}>
<!-- Single dialog for all items -->
</AlertDialog>The Pattern (Self-Contained Components)
推荐模式(独立组件)
svelte
<!-- DeleteItemButton.svelte -->
<script lang="ts">
import { createMutation } from '@tanstack/svelte-query';
import { rpc } from '$lib/query';
let { item }: { item: Item } = $props();
let open = $state(false);
const deleteItem = createMutation(() => rpc.items.delete.options);
</script>
<AlertDialog.Root bind:open>
<AlertDialog.Trigger>
<Button>Delete</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<Button onclick={() => deleteItem.mutate({ id: item.id })}>
Confirm Delete
</Button>
</AlertDialog.Content>
</AlertDialog.Root>
<!-- Parent component -->
{#each items as item}
<DeleteItemButton {item} />
{/each}svelte
<!-- DeleteItemButton.svelte -->
<script lang="ts">
import { createMutation } from '@tanstack/svelte-query';
import { rpc } from '$lib/query';
let { item }: { item: Item } = $props();
let open = $state(false);
const deleteItem = createMutation(() => rpc.items.delete.options);
</script>
<AlertDialog.Root bind:open>
<AlertDialog.Trigger>
<Button>Delete</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<Button onclick={() => deleteItem.mutate({ id: item.id })}>
Confirm Delete
</Button>
</AlertDialog.Content>
</AlertDialog.Root>
<!-- Parent component -->
{#each items as item}
<DeleteItemButton {item} />
{/each}Why This Pattern Works
为什么这种模式有效
- No parent state pollution: Parent doesn't need to track which item is being deleted
- Better encapsulation: All delete logic lives in one place
- Simpler mental model: Each row has its own delete button with its own dialog
- No callbacks needed: Component handles everything internally
- Scales better: Adding new actions doesn't complicate the parent
- 不污染父组件状态:父组件无需跟踪正在删除的是哪个条目
- 更好的封装性:所有删除逻辑都集中在一处
- 更简单的心智模型:每行都有自己的删除按钮和对应的对话框
- 无需回调:组件内部处理所有事情
- 扩展性更好:添加新操作不会让父组件变得复杂
When to Apply This Pattern
何时应用这种模式
- Action buttons in table rows (delete, edit, etc.)
- Confirmation dialogs for list items
- Any repeating UI element that needs modal interactions
- When you find yourself passing callbacks just to update parent state
The key insight: It's perfectly fine to instantiate multiple dialogs (one per row) rather than managing a single shared dialog with complex state. Modern frameworks handle this efficiently, and the code clarity is worth it.
- 表格行中的操作按钮(删除、编辑等)
- 列表项的确认对话框
- 需要模态交互的任何重复UI元素
- 当你发现自己为了更新父组件状态而传递回调时
核心要点:实例化多个对话框(每行一个)是完全可行的,比管理单个共享对话框并处理复杂状态要好得多。现代框架能高效处理这种情况,代码的清晰度值得这样做。