你的组件 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 参数里,让你的应用拥有"记忆"。

相关推荐
Hooray33 分钟前
为了在 Vue 项目里用上想要的 React 组件,我写了这个 skill
前端·ai编程
咸鱼翻身了么35 分钟前
模仿ai数据流 开箱即用
前端
风花雪月_35 分钟前
🔥IntersectionObserver:前端性能优化的“隐形监工”
前端
Bigger35 分钟前
告别 AI 塑料感:我是如何用 frontend-design skill 重塑项目官网的
前端·ai编程·trae
发际线向北36 分钟前
0x02 Android DI 框架解析之Hilt
前端
Ruihong36 分钟前
Vue v-bind 转 React:VuReact 怎么处理?
vue.js·react.js·面试
zhensherlock1 小时前
Protocol Launcher 系列:Overcast 一键订阅播客
前端·javascript·typescript·node.js·自动化·github·js
liangdabiao1 小时前
开源AI拼豆大升级 - 一键部署cloudflare page - 全免费 web和小程序
前端·人工智能·小程序
程序员小胖胖1 小时前
来聊聊我为什么放弃了三层架构
架构
SuperHeroWu72 小时前
【鸿蒙基础入门】概念理解和学习方法论说明
前端·学习·华为·开源·harmonyos·鸿蒙·移动端