name: frontend-component-patterns description: 构建可重用、可组合和可维护的React/Vue/Angular组件,遵循如复合组件、渲染属性、自定义钩子和高阶组件等已建立的设计模式。用于创建组件库、实现组件组合、构建可重用的UI元素、设计属性API、管理组件状态模式、实现受控与非受控组件、创建复合组件、使用渲染属性或子元素作为函数、构建自定义钩子,或建立组件架构标准。
前端组件模式 - 构建可重用的React组件
何时使用此技能
- 创建可重用的组件库
- 实现组件组合模式
- 构建灵活、可配置的UI组件
- 设计直观的组件属性API
- 使用模式管理组件状态
- 实现受控与非受控组件
- 创建复合组件(如标签页、手风琴)
- 使用渲染属性或子元素作为函数
- 构建用于共享逻辑的自定义React钩子
- 实现高阶组件(HOC)
- 建立组件架构标准
- 创建可访问、键盘可导航的组件
何时使用此技能
- 设计React组件架构、提高组件可重用性、管理状态或解决常见UI模式时。
- 在需要此专业知识的开发任务或功能中
使用时机: 设计React组件架构、提高组件可重用性、管理状态或解决常见UI模式时。
核心原则
- 组合优于继承 - 从简单组件构建复杂UI
- 单一职责 - 每个组件做好一件事
- 属性向下、事件向上 - 单向数据流
- 关注点分离 - 逻辑与表示分离
- 可访问性优先 - ARIA、键盘导航、语义HTML
组件模式
1. 表示型与容器型组件
// ❌ 混合关注点 - 逻辑和表示在一起
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>加载中...</div>;
return (
<div className="profile">
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
// ✅ 分离 - 容器处理逻辑
function UserProfileContainer({ userId }: { userId: string }) {
const { data: user, isLoading } = useUser(userId);
if (isLoading) return <LoadingSpinner />;
if (!user) return <NotFound />;
return <UserProfileView user={user} />;
}
// ✅ 表示型 - 纯显示组件
interface UserProfileViewProps {
user: {
avatar: string;
name: string;
bio: string;
};
}
function UserProfileView({ user }: UserProfileViewProps) {
return (
<div className="profile">
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
2. 复合组件
// ✅ 灵活、可组合的API
interface TabsProps {
children: React.ReactNode;
defaultValue?: string;
}
interface TabsContextValue {
activeTab: string;
setActiveTab: (value: string) => void;
}
const TabsContext = React.createContext<TabsContextValue | null>(null);
function Tabs({ children, defaultValue }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue || '');
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabsList({ children }: { children: React.ReactNode }) {
return <div className="tabs-list" role="tablist">{children}</div>;
}
function TabsTrigger({ value, children }: { value: string; children: React.ReactNode }) {
const context = useContext(TabsContext);
if (!context) throw new Error('TabsTrigger必须在Tabs内部');
const isActive = context.activeTab === value;
return (
<button
role="tab"
aria-selected={isActive}
onClick={() => context.setActiveTab(value)}
className={isActive ? 'active' : ''}
>
{children}
</button>
);
}
function TabsContent({ value, children }: { value: string; children: React.ReactNode }) {
const context = useContext(TabsContext);
if (!context) throw new Error('TabsContent必须在Tabs内部');
if (context.activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
// 导出为复合组件
export { Tabs, TabsList, TabsTrigger, TabsContent };
// 用法 - 灵活直观
function App() {
return (
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">账户</TabsTrigger>
<TabsTrigger value="password">密码</TabsTrigger>
<TabsTrigger value="notifications">通知</TabsTrigger>
</TabsList>
<TabsContent value="account">
<AccountSettings />
</TabsContent>
<TabsContent value="password">
<PasswordSettings />
</TabsContent>
<TabsContent value="notifications">
<NotificationSettings />
</TabsContent>
</Tabs>
);
}
3. 渲染属性模式
// ✅ 使用渲染属性实现灵活渲染
interface MousePositionProps {
children: (position: { x: number; y: number }) => React.ReactNode;
}
function MousePosition({ children }: MousePositionProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return <>{children(position)}</>;
}
// 用法 - 消费者控制渲染
function App() {
return (
<MousePosition>
{({ x, y }) => (
<div>
鼠标位置在 ({x}, {y})
</div>
)}
</MousePosition>
);
}
4. 自定义钩子(现代替代方案)
// ✅ 提取可重用逻辑为钩子
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return position;
}
// 用法 - 比渲染属性更简洁
function App() {
const { x, y } = useMousePosition();
return (
<div>
鼠标位置在 ({x}, {y})
</div>
);
}
// ✅ 更多自定义钩子示例
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
5. 高阶组件(传统模式)
// ✅ 用于添加功能的HOC
function withLoading<P extends object>(
Component: React.ComponentType<P>
) {
return function WithLoadingComponent(
props: P & { isLoading: boolean }
) {
if (props.isLoading) {
return <LoadingSpinner />;
}
return <Component {...props} />;
};
}
// 用法
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading users={users} isLoading={loading} />
// 注意:自定义钩子现在优先于HOC
状态管理模式
1. 属性与状态
// ✅ 属性 - 从父组件传递
interface ButtonProps {
label: string; // 显示文本
onClick: () => void; // 回调函数
disabled?: boolean; // 可选配置
}
function Button({ label, onClick, disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// ✅ 状态 - 组件内部
function Counter() {
const [count, setCount] = useState(0); // 本地状态
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
// ✅ 受控与非受控
// 受控 - 值来自属性(父组件控制)
function ControlledInput({ value, onChange }: {
value: string;
onChange: (value: string) => void;
}) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}
// 非受控 - 内部状态(组件控制)
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log(inputRef.current?.value);
};
return (
<>
<input ref={inputRef} defaultValue="初始值" />
<button onClick={handleSubmit}>提交</button>
</>
);
}
2. 状态提升
// ❌ 重复状态 - 兄弟组件无法通信
function ParentBad() {
return (
<>
<SearchBox /> {/* 有自己的搜索状态 */}
<ResultsList /> {/* 有自己的搜索状态 */}
</>
);
}
// ✅ 父组件中的共享状态
function ParentGood() {
const [searchQuery, setSearchQuery] = useState('');
return (
<>
<SearchBox query={searchQuery} onQueryChange={setSearchQuery} />
<ResultsList query={searchQuery} />
</>
);
}
3. 用于深层属性的上下文
// ❌ 属性传递 - 传递通过多个层级
function App() {
const [theme, setTheme] = useState('light');
return <Layout theme={theme} setTheme={setTheme} />;
}
function Layout({ theme, setTheme }) {
return <Sidebar theme={theme} setTheme={setTheme} />;
}
function Sidebar({ theme, setTheme }) {
return <ThemeToggle theme={theme} setTheme={setTheme} />;
}
// ✅ 上下文 - 在任何层级直接访问
interface ThemeContextValue {
theme: string;
setTheme: (theme: string) => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme必须在ThemeProvider内部');
return context;
}
// 用法 - 无属性传递
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换 {theme} 主题
</button>
);
}
性能优化
1. React.memo - 防止重新渲染
// ❌ 每次父组件渲染时重新渲染
function ExpensiveComponent({ data }: { data: string }) {
console.log('渲染中...');
return <div>{data}</div>;
}
// ✅ 仅当属性改变时重新渲染
const ExpensiveComponent = memo(function ExpensiveComponent({
data
}: {
data: string
}) {
console.log('渲染中...');
return <div>{data}</div>;
});
// ✅ 自定义比较函数
const ExpensiveList = memo(
function ExpensiveList({ items }: { items: Item[] }) {
return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
},
(prevProps, nextProps) => {
// 仅当项目数组长度改变时重新渲染
return prevProps.items.length === nextProps.items.length;
}
);
2. useMemo - 缓存昂贵计算
function ProductList({ products, filters }: { products: Product[]; filters: Filters }) {
// ❌ 每次渲染重新计算
const filteredProducts = products.filter(p => matchesFilters(p, filters));
// ✅ 仅当依赖项改变时重新计算
const filteredProducts = useMemo(() => {
return products.filter(p => matchesFilters(p, filters));
}, [products, filters]);
return (
<div>
{filteredProducts.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
3. useCallback - 稳定的函数引用
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次渲染创建新函数(导致子组件重新渲染)
const handleClick = () => {
console.log('点击了');
};
// ✅ 稳定的函数引用
const handleClick = useCallback(() => {
console.log('点击了');
}, []); // 无依赖 - 永不改变
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<MemoizedChild onClick={handleClick} />
</>
);
}
const MemoizedChild = memo(function Child({ onClick }: { onClick: () => void }) {
console.log('子组件渲染');
return <button onClick={onClick}>点击我</button>;
});
4. 代码分割与懒加载
// ✅ 懒加载重型组件
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<HeavyChart />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
);
}
可访问性模式
1. 语义HTML与ARIA
// ✅ 可访问按钮
function AccessibleButton({ label, onClick }: { label: string; onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
aria-label={label}
>
{label}
</button>
);
}
// ✅ 可访问模态框
function Modal({ isOpen, onClose, children }: {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
// 焦点捕获、ESC键处理等
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
className="modal-overlay"
onClick={onClose}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{children}
<button onClick={onClose} aria-label="关闭模态框">×</button>
</div>
</div>
);
}
// ✅ 可访问表单
function SignupForm() {
return (
<form>
<label htmlFor="email">邮箱</label>
<input
id="email"
type="email"
required
aria-required="true"
aria-describedby="email-error"
/>
<span id="email-error" role="alert" className="error">
请输入有效的邮箱
</span>
<button type="submit">注册</button>
</form>
);
}
2. 键盘导航
// ✅ 键盘可访问的下拉菜单
function Dropdown({ options }: { options: string[] }) {
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
setIsOpen(!isOpen);
} else if (e.key === 'ArrowDown') {
setSelectedIndex((i) => Math.min(i + 1, options.length - 1));
} else if (e.key === 'ArrowUp') {
setSelectedIndex((i) => Math.max(i - 1, 0));
} else if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div
role="combobox"
aria-expanded={isOpen}
aria-controls="dropdown-options"
tabIndex={0}
onKeyDown={handleKeyDown}
>
<button onClick={() => setIsOpen(!isOpen)}>
选择选项
</button>
{isOpen && (
<ul id="dropdown-options" role="listbox">
{options.map((opt, i) => (
<li
key={opt}
role="option"
aria-selected={i === selectedIndex}
className={i === selectedIndex ? 'selected' : ''}
>
{opt}
</li>
))}
</ul>
)}
</div>
);
}
组件设计检查清单
结构:
□ 每个组件单一职责
□ 表示型与容器型分离
□ 适当的属性类型(TypeScript)
□ 定义默认属性
□ 关键输入的属性验证
状态管理:
□ 状态在最低必要层级
□ 需要共享时提升状态
□ 上下文用于深层属性传递
□ 无属性突变
□ 表单使用受控组件
性能:
□ 用于昂贵组件的memo()
□ 用于昂贵计算的useMemo()
□ 用于稳定回调的useCallback()
□ 大型组件的代码分割
□ 路由的懒加载
可访问性:
□ 语义HTML元素
□ ARIA标签和角色
□ 键盘导航支持
□ 焦点管理
□ 屏幕阅读器测试
可重用性:
□ 通过属性可配置
□ 可与子元素组合
□ 无硬编码值
□ 清晰、有文档的API
□ 提供示例用法
资源
记住: 优秀的组件是简单、可重用、可访问且高效的。从简单开始,仅在需要时增加复杂性。