svelte

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Svelte Guidelines

Svelte 开发指南

Mutation Pattern Preference

突变模式偏好

In Svelte Files (.svelte)

在 Svelte 文件(.svelte)中

Always prefer
createMutation
from TanStack Query for mutations. This provides:
  • 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
onSuccess
and
onError
as the second argument to
.mutate()
to get maximum context:
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>
onSuccess
onError
作为第二个参数传递给
.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
.execute()
since createMutation requires component context:
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
.execute()
in Svelte files when:
  1. You don't need loading states
  2. You're performing a one-off operation
  3. You need fine-grained control over async flow
仅在以下情况时,才在 Svelte 文件中使用
.execute()
  1. 不需要加载状态
  2. 执行一次性操作
  3. 需要对异步流程进行细粒度控制

No
handle*
Functions - Always Inline

禁止使用
handle*
函数 - 始终内联

Never create functions prefixed with
handle
in the script tag. If the function is used only once and the logic isn't deeply nested, inline it directly in the template:
svelte
<!-- 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.
永远不要在脚本标签中创建以
handle
为前缀的函数。如果函数仅使用一次且逻辑不复杂,请直接在模板中内联:
svelte
<!-- 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
styling
skill.
有关通用 CSS 和 Tailwind 指南,请参考
styling
技能文档。

shadcn-svelte Best Practices

shadcn-svelte 最佳实践

Component Organization

组件组织

  • Use the CLI:
    bunx shadcn-svelte@latest add [component]
  • Each component in its own folder under
    $lib/components/ui/
    with an
    index.ts
    export
  • 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/svelte
):
typescript
// 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.,
more-vertical
,
minimize-2
), and you can name the import whatever you want (typically PascalCase with optional Icon suffix).
命名空间导入(推荐用于多部分组件):
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/svelte
单独导入):
typescript
// 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';
路径使用短横线命名法(如
more-vertical
minimize-2
),你可以随意命名导入(通常使用大驼峰命名,可选择性添加Icon后缀)。

Styling and Customization

样式与自定义

  • Always use the
    cn()
    utility from
    $lib/utils
    for combining Tailwind classes
  • Modify component code directly rather than overriding styles with complex CSS
  • Use
    tailwind-variants
    for component variant systems
  • Follow the
    background
    /
    foreground
    convention for colors
  • Leverage CSS variables for theme consistency
  • 始终使用
    $lib/utils
    中的
    cn()
    工具类来组合Tailwind类
  • 直接修改组件代码,而不是用复杂的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
type Props = {...}
declaration. Always inline the type directly in
$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
children
prop is implicitly typed in Svelte. Never annotate it:
svelte
<!-- 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中,
children
属性是隐式类型的。永远不要为它添加注解:
svelte
<!-- 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元素
  • 当你发现自己为了更新父组件状态而传递回调时
核心要点:实例化多个对话框(每行一个)是完全可行的,比管理单个共享对话框并处理复杂状态要好得多。现代框架能高效处理这种情况,代码的清晰度值得这样做。