1. 从组件复用谈起

在React应用开发中,我们总会遇到需要复用组件逻辑的场景。想象你正在搭建乐高玩具:如何让基础结构既能单独使用,又能灵活组合出不同形态?这正是组件设计模式要解决的核心问题。本文将带你深入探索三种主流模式,通过真实示例感受它们的特点与适用场景。

本文采用React 18技术栈,所有示例均为函数组件+Hooks实现。现在让我们通过具体场景来拆解各模式特点:

2. 高阶组件(HOC)探秘

2.1 什么是HOC?

高阶组件如同"组件增强器",接收组件作为参数并返回增强后的新组件。它像给咖啡加奶泡的机器——保留原味同时添加额外风味。

示例:用户权限控制组件

// 高阶组件工厂函数
const withAdminAccess = (WrappedComponent) => {
  return function EnhancedComponent(props) {
    const [userRole] = useUserRoleContext();
    
    if (userRole !== 'admin') {
      return <div>无访问权限</div>;
    }

    // 透传props并添加新的属性
    return <WrappedComponent {...props} adminToolsEnabled={true} />;
  };
};

// 基础业务组件
const Dashboard = ({ adminToolsEnabled }) => {
  return (
    <div>
      {adminToolsEnabled && <button>管理面板</button>}
      <h2>业务看板</h2>
    </div>
  );
};

// 增强后的组件
const EnhancedDashboard = withAdminAccess(Dashboard);

2.2 应用场景

  • 多组件共享权限校验逻辑
  • 全局埋点监控
  • 样式主题注入

2.3 技术特点

✅ 优势:

  • 实现纯函数式的逻辑抽象
  • 良好的类型推导(配合TypeScript)
  • 支持多层嵌套增强

❌ 局限:

  • 多层嵌套可能形成"组件包装地狱"
  • props命名冲突风险
  • 静态组合导致动态性受限

3. Render Props模式解析

3.1 何为Render Props?

这种模式将渲染逻辑通过函数prop传递,如同组装台式电脑——将零部件的选择权交给用户。

示例:数据请求容器

const DataFetcher = ({ url, children }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, [url]);

  return children({ 
    data,
    loading,
    error: !data && !loading 
  });
};

// 使用示例
<DataFetcher url="/api/user/123">
  {({ data, loading }) => (
    loading ? <Spinner /> : 
    <ProfileCard user={data} />
  )}
</DataFetcher>

3.2 适用场景

  • 复杂状态逻辑共享
  • 动态渲染结构需求
  • 组件间存在多重依赖关系

3.3 核心特性

✅ 优势:

  • 运行时动态决定渲染内容
  • 清晰的组件职责划分
  • 更直观的数据流向

❌ 局限:

  • 嵌套层级较深影响可读性
  • 函数回调影响性能优化
  • 参数传递需显式处理

4. 组合式组件哲学

4.1 组合的本质

组合模式通过结构化嵌套实现功能聚合,就像在俄罗斯套娃中选择最合适的内外搭配。我们常用的Context API正是组合模式的典型应用。

示例:可折叠面板系统

const Accordion = ({ children }) => {
  const [activeIndex, setActiveIndex] = useState(null);
  
  return (
    <div className="accordion">
      {Children.map(children, (child, index) => (
        cloneElement(child, {
          isActive: index === activeIndex,
          onToggle: () => setActiveIndex(
            activeIndex === index ? null : index
          )
        })
      ))}
    </div>
  );
};

const AccordionItem = ({ title, children, isActive, onToggle }) => {
  return (
    <div className={`item ${isActive ? 'active' : ''}`}>
      <div className="header" onClick={onToggle}>
        {title}
        <Icon type={isActive ? 'up' : 'down'} />
      </div>
      {isActive && <div className="content">{children}</div>}
    </div>
  );
};

// 使用组合
<Accordion>
  <AccordionItem title="订单信息">
    <OrderDetails />
  </AccordionItem>
  <AccordionItem title="物流跟踪">
    <ShippingInfo />
  </AccordionItem>
</Accordion>

4.2 适用场景

  • UI结构高度可配置的组件
  • 树形导航系统
  • 多级表单布局

4.3 实现特点

✅ 优势:

  • 天然支持可视化嵌套
  • 直观的组件层次结构
  • 便于实现复杂交互组合

❌ 局限:

  • 子组件需要预先约定API
  • 过度拆分可能导致碎片化
  • 动态组合灵活性稍弱

5. 对比分析与选择策略

5.1 技术维度矩阵

维度 HOC Render Props 组合式组件
逻辑复用 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
可读性 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
性能优化 ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
类型支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
代码侵入性

5.2 典型选择路径

  • 何时选HOC:存在多个需要相同逻辑增强的组件时;需要维护高阶函数链时
  • 何时选Render Props:组件的渲染结果需动态决定时;处理复杂依赖注入时
  • 何时选组合:构建UI套件时;需要创建领域特定语言(DSL)时

6. 专家级注意事项

  1. HOC陷阱:避免在render方法中使用HOC,可能引起组件标识变化导致状态丢失
  2. Render Props性能:使用useCallback缓存渲染函数,避免不必要重渲染
  3. 组合式拆分:保持单一职责原则,组件拆分粒度控制在"可单独使用"的程度
  4. TypeScript技巧:使用泛型类型增强类型推导能力:
// HOC类型定义示例
type WithLoadingProps = {
  isLoading: boolean;
};

function withLoading<T extends WithLoadingProps>(WrappedComponent: React.ComponentType<T>) {
  return (props: Omit<T, keyof WithLoadingProps>) => {
    const [isLoading] = useLoadingState();
    return <WrappedComponent {...props as T} isLoading={isLoading} />;
  };
}

7. 现代React的新思路

随着Hooks的普及,自定义Hook逐渐成为逻辑复用的第四种选择。例如数据请求场景可以这样实现:

const useFetch = (url) => {
  const [state, setState] = useState({ data: null, loading: true });
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => setState({ data, loading: false }));
  }, [url]);

  return state;
};

// 在任何组件内部使用
const UserProfile = ({ userId }) => {
  const { data, loading } = useFetch(`/api/users/${userId}`);
  // ...渲染逻辑
};

这种方案结合了HOC的封装性和组合式的灵活性,值得作为设计模式的重要补充。