前端工程Technical Deep Dive
组件设计模式 —— 复合组件、高阶组件与 Render Props
发布时间2026/03/29
分类前端工程
预计阅读2 分钟
作者吴长龙
*
组件设计模式概述
01.内容
组件设计模式概述
好的组件设计能让代码更可复用、更易维护。本文介绍 React 中几种经典的组件设计模式。
一、复合组件 (Compound Components)
#### 什么是复合组件?
复合组件是一组协同工作的组件,父组件管理状态,子组件负责渲染:
jsx snippetjsx
// 复合组件示例:Tabs
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
Tabs.TabList = function TabList({ children }) {
return <div className="tab-list">{children}</div>;
};
Tabs.Tab = function Tab({ children, index }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isActive = index === activeIndex;
return (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
};
Tabs.Panel = function Panel({ children, index }) {
const { activeIndex } = useContext(TabsContext);
if (index !== activeIndex) return null;
return <div className="tab-panel">{children}</div>;
};
// 使用
function MyTabs() {
return (
<Tabs defaultIndex={0}>
<Tabs.TabList>
<Tabs.Tab index={0}>Tab 1</Tabs.Tab>
<Tabs.Tab index={1}>Tab 2</Tabs.Tab>
</Tabs.TabList>
<Tabs.Panel index={0}>Content 1</Tabs.Panel>
<Tabs.Panel index={1}>Content 2</Tabs.Panel>
</Tabs>
);
}#### 高级复合组件:Select
jsx snippetjsx
const SelectContext = createContext({});
function Select({ children, value, onChange }) {
const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(0);
const context = {
value,
onChange,
isOpen,
setIsOpen,
highlightedIndex,
setHighlightedIndex,
};
return (
<SelectContext.Provider value={context}>
<div className="select">{children}</div>
</SelectContext.Provider>
);
}
Select.Trigger = function Trigger({ children }) {
const { value, isOpen, setIsOpen } = useContext(SelectContext);
return (
<button
className={`select-trigger ${isOpen ? 'open' : ''}`}
onClick={() => setIsOpen(!isOpen)}
>
{value?.label || children || 'Select...'}
</button>
);
};
Select.Option = function Option({ children, value }) {
const { value: selectedValue, onChange, setIsOpen } = useContext(SelectContext);
const isSelected = selectedValue?.value === value?.value;
return (
<div
className={`select-option ${isSelected ? 'selected' : ''}`}
onClick={() => {
onChange(value);
setIsOpen(false);
}}
>
{children}
</div>
);
};
Select.List = function List({ children }) {
const { isOpen } = useContext(SelectContext);
if (!isOpen) return null;
return <div className="select-list">{children}</div>;
};
// 使用
<Select value={selectedOption} onChange={setSelectedOption}>
<Select.Trigger>Choose...</Select.Trigger>
<Select.List>
<Select.Option value={{ value: 'a', label: 'Option A' }}>Option A</Select.Option>
<Select.Option value={{ value: 'b', label: 'Option B' }}>Option B</Select.Option>
</Select.List>
</Select>二、高阶组件 (HOC)
#### 什么是 HOC?
高阶组件是接收组件并返回新组件的函数:
javascript snippetjavascript
// 基础 HOC 结构
function withHOC(WrappedComponent) {
return function EnhancedComponent(props) {
// 添加新功能
return <WrappedComponent {...props} />;
};
}#### 实际示例:日志 HOC
javascript snippetjavascript
function withLogger(WrappedComponent) {
return function LoggerWrapper(props) {
useEffect(() => {
console.log(`[${WrappedComponent.name}] mounted`);
return () => console.log(`[${WrappedComponent.name}] unmounted`);
}, []);
return <WrappedComponent {...props} />;
};
}
// 使用
const LoggedButton = withLogger(Button);
function App() {
return <LoggedButton onClick={() => {}}>Click me</LoggedButton>;
}#### 实际示例:权限 HOC
javascript snippetjavascript
function withAuth(WrappedComponent) {
return function AuthWrapper(props) {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
return <WrappedComponent {...props} user={user} />;
};
}
// 使用:保护路由
const ProtectedDashboard = withAuth(Dashboard);
function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<ProtectedDashboard />} />
</Routes>
);
}#### 实际示例:数据获取 HOC
javascript snippetjavascript
function withData(WrappedComponent, fetchFn) {
return function DataWrapper(props) {
const { data, loading, error, refetch } = useQuery({
queryKey: [fetchFn.name],
queryFn: fetchFn,
});
if (loading) return <Skeleton />;
if (error) return <Error error={error} />;
return (
<WrappedComponent
{...props}
data={data}
refetch={refetch}
/>
);
};
}
// 使用
const UserListWithData = withData(UserList, fetchUsers);
function App() {
return <UserListWithData />;
}#### HOC 链式组合
javascript snippetjavascript
// 组合多个 HOC
const EnhancedComponent = withLogger(
withAuth(
withData(WrappedComponent)
)
);
// 使用装饰器语法(需要配置)
@withLogger
@withAuth
@withData
class MyComponent extends React.Component {}三、Render Props
#### 什么是 Render Props?
render props 是共享代码的技术,通过 props 传递函数:
javascript snippetjavascript
// 基础 Render Props
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// 使用
<MouseTracker render={({ x, y }) => (
<div>Mouse at {x}, {y}</div>
)} />#### Render Props vs HOC
| 特性 | HOC | Render Props |
|---|---|---|
| 代码组织 | 包装组件 | 传递渲染函数 |
| Props | 透传困难 | 直接使用 |
| TypeScript | 需要泛型 | 更友好 |
| 组合 | 链式 | 嵌套 |
#### 实际示例:模态框
javascript snippetjavascript
function Modal({ isOpen, onClose, render }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{render({ onClose })}
</div>
</div>
);
}
// 使用
function App() {
return (
<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
render={({ onClose }) => (
<>
<h2>Confirm</h2>
<p>Are you sure?</p>
<button onClick={onClose}>Cancel</button>
<button onClick={handleConfirm}>OK</button>
</>
)}
/>
);
}四、自定义 Hook(现代替代)
javascript snippetjavascript
// Render Props 可以用 Hook 替代
function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
// ... same as before
}, []);
return position;
}
// 使用:更简洁
function App() {
const { x, y } = useMouse();
return <div>Mouse at {x}, {y}</div>;
}五、实践:构建一个图表组件
javascript snippetjavascript
// 复合组件方式
function Chart({ data }) {
return (
<ChartContext.Provider value={data}>
<div className="chart">
<Chart.Title />
<Chart.Legend />
<Chart.Canvas />
<Chart.Tooltip />
</div>
</ChartContext.Provider>
);
}
Chart.Title = function ChartTitle({ children }) {
return <h3 className="chart-title">{children}</h3>;
};
Chart.Legend = function ChartLegend() {
const { data } = useContext(ChartContext);
return (
<div className="chart-legend">
{data.map(item => (
<span key={item.label}>{item.label}</span>
))}
</div>
);
};
// 使用
<Chart data={chartData}>
<Chart.Title>销售趋势</Chart.Title>
<Chart.Legend />
<Chart.Canvas />
</Chart>总结
| 模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 复合组件 | 组件内部状态共享 | API 清晰、易理解 | 需要 Context |
| HOC | 逻辑复用、权限 | 纯函数、易组合 | Props 丢失、嵌套 |
| Render Props | 状态共享 | 灵活、透明 | 嵌套地狱 |
| Hook | 逻辑复用 | 简洁、可组合 | 需要 Hooks 规则 |
现代推荐:优先使用 Hooks,复杂场景用复合组件。HOC 和 Render Props 逐渐被 Hooks 取代。