This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md
T
2026-04-29 23:36:32 +08:00

12 KiB
Raw Blame History

tags, create time
tags create time
React
HOC
Render Props
Pattern
Frontend
2026-04-29 22:11

HOC 与 Render Props

概述

[!question] 思考:如果两个组件需要共享同一套数据获取逻辑,该怎么设计?

Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中复用状态逻辑的两种主要模式。理解它们的原理、适用场景和局限,不仅有助于阅读遗留代码,更能帮你深刻理解为什么 Hooks 成为更优解——以及哪些场景下旧模式依然不可替代。

维度 HOC Render Props Custom Hook
本质 函数包裹组件 prop 传递渲染回调 纯函数抽离逻辑
侵入性 不改动原组件 JSX 需修改 JSX 调用处 零侵入,仅在 hooks 内使用
性能影响 额外 wrapper 层 同上 无额外组件树开销

HOC(高阶组件)

概念

[!question] 类比理解:如果把组件看作"数据 → UI"的转换器,HOC 就是在输入输出之间插入了一层"调料"。

graph LR
    A["原始组件 Component"] -->|"注入 props"| B["HOC 函数"]
    B --> C["增强组件 EnhancedComponent"]
    C --> D["获得额外能力:日志/权限/数据"]
    
    style B fill:#F5A87D,color:#000

HOC 是一个函数,接收组件作为参数,返回增强后的新组件:

// 基础模式
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 不变

[!note] 核心原理 HOC 本质上是闭包 + 组合。WithAuth 作为一个新的函数组件,可以正常使用 Hooks(因为它是组件),而包裹的 WrappedComponent 只负责渲染。这种方式不修改原组件的任何代码,仅通过 props 传递增强信息。

关键细节:displayName

调试嵌套 HOC 时,React DevTools 会显示一层层无意义的 wrapper 名。可以用 displayName 让调试更清晰:

export function withAuth<P>(Wrapped: React.ComponentType<P>) {
  const WithAuth: React.FC<P> = (props) => {
    // ... 认证逻辑
    return <Wrapped {...props} />;
  };
  WithAuth.displayName = `withAuth(${Wrapped.name || 'Component'})`;
  return WithAuth;
}
// DevTools 中显示:withAuth(Dashboard) → 一目了然

常见 HOC 模式

1. 日志 / HUD

// 通用:为组件自动记录生命周期事件
function withLogger<P extends object>(Comp: React.FC<P>): React.FC<P> {
  return function LoggedComponent(props: P) {
    useEffect(() => console.log(`[Mount] ${Comp.name || 'Anonymous'}`), []);
    return <Comp {...props} />;
  };
}

[!tip] 解释 不传任何额外 prop,只"旁路"添加副作用(如日志)。这是 HOC 最轻量的用法——原组件完全不知情。

2. 加载态封装

// 自动管理 loading 状态 + 错误处理
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);
    const [error, setError] = useState<Error | null>(null);
    
    useEffect(() => {
      fetchData().then(() => setLoading(false)).catch(setError);
    }, []);
    
    if (error) return <ErrorMessage err={error} />;
    return loading ? <Spinner /> : <Comp {...props} />;
  };
}

[!note] 解释 HOC 在不改动组件渲染逻辑的前提下,统一接管了 "加载中 / 出错 / 成功" 三种 UI 状态。多个页面复用同一套加载策略,DRY。

3. Props 转换

// 将 props 做格式变换后注入子组件
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);
    // ⚠️ transform 返回的 key 可能与原 props 冲突
    return <Comp {...(props as any)} {...extra} />;
  };
}

[!warning] 注意 当 transform 返回的 key 和原 props 同名时,后者会覆盖前者。实际项目中建议约定命名空间前缀,例如 apiData、cacheMeta 等。

HOC 的局限性

问题 说明 解决方案
Static 属性丢失 withAuth(Page) 返回的新组件没有 Page.getInitialProps 手动拷贝:EnhancedComponent.getInitialProps = Wrapped.getInitialProps
Wrapper Hell withAuth(withLogging(withData(Page)))——嵌套过深调试困难 用 compose 函数扁平化,或直接改用 Custom Hook
Props 冲突 多个 HOC 都注入 data prop,后一个覆盖前一个 使用唯一命名空间前缀(如 _data → useData.data)
ref 丢失 直接传 ref 给增强组件会报错(需用 forwardRef 包装) 见下方兼容 ref 的方案
隐式依赖 HOC 内部调用的 hook 不透明,阅读者不知道注入了什么 规范命名(withXxx),文档说明行为

为什么需要 forwardRef?

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} />;
  });
}

[!tip] 解释 普通函数组件不能接收 ref。当 HOC 需要透传 ref 给子组件时,必须用 React.forwardRef 包裹,否则 React 会抛出 "Function components cannot be given refs" 错误。这增加了额外的样板代码——Hooks 天然解决了这个问题。

compose 工具

当多层 HOC 叠加时,compose 可以让可读性更好:

// before: 从内到外,阅读时要先找最内层
const Enhanced = withAuth(withCache(withPagination(UserList)));

// after: 从左到右,符合阅读习惯
const Enhanced = compose(
  withAuth,
  withCache,
  withPagination,
)(UserList);

[!question] 思考 compose 真的改善了可读性吗?如果每个 HOC 的作用一目了然,你是否还需要它?

Render Props

核心思想

[!question] 和 HOC 相比,Render Props 到底"额外"给了什么能力?

通过 prop 传递一个函数,该函数返回 JSX——将 UI 渲染逻辑委托给调用方。

与 HOC 的本质区别:HOC 通过组件嵌套注入 props;Render Props 通过函数传参直接暴露状态。

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 函数返回什么就渲染什么
  // 状态在父组件,UI 由调用方决定
  return <div>{render(pos)}</div>;
}

// 使用
<MouseTracker render={({ x, y }) => (
  <div>Cursor at ({x}, {y})</div>
)} />;

[!note] 解释 MouseTracker 是纯粹的关注点分离:它只负责追踪鼠标位置并触发重渲染,完全不关心屏幕上画什么。这种"关注点解耦"是 HOC 难以优雅表达的。

children 作为 Render Prop

当 render prop 只是把数据传给子内容时,可以用 children(本身也是函数)替代——语义更自然。

// children prop 的类型:接收 data,返回 ReactNode
function DataProvider({ children }: { children: (data: Data) => React.ReactNode }) {
  const data = useDatabase();
  return <>{children(data)}</>;
}

<DataProvider>
  {(data) => <Table items={data.items} />}
</DataProvider>;

[!tip] children vs render

  • 用 children:渲染逻辑简单、只需要传入数据 → 代码最简洁
  • 用 render prop:需要多个渲染入口(如 renderEmpty / renderLoading)、或需要对渲染做参数校验 → 类型约束更精确

常见模式:多回调拆分

当渲染逻辑复杂时,可以将一个大 render 拆成多个小 prop,降低心智负担:

interface CarouselProps {
  slides: Slide[];
  renderItem: (slide: Slide, index: number) => React.ReactNode;
  renderIndicator?: (index: number, active: boolean) => React.ReactNode;
}

function Carousel({ slides, renderItem, renderIndicator }: CarouselProps) {
  const [active, setActive] = useState(0);
  return (
    <div>
      {slides.map((s, i) => renderItem(s, i))}
      <div className="indicators">
        {slides.map((_, i) => renderIndicator?.(i, i === active) ?? (
          <span key={i} onClick={() => setActive(i)} 
                className={i === active ? 'dot-active' : 'dot'} />
        ))}
      </div>
    </div>
  );
}

[!note] 为什么拆比合好? 一个巨大的 render 回调会让调用处变得冗长。拆成独立的小 prop(renderItem、renderIndicator),调用方按需实现,未实现的可以省略——这是 React 库设计的经典模式。

HOC vs Render Props vs Custom Hook

graph TB
    A["逻辑复用需求"] --> B["三种方案"]
    
    B --> C["HOC<br/>函数包裹组件"]
    B --> D["Render Props<br/>函数传递 prop"]
    B --> E["Custom Hook<br/>纯函数抽离逻辑"]
    
    C --> C1["⚠️ Wrapper 嵌套深"]
    C --> C2["⚠️ 静态方法丢失"]
    C --> C3["⚠️ ref 需 forwardRef"]
    C --> C4["✅ 不修改原组件 JSX"]
    
    D --> D1["⚠️ 回调嵌套过深"]
    D --> D2["⚠️ Prop 命名冲突风险"]
    D --> D3["✅ UI 控制权完全交给调用方"]
    
    E --> E1["✅ 扁平可读"]
    E --> E2["✅ 直接操作 state / effect"]
    E --> E3["✅ 无额外组件树开销"]
    E --> E4["❌ 只能在组件内部使用"]
    
    style C4 fill:#4FC08D,color:#fff
    style D3 fill:#4FC08D,color:#fff
    style E1 fill:#4FC08D,color:#fff
    style E2 fill:#4FC08D,color:#fff
    style E3 fill:#4FC08D,color:#fff

为什么 Hooks 取代了它们?

Hooks 的核心理念是把状态逻辑从组件中抽离出来,但保持调用处扁平。它结合了 HOC 和 Render Props 的优点:

维度 HOC / Render Props Custom Hook
嵌套层级 多层 wrapper 或回调嵌套 零嵌套,直接平铺
状态共享 需要 prop 层层传递,容易混乱 每个 hook 管理自己的 state,互不干扰
TypeScript 泛型约束复杂,组合后类型可能丢失 完美推断,不需要额外 type wrangling
调试 DevTools 多一层 wrapper,栈更混乱 与普通函数无异
// ❌ HOC 方式:三层嵌套 → 调试困难、性能不可见、type 推导混乱
const ConnectedUserList = withAuth(withCache(withPagination(UserList)));

// ✅ Hook 方式:扁平可读,逻辑一目了然
function UserList() {
  useAuth();                          // 身份验证
  const cache = useCache<User[]>();   // 数据缓存
  const pagination = usePagination(); // 分页管理
  
  return <table>{/* ... */}</table>;
}

[!question] 思考 Hooks 真的完全淘汰了 HOC 和 Render Props 吗?回想一下文档开头的两个核心概念——HOC 通过包裹增强组件,Render Props 通过回调暴露渲染控制。这两种模式在哪些场景中仍然无法被 Hook 替代?

何时仍应使用旧模式?

场景 推荐方案 原因
第三方库封装(不接触用户代码) HOC 库方只操作组件类,无需知道用户内部结构
UI 框架需要提供渲染占位符 Render Props 父组件需要定义多个渲染回调(如 renderEmpty、renderLoading)
通用业务逻辑复用 Custom Hook 首选方案:简洁、可组合、无 DOM 开销

关联笔记

  • 00.Readme
  • 3. Hooks 篇/01-为什么需要 Hooks
  • 3. Hooks 篇/02-useEffect 深度解析
  • 3. Hooks 篇/08-自定义 Hook 最佳实践
  • 4. 进阶篇/07-forwardRef 与 useImperativeHandle
  • 4. 进阶篇/11-组件 Composition 模式