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

引言

在 React 开发中,组件复用是核心课题。昨天我们学习了受控/非受控组件和容器组件模式,今天继续深入三种高级设计模式:高阶组件( HOC Render PropsCompound 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 替代,但理解这些经典模式对阅读老代码和设计复杂组件仍有重要价值。

相关推荐
用户2814512549922 小时前
迷你React手写系列-React基本概念
react.js
im_AMBER3 小时前
万字长文:编辑器集成Vercel AI SDK
前端·人工智能·react.js·前端框架·编辑器
大雷神3 小时前
HarmonyOS APP<玩转React>开源教程二十:收藏功能实现
前端·react.js·开源·harmonyos
问道飞鱼4 小时前
【前端知识】React生态你了解多少?
前端·react.js·前端框架·生态
We་ct4 小时前
React Diff & Key 核心解析
开发语言·前端·javascript·react.js·前端框架·reactjs·diff
Highcharts.js17 小时前
React 图表如何实现下钻(Drilldown)效果
开发语言·前端·javascript·react.js·前端框架·数据可视化·highcharts
发现一只大呆瓜21 小时前
React-深度拆解 React路由:从实战进阶到底层原理
前端·react.js·面试
发现一只大呆瓜21 小时前
React-手把手带你实现 Keep-Alive 效果
前端·react.js·面试
张一凡931 天前
重新理解 React 状态管理:用类的方式思考业务
前端·react.js