components
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseComponents (Server & Client)
组件(Server & Client)
IMPORTANT: This skill defines architecture, rules, structure, component capabilities, constraints, and Value Object usage. It DOES NOT explain or restrict visual design or styling logic (e.g. CSS layout choices). You MAY use component libraries likeor similar inside your components.shadcn/ui
重要提示: 本指南定义了架构、规则、结构、组件能力、约束以及值对象(Value Object,VO)的使用方式。不解释或限制视觉设计或样式逻辑(如CSS布局选择)。你可以在组件中使用或类似的组件库。shadcn/ui
Server vs Client — Decision Rule
Server与Client组件——决策规则
If the component needs interactivity (hooks, events, browser APIs) → .
If not → Server Component (no directive needed).
"use client"If not → Server Component (no directive needed).
The key project-specific distinction is what each type can access:
| Capability | Server Component | Client Component |
|---|---|---|
| ✅ | ❌ |
Value Object instantiation ( | ✅ | ✅ |
| Receive Class Instances as parameters | ✅ | ❌ (MUST use |
| ✅ | ❌ |
| ✅ | ❌ — MUST use |
| ❌ | ✅ |
Server-only modules (drizzle, | ✅ | ❌ |
| ✅ | ❌ |
Default to Server Components. Push theboundary as low in the tree as possible."use client"
如果组件需要交互性(hooks、事件、浏览器API)→ 添加。
如果不需要→ Server Component(无需指令)。
"use client"如果不需要→ Server Component(无需指令)。
项目特有的核心区别在于两种组件可以访问的资源:
| 能力 | Server Component | Client Component |
|---|---|---|
| ✅ | ❌ |
值对象实例化( | ✅ | ✅ |
| 接收类实例作为参数 | ✅ | ❌(必须使用 |
| ✅ | ❌ |
| ✅ | ❌ — 必须使用 |
| ❌ | ✅ |
仅服务端模块(drizzle、 | ✅ | ❌ |
| ✅ | ❌ |
默认使用Server Component。尽可能将边界置于组件树的最底层。"use client"
File & Naming Conventions
文件与命名规范
- Name: matching the exported function —
PascalCase→LinksTable.tsxLinksTable - MUST be the absolute first line of the file when present — before any imports.
"use client" - One primary component per file. Small co-located sub-components used only in that file are acceptable.
- 命名:采用与导出函数名匹配 — 如
PascalCase对应LinksTable.tsxLinksTable - 当需要添加时,必须放在文件的第一行 — 位于所有导入语句之前。
"use client" - 每个文件对应一个主组件。允许在同一文件中放置仅在该组件内使用的小型子组件。
1. Value Objects in Components
1. 组件中的值对象(VO)
Server Components
Server Component
Server Components CAN explicitly receive, instantiate, and use Value Objects natively in their parameters and body. Since they never cross the network boundary as JSON payloads, they aren't subject to serialization limits.
Server Component可以在参数和代码中直接接收、实例化并使用值对象。由于它们不会以JSON payload形式跨网络传输,因此不受序列化限制。
Client Components
Client Component
Client Components CANNOT receive Value Objects as class instances via parameters from a Server Component. Next.js RSC serialization fails on classes. Client Components MUST receive primitive Branded Types via props.
However, a Client Component MAY instantiate or manipulate its own Value Objects internally (e.g., during form validation before submitting to a Server Action).
Client Component无法通过参数从Server Component接收值对象的类实例。Next.js RSC序列化对类实例会失效。Client Component必须通过props接收原始的品牌类型(Branded Types)。
不过,Client Component可以在内部自行实例化或处理值对象(例如,在提交到Server Action之前的表单验证过程中)。
2. Shared Library Components (shadcn
style)
shadcn2. 共享库组件(shadcn
风格)
shadcnCommon Components (cva
& cn
)
cvacn通用组件(cva
& cn
)
cvacnWhen building common, reusable UI components (e.g., Buttons, Badges, Inputs) that appear in multiple contexts and possess differing styles but identical underlying functionality, you MUST manage these styling variants using the (class-variance-authority) library and the utility. This directly mirrors the architectural style.
cvacnshadcn/ui当构建在多个场景中出现、样式不同但核心功能一致的通用可复用UI组件(如按钮、徽章、输入框)时,必须使用(class-variance-authority)库和工具管理这些样式变体。这与的架构风格完全一致。
cvacnshadcn/uiSimple Flat Composition
简单扁平组合
When designing complex reusable UI components (e.g., Table, DropdownMenu, Select), keep them as independent, flat components (e.g., , , , ). You MUST NOT use the dot-notation Compound Component pattern () for now to avoid unnecessary complexity. Each part should simply be imported and used as a distinct component.
TableTableRowTableBodyTableCellTable.Row在设计复杂的可复用UI组件(如表、下拉菜单、选择器)时,需将它们设计为独立的扁平组件(如、、、)。目前请勿使用点符号复合组件模式(),以避免不必要的复杂度。每个组件部分应作为独立组件导入并使用。
TableTableRowTableBodyTableCellTable.Row3. Logic-Based Responsive Separation
3. 基于逻辑的响应式拆分
If a component requires completely different React structural elements or rendering logic for Mobile screens versus Desktop screens (e.g., a Mobile user gets a swipe-up , and a Desktop user gets a centered ), you SHALL separate this into multiple distinct local components within the same file.
<Drawer><Dialog>For example, you MUST structure your file with:
- (The wrapper that measures
function ContentList())useIsMobile() function ContentListMobile()function ContentListDesktop()
This separation MUST be based on structural or lifecycle logic (), NOT merely on visual styling differences which belong in CSS (Tailwind classes).
{isMobile ? <Mobile> : <Desktop>}如果组件针对移动端和桌面端需要完全不同的React结构元素或渲染逻辑(例如,移动端用户看到上滑,桌面端用户看到居中),则应将其拆分为同一文件中的多个独立本地组件。
<Drawer><Dialog>例如,你必须按以下结构组织文件:
- (用于检测
function ContentList()的包装组件)useIsMobile() function ContentListMobile()function ContentListDesktop()
这种拆分必须基于结构或生命周期逻辑(),而非仅基于视觉样式差异(视觉样式差异应通过CSS/Tailwind类处理)。
{isMobile ? <Mobile> : <Desktop>}4. The Server → Client Serialization Boundary
4. Server → Client序列化边界
Next.js serializes props when passing them from a Server Component to a Client Component (RSC payload). Class instances are not serializable — this includes all Value Object classes. This is a transport constraint, not a general restriction: a Client Component can instantiate or use VOs internally just fine.
当从Server Component向Client Component传递props时,Next.js会对props进行序列化(RSC payload)。类实例不可序列化 — 包括所有值对象类。这是一个传输约束,而非通用限制:Client Component可以在内部正常实例化或使用VO。
The toBranded()
pattern
toBranded()toBranded()
模式
toBranded()VOs expose a method that returns a branded primitive (a plain or tagged with a unique TypeScript symbol). It solves two problems at once:
.toBranded()numberstring- Serializability — a branded primitive crosses the RSC boundary without issues.
- Validation guarantee — because can only be called on an already-constructed VO, the receiving Client Component knows the value has already been validated. There is no need to call
toBranded()again or handle.from()errors — the branded type itself is the proof that the value is valid.Result
typescript
// In the VO definition
declare const uniqueBrand: unique symbol;
export type BrandedFolderId = string & { readonly [uniqueBrand]: never };
export class FolderId {
// ...
toBranded(): BrandedFolderId {
return this.value as BrandedFolderId;
// Caller receives a plain string at runtime,
// but TypeScript knows it came from a valid FolderId.
}
}Server Component — calls before passing the prop:
.toBranded()tsx
// Server Component
import { FolderItem } from "./FolderItem"; // client component
export async function FolderList() {
const folders = /* from use case */;
return folders.map((folder) => (
<FolderItem
key={folder.getId().toBranded()}
folderId={folder.getId().toBranded()} // ✅ branded primitive
name={folder.getName().toBranded()} // ✅ plain string
/>
));
}Client Component — receives and uses the branded primitive:
tsx
"use client";
import { type BrandedFolderId } from "@/folders/domain/value-objects/folderId";
interface FolderItemProps {
folderId: BrandedFolderId; // branded primitive — valid by construction, no need to re-validate
name: string;
}
export function FolderItem({ folderId, name }: FolderItemProps) {
// folderId is a string at runtime, branded for type safety.
// Pass it directly to a server action — no .from() or error handling needed.
// The server action receives it as a string and reconstructs the VO internally.
}值对象暴露了方法,该方法返回一个品牌化原始值(带有唯一TypeScript标记的普通或)。它同时解决了两个问题:
.toBranded()numberstring- 可序列化 — 品牌化原始值可以无问题地跨越RSC边界。
- 验证保证 — 因为只能在已构建的VO上调用,接收方Client Component知道该值已通过验证。无需再次调用
.toBranded()或处理.from()错误 — 品牌类型本身就是值有效的证明。Result
typescript
// 在VO定义中
declare const uniqueBrand: unique symbol;
export type BrandedFolderId = string & { readonly [uniqueBrand]: never };
export class FolderId {
// ...
toBranded(): BrandedFolderId {
return this.value as BrandedFolderId;
// 运行时调用方收到普通字符串,
// 但TypeScript知道它来自有效的FolderId。
}
}Server Component — 在传递props前调用:
.toBranded()tsx
// Server Component
import { FolderItem } from "./FolderItem"; // client component
export async function FolderList() {
const folders = /* 来自用例 */;
return folders.map((folder) => (
<FolderItem
key={folder.getId().toBranded()}
folderId={folder.getId().toBranded()} // ✅ 品牌化原始值
name={folder.getName().toBranded()} // ✅ 普通字符串
/>
));
}Client Component — 接收并使用品牌化原始值:
tsx
"use client";
import { type BrandedFolderId } from "@/folders/domain/value-objects/folderId";
interface FolderItemProps {
folderId: BrandedFolderId; // 品牌化原始值 — 构建时已验证,无需重新验证
name: string;
}
export function FolderItem({ folderId, name }: FolderItemProps) {
// 运行时folderId是字符串,通过品牌化实现类型安全。
// 直接传递给Server Action — 无需调用.from()或处理错误。
// Server Action接收字符串并在内部重建VO。
}What can cross the boundary
可跨越边界的类型
| Type | Serializable | How |
|---|---|---|
| ✅ | Directly |
Plain objects | ✅ | Directly (no class methods) |
| Arrays of the above | ✅ | Directly |
| ✅ | Passed as-is (Next.js handles it) |
| VO class instance | ❌ | Use |
| Functions / callbacks | ❌ | Not serializable — use Server Actions instead |
| ⚠️ | Omit the prop or use |
| 类型 | 是否可序列化 | 方式 |
|---|---|---|
| ✅ | 直接传递 |
普通对象 | ✅ | 直接传递(无类方法) |
| 上述类型的数组 | ✅ | 直接传递 |
| ✅ | 直接传递(Next.js会处理) |
| VO类实例 | ❌ | 使用 |
| 函数/回调 | ❌ | 不可序列化 — 使用Server Action替代 |
| ⚠️ | 省略该props或使用 |
4. Composition Patterns
4. 组合模式
Server renders Client (standard)
Server渲染Client(标准方式)
A Server Component can render a Client Component and pass props. Props must be serializable (see table above):
tsx
// ServerPage.tsx (server)
import { ClientWidget } from "./ClientWidget";
export default async function ServerPage() {
const data = await fetchData();
return <ClientWidget items={data.map((d) => ({ id: d.getId().toBranded(), name: d.getName().toBranded() }))} />;
}Server Component可以渲染Client Component并传递props。props必须可序列化(见上表):
tsx
// ServerPage.tsx (server)
import { ClientWidget } from "./ClientWidget";
export default async function ServerPage() {
const data = await fetchData();
return <ClientWidget items={data.map((d) => ({ id: d.getId().toBranded(), name: d.getName().toBranded() }))} />;
}Client renders Server via children
(slot pattern)
childrenClient通过children
渲染Server(插槽模式)
childrenA Client Component cannot import Server Components. But a Server Component can pass Server Components as to a Client Component:
childrentsx
// Layout.tsx (server)
import { Sidebar } from "./Sidebar"; // client component
import { UserProfile } from "./UserProfile"; // server component
export default async function Layout({ children }: { children: ReactNode }) {
return (
<Sidebar>
<UserProfile /> {/* server component passed as children — valid */}
{children}
</Sidebar>
);
}tsx
// Sidebar.tsx (client)
"use client";
export function Sidebar({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(true);
return <aside>{children}</aside>; // renders server components correctly
}Rule: Client Components must neverServer-only modules. Pass server-rendered content viaimportor other slot props instead.children
Client Component 无法导入Server Component。但Server Component可以将Server Component作为传递给Client Component:
childrentsx
// Layout.tsx (server)
import { Sidebar } from "./Sidebar"; // client component
import { UserProfile } from "./UserProfile"; // server component
export default async function Layout({ children }: { children: ReactNode }) {
return (
<Sidebar>
<UserProfile /> {/* 作为children传递的server组件 — 合法 */}
{children}
</Sidebar>
);
}tsx
// Sidebar.tsx (client)
"use client";
export function Sidebar({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(true);
return <aside>{children}</aside>; // 正确渲染server组件
}规则:Client Component绝不能仅服务端模块。通过import或其他插槽props传递服务端渲染的内容。children
Pushing the client boundary down
降低Client边界层级
Keep the boundary as low in the tree as possible. Extract only the interactive part into a Client Component and keep the rest as a Server Component:
"use client"tsx
// ✅ Only the button is a client component
export async function FolderCard({ folder }: Props) {
return (
<div>
<h2>{folder.getName().toBranded()}</h2>
<DeleteFolderButton folderId={folder.getId().toBranded()} /> {/* client */}
</div>
);
}
// ❌ The entire card becomes client just because of one button
"use client";
export function FolderCard({ folder }: Props) { ... }尽可能将边界置于组件树的最低层。仅将交互部分提取为Client Component,其余部分保留为Server Component:
"use client"tsx
// ✅ 仅按钮是client组件
export async function FolderCard({ folder }: Props) {
return (
<div>
<h2>{folder.getName().toBranded()}</h2>
<DeleteFolderButton folderId={folder.getId().toBranded()} /> {/* client */}
</div>
);
}
// ❌ 仅因为一个按钮,整个卡片变成client组件
"use client";
export function FolderCard({ folder }: Props) { ... }5. Props Interface Rules
5. Props接口规则
- Always define an explicit or
interfacefor component props — never inline complex shapes or usetype.any - Export the props type if other components or tests need it.
- VO class instances MUST NOT appear in Client Component props received from a Server Component — use the branded primitive type. A Client Component may, however, instantiate or work with VOs internally.
tsx
// ✅ Correct — serializable props for data coming from a Server Component
interface LinksTableProps {
links: {
id: BrandedUUID;
name: BrandedLinkName;
shortLink: BrandedShortLink;
destination: BrandedDestination;
clicks: BrandedNonNegativeInteger;
}[];
}
// ❌ Wrong — VO class in props passed from a Server Component (not serializable)
interface LinksTableProps {
links: ShortUrl[]; // ShortUrl is a class — cannot cross the RSC boundary
}- 始终为组件props定义明确的或
interface— 绝不要内联复杂结构或使用type。any - 如果其他组件或测试需要,导出props类型。
- VO类实例绝不能出现在从Server Component接收的Client Componentprops中 — 使用品牌化原始类型。不过,Client Component可以在内部实例化或处理VO。
tsx
// ✅ 正确 — 从Server Component传递的可序列化props
interface LinksTableProps {
links: {
id: BrandedUUID;
name: BrandedLinkName;
shortLink: BrandedShortLink;
destination: BrandedDestination;
clicks: BrandedNonNegativeInteger;
}[];
}
// ❌ 错误 — 从Server Component传递的props中包含VO类(不可序列化)
interface LinksTableProps {
links: ShortUrl[]; // ShortUrl是类 — 无法跨越RSC边界
}Checklist
检查清单
Server Components
Server Component
- No directive
"use client" - Function is
async - Locale via
await determineLocale() - Data fetched via use cases
serviceContainer - VO values passed to Client Components via , never as class instances
.toBranded()
- 无指令
"use client" - 函数为
async - 通过获取Locale
await determineLocale() - 通过用例获取数据
serviceContainer - 传递给Client Component的VO值使用,绝不是类实例
.toBranded()
Client Components
Client Component
- is the first line of the file — before any imports
"use client" - Function is not
async - Locale via — never
useLocale()determineLocale() - Mutations call a Server Action then — never
router.refresh()window.location.reload() - No imports of server-only modules (drizzle, ,
load-env, etc.)auth.ts - Props received from Server Components use branded primitives or plain values — never VO class instances
- Explicit props defined and named
interface
- 位于文件第一行 — 所有导入语句之前
"use client" - 函数不是
async - 通过获取Locale — 绝不用
useLocale()determineLocale() - 变更操作调用Server Action后执行— 绝不用
router.refresh()window.location.reload() - 不导入仅服务端模块(drizzle、、
load-env等)auth.ts - 从Server Component接收的props使用品牌化原始值或普通值 — 绝不是VO类实例
- 定义并命名明确的props
interface
Boundary
边界
- All props crossing Server → Client are serializable (strings, numbers, booleans, plain objects, arrays, Date)
- IDs passed across the boundary use — branded type signals the value is already validated
.toBranded() - boundary pushed as low in the tree as possible
"use client" - Server content passed into Client Components via slot, not by importing Server Components
children
- 所有跨Server→Client的props均可序列化(字符串、数字、布尔值、普通对象、数组、Date)
- 跨边界传递的ID使用— 品牌类型表明值已验证
.toBranded() - 边界尽可能置于组件树最低层
"use client" - 服务端内容通过插槽传递给Client Component,而非导入Server Component
children
7. References
7. 参考资料
For practical examples of components adhering to these rules, see the directory:
.agents/skills/components/references/- server-component-i18n.md
- server-component.md
- client-component-i18n.md
- client-component.md
- responsive-component.md
如需符合这些规则的组件实用示例,请查看目录:
.agents/skills/components/references/- server-component-i18n.md
- server-component.md
- client-component-i18n.md
- client-component.md
- responsive-component.md