组件设计模式(下):HOC、Render Props 与 Compound Components

引言

在 React 开发中,组件复用是核心课题。昨天我们学习了受控/非受控组件和容器组件模式,今天继续深入三种高级设计模式:高阶组件( HOC ) 、Render Props 和 Compound Components。掌握这些模式,能让你写出更灵活、可维护的代码。

一、高阶组件(HOC)

HOC 是接收组件并返回新组件的函数,本质是函数式编程的组合思想。

典型场景:权限控制

javascript 复制代码
// withAuth.jsx
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { isAuthenticated, user } = useAuth();
    
    if (!isAuthenticated) {
      return <Navigate to="/login" replace />;
    }
    
    return <WrappedComponent {...props} user={user} />;
  };
}

// 使用
const ProtectedDashboard = withAuth(Dashboard);

典型场景:日志追踪

javascript 复制代码
// withLogging.jsx
function withLogging(WrappedComponent) {
  return function LoggedComponent(props) {
    useEffect(() => {
      console.log(`[Mount] ${WrappedComponent.name}`, props);
      return () => {
        console.log(`[Unmount] ${WrappedComponent.name}`);
      };
    }, [props]);
    
    return <WrappedComponent {...props} />;
  };
}

⚠️ HOC 注意事项

  1. 不要在 render 中使用 HOC:会导致组件树重建,状态丢失
  2. 静态方法 丢失 :需要用 hoist-non-react-statics 拷贝
  3. ref 传递问题 :需要用 forwardRef 处理

二、Render Props 模式

Render Props 是通过函数 prop 实现代码复用的模式,比 HOC 更灵活。

基础示例:鼠标追踪

javascript 复制代码
// MouseTracker.jsx
function MouseTracker({ children }) {
  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 children(position);
}

// 使用
<MouseTracker>
  {({ x, y }) => (
    <div>鼠标位置:{x}, {y}</div>
  )}
</MouseTracker>

实战:数据请求 Render Props

javascript 复制代码
// Fetch.jsx
function Fetch({ url, children, onError }) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });
  
  useEffect(() => {
    const controller = new AbortController();
    
    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => {
        if (err.name !== 'AbortError') {
          setState({ data: null, loading: false, error: err });
          onError?.(err);
        }
      });
    
    return () => controller.abort();
  }, [url]);
  
  return children(state);
}

// 使用
<Fetch url="/api/users">
  {({ data, loading, error }) => {
    if (loading) return <Spinner />;
    if (error) return <ErrorMessage error={error} />;
    return <UserList users={data} />;
  }}
</Fetch>

Render Props vs HOC

维度 HOC Render Props
嵌套层级 易产生包装地狱 更扁平
prop 冲突 可能覆盖原 prop 无冲突
类型推断 较复杂 更直观
使用场景 简单增强 复杂逻辑复用

三、Compound Components 模式

Compound Components 通过上下文实现组件间隐式通信,适合构建复杂 UI 组件。

实战:可组合的 Select 组件

javascript 复制代码
// Select.jsx
const SelectContext = createContext(null);

function Select({ children, value, onChange }) {
  const [open, setOpen] = useState(false);
  
  return (
    <SelectContext.Provider value={{ value, onChange, open, setOpen }}>
      <div className="select-container">{children}</div>
    </SelectContext.Provider>
  );
}

function SelectTrigger({ children }) {
  const { open, setOpen } = useContext(SelectContext);
  return (
    <button onClick={() => setOpen(!open)} className="trigger">
      {children}
    </button>
  );
}

function SelectContent({ children }) {
  const { open } = useContext(SelectContext);
  if (!open) return null;
  return <div className="content">{children}</div>;
}

function SelectOption({ value, children }) {
  const { value: selectedValue, onChange } = useContext(SelectContext);
  const isSelected = value === selectedValue;
  
  return (
    <div 
      className={`option ${isSelected ? 'selected' : ''}`}
      onClick={() => onChange(value)}
    >
      {children}
    </div>
  );
}

// 组合使用
Select.Trigger = SelectTrigger;
Select.Content = SelectContent;
Select.Option = SelectOption;

// 实际使用
<Select value={selected} onChange={setSelected}>
  <Select.Trigger>选择城市</Select.Trigger>
  <Select.Content>
    <SelectOption value="beijing">北京</SelectOption>
    <SelectOption value="shanghai">上海</SelectOption>
    <SelectOption value="guangzhou">广州</SelectOption>
  </Select.Content>
</Select>

优势分析

  1. 声明式 API:使用者只需关注组合,不关心内部实现
  2. 灵活布局:子组件可以任意排列,不受父组件限制
  3. 隐式通信:通过 Context 传递状态,避免 prop drilling

总结

模式 核心思想 适用场景
HOC 函数组合增强 权限、日志、数据注入
Render Props 函数 prop 复用 复杂逻辑、动态渲染
Compound Context 隐式通信 复杂 UI 组件库

选型建议:

  • 简单功能增强 → HOC
  • 需要灵活渲染 → Render Props
  • 构建组件库 → Compound Components

React 18 之后,许多场景可以用 Hooks + Context 替代,但理解这些经典模式对阅读老代码和设计复杂组件仍有重要价值。

相关推荐
Highcharts.js17 小时前
Highcharts Orbit 完全指南:把每一张图表变成交互式分析工具
react.js·前端框架·highcharts·orbit·使用指南·ai 图表·交互式分析工具
鸿蒙开发1 天前
小程序存档写到一半怎么办?双槽保存与损坏恢复实战
前端·javascript·react.js
古茗前端团队3 天前
由 key 引发的思考
前端·javascript·react.js
用户298698530146 天前
React 前端将 Excel 工作表导出为图片的技术实践
javascript·react.js·excel
終不似少年遊*7 天前
【安全业务Agent】从场景到编排
python·react.js·ai·langchain·agent·安全架构·工作流
JavaPub-rodert8 天前
从零搭建通用管理后台:ShiyuAdmin 的 Go、React 与 RBAC 实践
开发语言·react.js·golang
Highcharts.js8 天前
React 中原生支持使用 JSX 组件配置自定义表格 Highcharts Grid
前端·javascript·react.js·前端框架·jsx·highcharts·自定义表格
JudithHuang8 天前
React 嵌套路由与布局
前端·react.js·arcgis·react·react router
前端小D8 天前
React Native项目开发(iOS侧为例)
javascript·react native·react.js
志尊宝9 天前
AI原生前端开发(042):AI辅助的路由设计与优化
javascript·react.js·ai-native