202 lines
5.6 KiB
Markdown
202 lines
5.6 KiB
Markdown
|
|
---
|
|||
|
|
tags: [React, HOC, Render Props, Pattern, Frontend]
|
|||
|
|
create time: 2026-04-29 22:11
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# HOC 与 Render Props
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中复用它逻辑的两种主要模式。理解它们的原理、适用场景和局限,有助于阅读遗留代码并理解为什么 Hooks 成为更优解。
|
|||
|
|
|
|||
|
|
## HOC(高阶组件)
|
|||
|
|
|
|||
|
|
### 概念
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
A["原始组件 Component"] -->|"注入 props"| B["HOC 函数"]
|
|||
|
|
B --> C["增强组件 EnhancedComponent"]
|
|||
|
|
C --> D["获得额外能力:日志/权限/数据"]
|
|||
|
|
|
|||
|
|
style B fill:#F5A87D,color:#000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
HOC 是一个**函数**,接收组件作为参数,返回增强后的新组件:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 基础模式
|
|||
|
|
function withAuth<P extends object>(
|
|||
|
|
WrappedComponent: React.ComponentType<P>
|
|||
|
|
) {
|
|||
|
|
return function WithAuth(props: P) {
|
|||
|
|
const user = useAuth();
|
|||
|
|
if (!user) return <Navigate to="/login" replace />;
|
|||
|
|
return <WrappedComponent {...props} />;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用:装饰器风格
|
|||
|
|
const ProtectedPage = withAuth(function Dashboard() {
|
|||
|
|
return <h1>Dashboard</h1>;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// TS 类型推导:保留原始 props
|
|||
|
|
interface Props { title: string }
|
|||
|
|
const StyledTitle = withTheme(function Title({ title }: Props) {
|
|||
|
|
return <h1>{title}</h1>;
|
|||
|
|
}); // ✅ TS 推断 Props 不变
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 常见 HOC 模式
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 1. 日志/HUD
|
|||
|
|
function withLogger<P extends object>(Comp: React.FC<P>): React.FC<P> {
|
|||
|
|
return function LoggedComponent(props: P) {
|
|||
|
|
useEffect(() => console.log(`${Comp.name} mounted`), []);
|
|||
|
|
return <Comp {...props} />;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 加载态封装
|
|||
|
|
function withLoading<P extends object>(
|
|||
|
|
Comp: React.FC<P>,
|
|||
|
|
fetchData: () => Promise<void>
|
|||
|
|
): React.FC<P> {
|
|||
|
|
return function LoadingWrapper(props: P) {
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
useEffect(() => { fetchData().then(() => setLoading(false)); }, []);
|
|||
|
|
return loading ? <Spinner /> : <Comp {...props} />;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Props 转换(驼峰→kebab)
|
|||
|
|
function withPropsTransformer<P extends object>(
|
|||
|
|
Comp: React.FC<P>,
|
|||
|
|
transform: (p: P) => Record<string, unknown>
|
|||
|
|
): React.FC {
|
|||
|
|
return function Transformed(props: P) {
|
|||
|
|
const extra = transform(props);
|
|||
|
|
return <Comp {...props as any} {...extra} />;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### HOC 的局限性
|
|||
|
|
|
|||
|
|
| 问题 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| **Static 属性丢失** | `withAuth(Page)` 返回的新组件没有 Page.getInitialProps |
|
|||
|
|
| **Wrapper Hell** | `withAuth(withLogging(withData(Page)))`——嵌套过深调试困难 |
|
|||
|
|
| **Props 冲突** | 多个 HOC 都注入 `data` prop,后一个覆盖前一个 |
|
|||
|
|
| **ref 丢失** | 直接传 ref 给增强组件会报错(需用 forwardRef 包装) |
|
|||
|
|
|
|||
|
|
> [!tip] HOC 兼容 ref
|
|||
|
|
> ```tsx
|
|||
|
|
> export function withAuth<P>(Wrapped: React.ComponentType<P>) {
|
|||
|
|
> return React.forwardRef<HTMLDivElement, P>((props, ref) => {
|
|||
|
|
> const { authenticated } = useAuth();
|
|||
|
|
> if (!authenticated) return <Navigate to="/login" />;
|
|||
|
|
> return <Wrapped {...props} ref={ref} />;
|
|||
|
|
> });
|
|||
|
|
> }
|
|||
|
|
> ```
|
|||
|
|
|
|||
|
|
## Render Props
|
|||
|
|
|
|||
|
|
### 核心思想
|
|||
|
|
|
|||
|
|
通过 prop 传递一个**函数**,该函数返回 JSX——将 UI 渲染逻辑委托给调用方。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
interface MouseTrackerProps {
|
|||
|
|
render: (position: { x: number; y: number }) => React.ReactNode;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function MouseTracker({ render }: MouseTrackerProps) {
|
|||
|
|
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
|
|||
|
|
window.addEventListener("mousemove", handler);
|
|||
|
|
return () => window.removeEventListener("mousemove", handler);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 🎯 关键:render 函数返回什么就渲染什么
|
|||
|
|
return <div>{render(pos)}</div>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用
|
|||
|
|
<MouseTracker render={({ x, y }) => (
|
|||
|
|
<div>Cursor at ({x}, {y})</div>
|
|||
|
|
)} />;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 等价于 children 的情况
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 当 render prop 只是把数据传给 children 时,可以用 children 替代
|
|||
|
|
function DataProvider({ children }: { children: (data: Data) => React.ReactNode }) {
|
|||
|
|
const data = useDatabase();
|
|||
|
|
return <>{children(data)}</>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
<DataProvider>
|
|||
|
|
{(data) => <Table items={data.items} />}
|
|||
|
|
</DataProvider>;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## HOC vs Render Props vs Custom Hook
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TB
|
|||
|
|
A["逻辑复用需求"] --> B["方案对比"]
|
|||
|
|
|
|||
|
|
B --> C["HOC"]
|
|||
|
|
B --> D["Render Props"]
|
|||
|
|
B --> E["Custom Hook"]
|
|||
|
|
|
|||
|
|
C --> F["⚠️ Wrapper 嵌套深"]
|
|||
|
|
C --> G["⚠️ 静态方法丢失"]
|
|||
|
|
C --> H["✅ 不修改原组件结构"]
|
|||
|
|
|
|||
|
|
D --> I["⚠️ 回调地狱"]
|
|||
|
|
D --> J["⚠️ Prop 命名冲突风险"]
|
|||
|
|
D --> K["✅ 灵活的 UI 控制"]
|
|||
|
|
|
|||
|
|
E --> L["✅ 简洁直观"]
|
|||
|
|
E --> M["✅ 可直接操作 state / effect"]
|
|||
|
|
E --> N["✅ 无 wrapper 嵌套"]
|
|||
|
|
E --> O["❌ 只能用于组件内部"]
|
|||
|
|
|
|||
|
|
style L fill:#4FC08D,color:#fff
|
|||
|
|
style M fill:#4FC08D,color:#fff
|
|||
|
|
style N fill:#4FC08D,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 为什么 Hooks 取代了它们?
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// ❌ HOC 方式
|
|||
|
|
const ConnectedUserList = withAuth(withCache(withPagination(UserList)));
|
|||
|
|
// 三层嵌套 → 调试困难、性能不可见、type 推导混乱
|
|||
|
|
|
|||
|
|
// ✅ Hook 方式
|
|||
|
|
function UserList() {
|
|||
|
|
useAuth(); // 身份验证
|
|||
|
|
const cache = useCache<User[]>(); // 数据缓存
|
|||
|
|
const pagination = usePagination(); // 分页管理
|
|||
|
|
|
|||
|
|
return <table>{/* ... */}</table>;
|
|||
|
|
}
|
|||
|
|
// 扁平可读、天然共享 state、TS 完美推断
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] HOC 和 Render Props 真的被淘汰了吗?
|
|||
|
|
> - HOC:在需要**包裹**组件但不修改其内部的场景仍有价值(如第三方库封装)
|
|||
|
|
> - Render Props:当父组件需要**完全控制子组件的渲染内容**时仍然有用
|
|||
|
|
> - 但 90%+ 的场景,Custom Hook 是更好的选择
|
|||
|
|
|
|||
|
|
## 关联笔记
|