🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx @anthropic-ai/skills install supercent-io/skills-template/ui-component-patterns
💡 提示:需要 Node.js 和 NPM
UI 组件模式
何时使用此技能
- 构建组件库:创建可复用的 UI 组件
- 实现设计系统:应用一致的 UI 模式
- 复杂 UI:需要多个变体的组件(按钮、模态框、下拉菜单)
- 重构:将重复代码提取到组件中
指示
步骤 1:Props API 设计
设计易于使用且可扩展的 Props。
原则:
- 命名清晰
- 合理的默认值
- 使用 TypeScript 进行类型定义
- 可选 Props 使用可选标记 (?)
示例(按钮):
interface ButtonProps {
// 必需
children: React.ReactNode;
// 可选(带默认值)
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
isLoading?: boolean;
// 事件处理器
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
// HTML 属性继承
type?: 'button' | 'submit' | 'reset';
className?: string;
}
function Button({
children,
variant = 'primary',
size = 'md',
disabled = false,
isLoading = false,
onClick,
type = 'button',
className = '',
...rest
}: ButtonProps) {
const baseClasses = 'btn';
const variantClasses = `btn-${variant}`;
const sizeClasses = `btn-${size}`;
const classes = `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`;
return (
<button
type={type}
className={classes}
disabled={disabled || isLoading}
onClick={onClick}
{...rest}
>
{isLoading ? <Spinner /> : children}
</button>
);
}
// 使用示例
<Button variant="primary" size="lg" onClick={() => alert('点击了!')}>
点击我
</Button>
步骤 2:组合模式
组合小组件以构建复杂的 UI。
示例(卡片):
// Card 组件(容器)
interface CardProps {
children: React.ReactNode;
className?: string;
}
function Card({ children, className = '' }: CardProps) {
return <div className={`card ${className}`}>{children}</div>;
}
// Card.Header
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>;
}
// Card.Body
function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>;
}
// Card.Footer
function CardFooter({ children }: { children: React.ReactNode }) {
return <div className="card-footer">{children}</div>;
}
// 复合组件模式
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
export default Card;
// 使用
import Card from './Card';
function ProductCard() {
return (
<Card>
<Card.Header>
<h3>产品名称</h3>
</Card.Header>
<Card.Body>
<img src="..." alt="产品" />
<p>这里是产品描述...</p>
</Card.Body>
<Card.Footer>
<button>加入购物车</button>
</Card.Footer>
</Card>
);
}
步骤 3:渲染 Props / 作为函数的子元素
一种用于灵活定制的模式。
示例(下拉菜单):
interface DropdownProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
onSelect: (item: T) => void;
placeholder?: string;
}
function Dropdown<T>({ items, renderItem, onSelect, placeholder }: DropdownProps<T>) {
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState<T | null>(null);
const handleSelect = (item: T) => {
setSelected(item);
onSelect(item);
setIsOpen(false);
};
return (
<div className="dropdown">
<button onClick={() => setIsOpen(!isOpen)}>
{selected ? renderItem(selected, -1) : placeholder || '选择...'}
</button>
{isOpen && (
<ul className="dropdown-menu">
{items.map((item, index) => (
<li key={index} onClick={() => handleSelect(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
)}
</div>
);
}
// 使用
interface User {
id: string;
name: string;
avatar: string;
}
function UserDropdown() {
const users: User[] = [...];
return (
<Dropdown
items={users}
placeholder="选择用户"
renderItem={(user) => (
<div className="user-item">
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
</div>
)}
onSelect={(user) => console.log('选中:', user)}
/>
);
}
步骤 4:使用自定义钩子分离逻辑
将 UI 与业务逻辑分离。
示例(模态框):
// hooks/useModal.ts
function useModal(initialOpen = false) {
const [isOpen, setIsOpen] = useState(initialOpen);
const open = useCallback(() => setIsOpen(true), []);
const close = useCallback(() => setIsOpen(false), []);
const toggle = useCallback(() => setIsOpen(prev => !prev), []);
return { isOpen, open, close, toggle };
}
// components/Modal.tsx
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, title, children }: ModalProps) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose} aria-label="关闭">×</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>
);
}
// 使用
function App() {
const { isOpen, open, close } = useModal();
return (
<>
<button onClick={open}>打开模态框</button>
<Modal isOpen={isOpen} onClose={close} title="我的模态框">
<p>这里是模态框内容...</p>
</Modal>
</>
);
}
步骤 5:性能优化
防止不必要的重新渲染。
React.memo:
// ❌ 反面示例:每次父组件重新渲染时,子组件都会重新渲染
function ExpensiveComponent({ data }) {
console.log('渲染...');
return <div>{/* 复杂 UI */}</div>;
}
// ✅ 好示例:仅在 props 变化时重新渲染
const ExpensiveComponent = React.memo(({ data }) => {
console.log('渲染...');
return <div>{/* 复杂 UI */}</div>;
});
useMemo & useCallback:
function ProductList({ products, category }: { products: Product[]; category: string }) {
// ✅ 记忆化过滤结果
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === category);
}, [products, category]);
// ✅ 记忆化回调
const handleAddToCart = useCallback((productId: string) => {
// 加入购物车
console.log('添加:', productId);
}, []);
return (
<div>
{filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
);
}
const ProductCard = React.memo(({ product, onAddToCart }) => {
return (
<div>
<h3>{product.name}</h3>
<button onClick={() => onAddToCart(product.id)}>加入购物车</button>
</div>
);
});
输出格式
组件文件结构
components/
├── Button/
│ ├── Button.tsx # 主要组件
│ ├── Button.test.tsx # 测试
│ ├── Button.stories.tsx # Storybook
│ ├── Button.module.css # 样式
│ └── index.ts # 导出
├── Card/
│ ├── Card.tsx
│ ├── CardHeader.tsx
│ ├── CardBody.tsx
│ ├── CardFooter.tsx
│ └── index.ts
└── Modal/
├── Modal.tsx
├── useModal.ts # 自定义钩子
└── index.ts
组件模板
import React from 'react';
export interface ComponentProps {
// Props 定义
children: React.ReactNode;
className?: string;
}
/**
* 组件描述
*
* @example
* ```tsx
* <Component>你好</Component>
* ```
*/
export const Component = React.forwardRef<HTMLDivElement, ComponentProps>(
({ children, className = '', ...rest }, ref) => {
return (
<div ref={ref} className={`component ${className}`} {...rest}>
{children}
</div>
);
}
);
Component.displayName = 'Component';
export default Component;
约束条件
必需规则(必须遵守)
- 单一职责原则:一个组件只有一个角色
- 按钮只处理按钮,表单只处理表单
- Props 类型定义:需要 TypeScript 接口
- 启用自动补全
- 类型安全
- 可访问性:aria-*、role、tabindex 等
禁止规则(不得违反)
- 过度 props 透传:禁止超过 5 层深度透传
- 使用 Context 或组合
- 无业务逻辑:禁止在 UI 组件中进行 API 调用和复杂计算
- 分离到自定义钩子中
- 内联对象/函数:性能下降
// ❌ 反面示例 <Component style={{ color: 'red' }} onClick={() => handleClick()} /> // ✅ 好示例 const style = { color: 'red' }; const handleClick = useCallback(() => {...}, []); <Component style={style} onClick={handleClick} />
示例
示例 1:手风琴(复合组件)
import React, { createContext, useContext, useState } from 'react';
// 使用 Context 共享状态
const AccordionContext = createContext<{
activeIndex: number | null;
setActiveIndex: (index: number | null) => void;
} | null>(null);
function Accordion({ children }: { children: React.ReactNode }) {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
return (
<AccordionContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ index, title, children }: {
index: number;
title: string;
children: React.ReactNode;
}) {
const context = useContext(AccordionContext);
if (!context) throw new Error('AccordionItem 必须在 Accordion 内部使用');
const { activeIndex, setActiveIndex } = context;
const isActive = activeIndex === index;
return (
<div className="accordion-item">
<button
className="accordion-header"
onClick={() => setActiveIndex(isActive ? null : index)}
aria-expanded={isActive}
>
{title}
</button>
{isActive && <div className="accordion-body">{children}</div>}
</div>
);
}
Accordion.Item = AccordionItem;
export default Accordion;
// 使用
<Accordion>
<Accordion.Item index={0} title="第 1 部分">
第 1 部分的内容
</Accordion.Item>
<Accordion.Item index={1} title="第 2 部分">
第 2 部分的内容
</Accordion.Item>
</Accordion>
示例 2:多态组件(as prop)
type PolymorphicComponentProps<C extends React.ElementType> = {
as?: C;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
function Text<C extends React.ElementType = 'span'>({
as,
children,
...rest
}: PolymorphicComponentProps<C>) {
const Component = as || 'span';
return <Component {...rest}>{children}</Component>;
}
// 使用
<Text>默认 span</Text>
<Text as="h1">标题 1</Text>
<Text as="p" style={{ color: 'blue' }}>段落</Text>
<Text as={Link} href="/about">链接</Text>
最佳实践
- 组合优于配置:利用子元素而不是大量 props
- 受控与非受控:根据情况选择
- 默认 Props:提供合理的默认值
- Storybook:组件文档和开发
参考资料
元数据
版本
- 当前版本:1.0.0
- 最后更新:2025-01-01
- 兼容平台:Claude, ChatGPT, Gemini
相关技能
- web-accessibility:可访问组件
- state-management:组件状态管理
标签
#UI组件 #React #设计模式 #组合 #TypeScript #前端
📄 原始文档
完整文档(英文):
https://skills.sh/supercent-io/skills-template/ui-component-patterns
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

评论(0)