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

12 阅读2分钟

引言

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

一、高阶组件(HOC)

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

典型场景:权限控制

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

典型场景:日志追踪

// 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 更灵活。

基础示例:鼠标追踪

// 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

// 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

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

三、Compound Components 模式

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

实战:可组合的 Select 组件

// 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 复用复杂逻辑、动态渲染
CompoundContext 隐式通信复杂 UI 组件库

选型建议

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

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