ui-component-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUI Component Patterns
UI组件设计模式
When to use this skill
何时使用此技能
- 컴포넌트 라이브러리 구축: 재사용 가능한 UI 컴포넌트 제작
- 디자인 시스템 구현: 일관된 UI 패턴 적용
- 복잡한 UI: 여러 변형이 필요한 컴포넌트 (Button, Modal, Dropdown)
- 리팩토링: 중복 코드를 컴포넌트로 추출
- 组件库构建:制作可复用的UI组件
- 设计系统实现:应用统一的UI模式
- 复杂UI:需要多种变体的组件(Button、Modal、Dropdown)
- 代码重构:将重复代码提取为组件
Instructions
操作指南
Step 1: Props API 설계
步骤1:Props API设计
사용하기 쉽고 확장 가능한 Props를 설계합니다.
원칙:
- 명확한 이름
- 합리적인 기본값
- TypeScript로 타입 정의
- 선택적 Props는 optional (?)
예시 (Button):
tsx
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('Clicked!')}>
Click Me
</Button>设计易用且可扩展的Props。
原则:
- 名称清晰
- 合理的默认值
- 使用TypeScript定义类型
- 可选Props标记为optional(?)
示例(Button):
tsx
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('Clicked!')}>
Click Me
</Button>Step 2: Composition Pattern (합성 패턴)
步骤2:组合模式(Composition Pattern)
작은 컴포넌트를 조합하여 복잡한 UI를 만듭니다.
예시 (Card):
tsx
// Card 컴포넌트 (Container)
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>;
}
// Compound Component 패턴
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
export default Card;
// 사용
import Card from './Card';
function ProductCard() {
return (
<Card>
<Card.Header>
<h3>Product Name</h3>
</Card.Header>
<Card.Body>
<img src="..." alt="Product" />
<p>Product description here...</p>
</Card.Body>
<Card.Footer>
<button>Add to Cart</button>
</Card.Footer>
</Card>
);
}通过组合小型组件来构建复杂UI。
示例(Card):
tsx
// 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>Product Name</h3>
</Card.Header>
<Card.Body>
<img src="..." alt="Product" />
<p>Product description here...</p>
</Card.Body>
<Card.Footer>
<button>Add to Cart</button>
</Card.Footer>
</Card>
);
}Step 3: Render Props / Children as Function
步骤3:Render Props / 子组件作为函数
유연한 커스터마이징을 위한 패턴입니다.
예시 (Dropdown):
tsx
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 || 'Select...'}
</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="Select a user"
renderItem={(user) => (
<div className="user-item">
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
</div>
)}
onSelect={(user) => console.log('Selected:', user)}
/>
);
}用于灵活定制的模式。
示例(Dropdown):
tsx
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 || 'Select...'}
</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="Select a user"
renderItem={(user) => (
<div className="user-item">
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
</div>
)}
onSelect={(user) => console.log('Selected:', user)}
/>
);
}Step 4: Custom Hooks로 로직 분리
步骤4:使用自定义Hook分离逻辑
UI와 비즈니스 로직을 분리합니다.
예시 (Modal):
tsx
// 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="Close">×</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>
);
}
// 사용
function App() {
const { isOpen, open, close } = useModal();
return (
<>
<button onClick={open}>Open Modal</button>
<Modal isOpen={isOpen} onClose={close} title="My Modal">
<p>Modal content here...</p>
</Modal>
</>
);
}分离UI与业务逻辑。
示例(Modal):
tsx
// 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="Close">×</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>
);
}
// 使用
function App() {
const { isOpen, open, close } = useModal();
return (
<>
<button onClick={open}>Open Modal</button>
<Modal isOpen={isOpen} onClose={close} title="My Modal">
<p>Modal content here...</p>
</Modal>
</>
);
}Step 5: 성능 최적화
步骤5:性能优化
불필요한 리렌더링을 방지합니다.
React.memo:
tsx
// ❌ 나쁜 예: 부모가 리렌더링될 때마다 자식도 리렌더링
function ExpensiveComponent({ data }) {
console.log('Rendering...');
return <div>{/* 복잡한 UI */}</div>;
}
// ✅ 좋은 예: props가 변경될 때만 리렌더링
const ExpensiveComponent = React.memo(({ data }) => {
console.log('Rendering...');
return <div>{/* 복잡한 UI */}</div>;
});useMemo & useCallback:
tsx
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('Adding:', 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)}>Add to Cart</button>
</div>
);
});避免不必要的重渲染。
React.memo:
tsx
// ❌ 不良示例:父组件重渲染时子组件也会重渲染
function ExpensiveComponent({ data }) {
console.log('Rendering...');
return <div>{/* 复杂UI */}</div>;
}
// ✅ 良好示例:仅在Props变化时重渲染
const ExpensiveComponent = React.memo(({ data }) => {
console.log('Rendering...');
return <div>{/* 复杂UI */}</div>;
});useMemo & useCallback:
tsx
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('Adding:', 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)}>Add to Cart</button>
</div>
);
});Output format
输出格式
컴포넌트 파일 구조
组件文件结构
components/
├── Button/
│ ├── Button.tsx # 메인 컴포넌트
│ ├── Button.test.tsx # 테스트
│ ├── Button.stories.tsx # Storybook
│ ├── Button.module.css # 스타일
│ └── index.ts # Export
├── Card/
│ ├── Card.tsx
│ ├── CardHeader.tsx
│ ├── CardBody.tsx
│ ├── CardFooter.tsx
│ └── index.ts
└── Modal/
├── Modal.tsx
├── useModal.ts # Custom hook
└── index.tscomponents/
├── 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 # 自定义Hook
└── index.ts컴포넌트 템플릿
组件模板
tsx
import React from 'react';
export interface ComponentProps {
// Props 정의
children: React.ReactNode;
className?: string;
}
/**
* Component description
*
* @example
* ```tsx
* <Component>Hello</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;tsx
import React from 'react';
export interface ComponentProps {
// Props定义
children: React.ReactNode;
className?: string;
}
/**
* 组件描述
*
* @example
* ```tsx
* <Component>Hello</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;Constraints
约束条件
필수 규칙 (MUST)
必须遵守的规则(MUST)
-
단일 책임 원칙: 한 컴포넌트는 하나의 역할만
- Button은 버튼만, Form은 폼만
-
Props 타입 정의: TypeScript interface 필수
- 자동완성 지원
- 타입 안정성
-
접근성 고려: aria-*, role, tabindex 등
-
单一职责原则:一个组件仅承担一项职责
- Button只做按钮,Form只做表单
-
Props类型定义:必须使用TypeScript interface
- 支持自动补全
- 保证类型安全
-
考虑可访问性:使用aria-*、role、tabindex等属性
금지 사항 (MUST NOT)
禁止事项(MUST NOT)
-
과도한 props drilling: 5단계 이상 금지
- Context 또는 Composition 사용
-
비즈니스 로직 포함: UI 컴포넌트에 API 호출, 복잡한 계산 금지
- Custom hooks로 분리
-
inline 객체/함수: 성능 저하tsx
// ❌ 나쁜 예 <Component style={{ color: 'red' }} onClick={() => handleClick()} /> // ✅ 좋은 예 const style = { color: 'red' }; const handleClick = useCallback(() => {...}, []); <Component style={style} onClick={handleClick} />
-
过度Props透传:禁止超过5层的Props透传
- 使用Context或组合模式替代
-
包含业务逻辑:UI组件中禁止包含API调用、复杂计算
- 使用自定义Hook分离
-
内联对象/函数:会导致性能下降tsx
// ❌ 不良示例 <Component style={{ color: 'red' }} onClick={() => handleClick()} /> // ✅ 良好示例 const style = { color: 'red' }; const handleClick = useCallback(() => {...}, []); <Component style={style} onClick={handleClick} />
Examples
示例
예시 1: Accordion (Compound Component)
示例1:手风琴组件(复合组件)
tsx
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 must be used within 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="Section 1">
Content for section 1
</Accordion.Item>
<Accordion.Item index={1} title="Section 2">
Content for section 2
</Accordion.Item>
</Accordion>tsx
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 must be used within 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="Section 1">
Content for section 1
</Accordion.Item>
<Accordion.Item index={1} title="Section 2">
Content for section 2
</Accordion.Item>
</Accordion>예시 2: Polymorphic Component (as prop)
示例2:多态组件(as prop)
tsx
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>Default span</Text>
<Text as="h1">Heading 1</Text>
<Text as="p" style={{ color: 'blue' }}>Paragraph</Text>
<Text as={Link} href="/about">Link</Text>tsx
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>Default span</Text>
<Text as="h1">Heading 1</Text>
<Text as="p" style={{ color: 'blue' }}>Paragraph</Text>
<Text as={Link} href="/about">Link</Text>Best practices
最佳实践
- Composition over Props: 많은 props 대신 children 활용
- Controlled vs Uncontrolled: 상황에 맞게 선택
- Default Props: 합리적인 기본값 제공
- Storybook: 컴포넌트 문서화 및 개발
- 优先组合而非Props:多使用children而非大量Props
- 受控与非受控组件:根据场景选择合适的类型
- 默认Props:提供合理的默认值
- Storybook:用于组件文档化与开发
References
参考资料
- React Patterns
- Compound Components
- Radix UI - Accessible components
- Chakra UI - Component library
- shadcn/ui - Copy-paste components
- React Patterns
- Compound Components
- Radix UI - 可访问组件库
- Chakra UI - 组件库
- shadcn/ui - 可复制粘贴的组件
Metadata
元数据
버전
版本
- 현재 버전: 1.0.0
- 최종 업데이트: 2025-01-01
- 호환 플랫폼: Claude, ChatGPT, Gemini
- 当前版本:1.0.0
- 最后更新:2025-01-01
- 兼容平台:Claude、ChatGPT、Gemini
관련 스킬
相关技能
- web-accessibility: 접근 가능한 컴포넌트
- state-management: 컴포넌트 상태 관리
- web-accessibility:可访问组件
- state-management:组件状态管理
태그
标签
#UI-components#React#design-patterns#composition#TypeScript#frontend#UI-components#React#design-patterns#composition#TypeScript#frontend