组件设计模式(下):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 替代,但理解这些经典模式对阅读老代码和设计复杂组件仍有重要价值。

相关推荐
CIO_Alliance2 小时前
AI提示系列(2)| Few-shot与ReAct有何不同? 大模型工具调用的底层逻辑详解
前端·人工智能·深度学习·神经网络·react.js·前端框架·ai+ipaas
为你学会写情书4 小时前
React Hooks 完全指南:从内置 Hook 到自定义 Hook,一文彻底掌握
react.js
为你学会写情书4 小时前
React Router 完全指南:从入门到实战,一文搞懂路由核心技巧
react.js
xm_xm_xm_115 小时前
react17版本以前类组件常用操作
前端·javascript·react.js
Damai1 天前
Next.js 切换动态路由后,为什么页面还保留着上一个分类的数据?
前端·react.js
用户298698530141 天前
React 中 Word 文档转图片的实践方案
前端·javascript·react.js
专业抄代码选手1 天前
06|双缓冲与 Commit:让 Fiber 支持第二次渲染
前端·javascript·react.js
专业抄代码选手1 天前
05|把递归渲染拆成 Fiber:让一棵大树可以暂停
前端·javascript·react.js
墨白曦煜1 天前
智能体架构范式总结(React、Plan-And-Solve、Reflection)
前端·react.js·前端框架
zzzzzz3101 天前
react-bits:把动效组件库当成前端表达的参考系
react.js·开源·动效