你的组件 API 为什么像个垃圾场?—— React 复合组件模式 (Compound Components) 实战教学

前言:一种名为"配置地狱"的组件

接上回。咱们用 React Query 把服务端状态剥离了,用 Context 把全局状态理顺了。现在你的数据流很干净。

但是,当你打开 components 文件夹,看着那个被你改了无数次的 Tabs 组件,是不是又想骂人了?

为了满足产品经理五彩斑斓的需求,你的组件 props 越加越多,最后变成了这样:

tsx 复制代码
// ❌ 典型的"配置型"组件
 }, { title: '设置', content:  }]}
  activeTab={currentTab}
  onTabChange={setCurrentTab}
  tabBarClassName="bg-gray-100" // 想改 Tab 栏背景?加个 prop
  tabItemClassName="text-lg"    // 想改文字大小?加个 prop
  activeTabClassName="text-blue" // 想改选中态颜色?再加个 prop
  renderTabBarExtraContent={新建} // 想在右边加个按钮?又要加 prop
  tabPosition="top" // 想把 Tab 放左边?还得加逻辑
/>

这就叫**"配置地狱"**。 你试图通过 props 暴露出所有的 UI 细节,结果就是这个组件变得巨臃肿,且极难复用。如果我想给第二个 Tab 加个红点(Badge),你是不是还得给 items 数组的数据结构里加个字段?

兄弟,别再折磨自己了。今天我们来学学 Compound Components(复合组件模式) 。看看人家 HTML 原生标签是怎么教我们做人的。

灵感来源:向 `` 致敬

你仔细想想,原生的 `` 标签是怎么用的?

复制代码
  苹果
  香蕉

你并没有传一个 options 数组给 ,而是直接把 塞到了 `` 里面。

  • `` 负责管理状态(当前选了谁)。
  • 负责渲染每一项,并且告诉 "我被点了"。

这种**"父组件管状态,子组件管渲染,通过隐式契约通信"**的模式,就是复合组件模式。


实战重构:把 Tabs 拆开

我们要把那个臃肿的 Tabs 组件,拆成 Tabs, TabList, Tab, TabPanels, Panel 这一套乐高积木。

第一步:创建上下文 (Context)

父组件需要一个地方来告诉子组件:现在的 activeTab 是谁,以及提供一个 setActiveTab 的方法。

import 复制代码
const TabsContext = createContext(null);

// 这是一个自定义 Hook,方便子组件拿数据,顺便做个错误检查
const useTabs = () => {
  const context = useContext(TabsContext);
  if (!context) throw new Error('Tabs 子组件必须包裹在  里面!');
  return context;
};

第二步:父组件 (Tabs) ------ 状态的大管家

它不负责画 UI,只负责提供 Context。

const 复制代码
  const [selectedIndex, setSelectedIndex] = useState(defaultIndex);

  return (
    
      <div>{children}</div>
    
  );
};

第三步:子组件 (Tab & Panel) ------ 真正的打工人

Tab 按钮:

const 复制代码
  const { selectedIndex, setSelectedIndex } = useTabs();
  const isActive = selectedIndex === index;

  return (
     setSelectedIndex(index)}
    >
      {children}
    
  );
};

Panel 内容区:

const 复制代码
  const { selectedIndex } = useTabs();
  // 只有选中时才渲染
  return selectedIndex === index ? <div>{children}</div> : null;
};

见证奇迹的时刻:调用方式

重构完之后,我们在页面里怎么用呢?

// 复制代码
  {/* 你可以在这里随便加 div,随便写样式,完全不受 props 限制 */}
  <div>
    <div>
      用户管理
      
      {/* 居然可以给单独某一个 Tab 加红点,甚至加 Tooltip,随你便! */}
      
        系统设置 <span>●</span>
      
    </div>

    {/* 想在右边加个按钮?直接写啊!不用传什么 renderExtraContent */}
    刷新
  </div>

  <div>
    
    
  </div>

对比一下之前的代码,现在的优势在哪里?

  1. UI 结构完全解耦 :你想把 Tab 列表放在下面?想把 Panel 放在上面?随便你怎么排版 HTML,组件逻辑完全不需要改。
  2. 内容随心所欲 :你想在 Tab 标题里加图标?加红点?加 loading 动画?直接在 children 里写 JSX 也就是了,不需要去改组件源码。
  3. 没有 Props Drilling :状态通过 Context 隐式传递,你不用手动把 activeTab 传给每一个 Tab。

进阶技巧:这就是 Headless UI 的雏形

聪明的你可能发现了,这种模式其实就是我在前几篇提到的 Headless UI 的一种实现方式。

像著名的 UI 库 Radix UI 或者 Headless UI (Tailwind) ,全是这个路子。

  • ``
  • ``
  • ``
  • ``

它们把组件拆得稀碎,把**"怎么组合"**的权力交还给了你。

当然,这种模式也有个小缺点:代码量变多了。 以前写个 `` 只要一行,现在要写十几行。

怎么解? 你可以基于这个复合组件,再封装一层"傻瓜式"组件给没特殊需求的场景用。但是底层的实现,一定要保持这种灵活性。


总结

当你发现你的组件需要接受 xxxStyle, xxxClassName, renderXxx 这种 props 的时候,请立刻停下来。

这说明你在试图控制你控制不了的事情(外部的 UI 展示)。

把控制权交出去。用 Compound Components 模式,让使用者像拼乐高一样组装你的组件。 你会发现,你再也不用因为设计稿改了一个 margin 或者加了一个 icon 而去改组件源码了。这才是真正的高内聚、低耦合

好了,我要去把那个传了 20 个 props 的 Modal 组件给拆了,祝大家的组件 API 永远性感。


下期预告 :Tabs 切换是搞定了,但有个问题:用户刷新页面后,又回到了第一个 Tab,辛辛苦苦填的表单也没了。 还有,我想把"当前在第二个 Tab"这个状态分享给同事,怎么做? 下一篇,我们来聊聊 "URL 即状态 (URL as State)" 。教你如何把 React 状态同步到 URL 参数里,让你的应用拥有"记忆"。

相关推荐
alanAltman2 小时前
前端架构范式:意图系统构建web
前端·javascript
梦鱼2 小时前
我踩了 72 小时的 Electron webview PDF 灰色坑,只为告诉你:别写这行代码!
前端·javascript·electron
ycgg2 小时前
Webpack vs Vite 全方位对比:原理、配置、场景一次讲透
前端
百罹鸟2 小时前
在langchain Next 项目中使用 shadcn/ui 的记录
前端·css·人工智能
华仔啊2 小时前
Vue3和Vue2的核心区别?很多开发者都没完全搞懂的10个细节
前端·vue.js
a努力。2 小时前
网易Java面试被问:fail-safe和fail-fast
java·windows·后端·面试·架构
亭上秋和景清2 小时前
指针进阶: 回调函数
开发语言·前端·javascript
Mintopia2 小时前
🌐 开源社区在 WebAIGC 技术迭代中的推动作用与争议
前端·人工智能·aigc
周杰伦_Jay2 小时前
【Open-AutoGLM】手机端智能助理框架详解
智能手机·架构·开源·云计算