前端工程Technical Deep Dive

组件设计模式 —— 复合组件、高阶组件与 Render Props

发布时间2026/03/29
分类前端工程
预计阅读2 分钟
作者吴长龙
*

组件设计模式概述

01.内容

组件设计模式概述

好的组件设计能让代码更可复用、更易维护。本文介绍 React 中几种经典的组件设计模式。

一、复合组件 (Compound Components)

#### 什么是复合组件?

复合组件是一组协同工作的组件,父组件管理状态,子组件负责渲染:

jsx snippetjsx
// 复合组件示例:Tabs
function Tabs({ children, defaultIndex = 0 }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);
  
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

Tabs.TabList = function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
};

Tabs.Tab = function Tab({ children, index }) {
  const { activeIndex, setActiveIndex } = useContext(TabsContext);
  const isActive = index === activeIndex;
  
  return (
    <button 
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={() => setActiveIndex(index)}
    >
      {children}
    </button>
  );
};

Tabs.Panel = function Panel({ children, index }) {
  const { activeIndex } = useContext(TabsContext);
  
  if (index !== activeIndex) return null;
  
  return <div className="tab-panel">{children}</div>;
};

// 使用
function MyTabs() {
  return (
    <Tabs defaultIndex={0}>
      <Tabs.TabList>
        <Tabs.Tab index={0}>Tab 1</Tabs.Tab>
        <Tabs.Tab index={1}>Tab 2</Tabs.Tab>
      </Tabs.TabList>
      <Tabs.Panel index={0}>Content 1</Tabs.Panel>
      <Tabs.Panel index={1}>Content 2</Tabs.Panel>
    </Tabs>
  );
}

#### 高级复合组件:Select

jsx snippetjsx
const SelectContext = createContext({});

function Select({ children, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const [highlightedIndex, setHighlightedIndex] = useState(0);
  
  const context = {
    value,
    onChange,
    isOpen,
    setIsOpen,
    highlightedIndex,
    setHighlightedIndex,
  };
  
  return (
    <SelectContext.Provider value={context}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}

Select.Trigger = function Trigger({ children }) {
  const { value, isOpen, setIsOpen } = useContext(SelectContext);
  
  return (
    <button 
      className={`select-trigger ${isOpen ? 'open' : ''}`}
      onClick={() => setIsOpen(!isOpen)}
    >
      {value?.label || children || 'Select...'}
    </button>
  );
};

Select.Option = function Option({ children, value }) {
  const { value: selectedValue, onChange, setIsOpen } = useContext(SelectContext);
  const isSelected = selectedValue?.value === value?.value;
  
  return (
    <div 
      className={`select-option ${isSelected ? 'selected' : ''}`}
      onClick={() => {
        onChange(value);
        setIsOpen(false);
      }}
    >
      {children}
    </div>
  );
};

Select.List = function List({ children }) {
  const { isOpen } = useContext(SelectContext);
  
  if (!isOpen) return null;
  
  return <div className="select-list">{children}</div>;
};

// 使用
<Select value={selectedOption} onChange={setSelectedOption}>
  <Select.Trigger>Choose...</Select.Trigger>
  <Select.List>
    <Select.Option value={{ value: 'a', label: 'Option A' }}>Option A</Select.Option>
    <Select.Option value={{ value: 'b', label: 'Option B' }}>Option B</Select.Option>
  </Select.List>
</Select>

二、高阶组件 (HOC)

#### 什么是 HOC?

高阶组件是接收组件并返回新组件的函数:

javascript snippetjavascript
// 基础 HOC 结构
function withHOC(WrappedComponent) {
  return function EnhancedComponent(props) {
    // 添加新功能
    return <WrappedComponent {...props} />;
  };
}

#### 实际示例:日志 HOC

javascript snippetjavascript
function withLogger(WrappedComponent) {
  return function LoggerWrapper(props) {
    useEffect(() => {
      console.log(`[${WrappedComponent.name}] mounted`);
      return () => console.log(`[${WrappedComponent.name}] unmounted`);
    }, []);
    
    return <WrappedComponent {...props} />;
  };
}

// 使用
const LoggedButton = withLogger(Button);

function App() {
  return <LoggedButton onClick={() => {}}>Click me</LoggedButton>;
}

#### 实际示例:权限 HOC

javascript snippetjavascript
function withAuth(WrappedComponent) {
  return function AuthWrapper(props) {
    const { user, isAuthenticated } = useAuth();
    
    if (!isAuthenticated) {
      return <Redirect to="/login" />;
    }
    
    return <WrappedComponent {...props} user={user} />;
  };
}

// 使用:保护路由
const ProtectedDashboard = withAuth(Dashboard);

function App() {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />
      <Route path="/dashboard" element={<ProtectedDashboard />} />
    </Routes>
  );
}

#### 实际示例:数据获取 HOC

javascript snippetjavascript
function withData(WrappedComponent, fetchFn) {
  return function DataWrapper(props) {
    const { data, loading, error, refetch } = useQuery({
      queryKey: [fetchFn.name],
      queryFn: fetchFn,
    });
    
    if (loading) return <Skeleton />;
    if (error) return <Error error={error} />;
    
    return (
      <WrappedComponent 
        {...props} 
        data={data} 
        refetch={refetch}
      />
    );
  };
}

// 使用
const UserListWithData = withData(UserList, fetchUsers);

function App() {
  return <UserListWithData />;
}

#### HOC 链式组合

javascript snippetjavascript
// 组合多个 HOC
const EnhancedComponent = withLogger(
  withAuth(
    withData(WrappedComponent)
  )
);

// 使用装饰器语法(需要配置)
@withLogger
@withAuth
@withData
class MyComponent extends React.Component {}

三、Render Props

#### 什么是 Render Props?

render props 是共享代码的技术,通过 props 传递函数:

javascript snippetjavascript
// 基础 Render Props
function MouseTracker({ render }) {
  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 render(position);
}

// 使用
<MouseTracker render={({ x, y }) => (
  <div>Mouse at {x}, {y}</div>
)} />

#### Render Props vs HOC

特性HOCRender Props
代码组织包装组件传递渲染函数
Props透传困难直接使用
TypeScript需要泛型更友好
组合链式嵌套

#### 实际示例:模态框

javascript snippetjavascript
function Modal({ isOpen, onClose, render }) {
  if (!isOpen) return null;
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {render({ onClose })}
      </div>
    </div>
  );
}

// 使用
function App() {
  return (
    <Modal
      isOpen={isOpen}
      onClose={() => setIsOpen(false)}
      render={({ onClose }) => (
        <>
          <h2>Confirm</h2>
          <p>Are you sure?</p>
          <button onClick={onClose}>Cancel</button>
          <button onClick={handleConfirm}>OK</button>
        </>
      )}
    />
  );
}

四、自定义 Hook(现代替代)

javascript snippetjavascript
// Render Props 可以用 Hook 替代
function useMouse() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    // ... same as before
  }, []);
  
  return position;
}

// 使用:更简洁
function App() {
  const { x, y } = useMouse();
  return <div>Mouse at {x}, {y}</div>;
}

五、实践:构建一个图表组件

javascript snippetjavascript
// 复合组件方式
function Chart({ data }) {
  return (
    <ChartContext.Provider value={data}>
      <div className="chart">
        <Chart.Title />
        <Chart.Legend />
        <Chart.Canvas />
        <Chart.Tooltip />
      </div>
    </ChartContext.Provider>
  );
}

Chart.Title = function ChartTitle({ children }) {
  return <h3 className="chart-title">{children}</h3>;
};

Chart.Legend = function ChartLegend() {
  const { data } = useContext(ChartContext);
  return (
    <div className="chart-legend">
      {data.map(item => (
        <span key={item.label}>{item.label}</span>
      ))}
    </div>
  );
};

// 使用
<Chart data={chartData}>
  <Chart.Title>销售趋势</Chart.Title>
  <Chart.Legend />
  <Chart.Canvas />
</Chart>

总结

模式适用场景优点缺点
复合组件组件内部状态共享API 清晰、易理解需要 Context
HOC逻辑复用、权限纯函数、易组合Props 丢失、嵌套
Render Props状态共享灵活、透明嵌套地狱
Hook逻辑复用简洁、可组合需要 Hooks 规则

现代推荐:优先使用 Hooks,复杂场景用复合组件。HOC 和 Render Props 逐渐被 Hooks 取代。