
问题背景:为什么要进行组件二次封装?
在现代前端开发中,特别是使用像 Ant Design (antd) 这样的 UI 组件库时,我们经常面临一个共同的挑战:如何在保持组件库原有功能和样式的基础上,根据项目特定需求进行定制和优化。
在我最近的项目中,我们大量使用了 antd 的 Tabs 组件来实现页面的标签页功能。虽然 antd 的 Tabs 组件功能已经相当完善,但在实际项目应用中,我们发现需要在多个地方重复实现一些相似的逻辑模式,比如:
- 统一的标签页切换逻辑处理
- 一致的默认激活键设置
- 项目特定的样式定制
- 标准化的 props 接口
为了解决这些重复性问题,提高开发效率,我决定对 antd 的 Tabs 组件进行二次封装 ,创建一个名为 CompTabs 的项目专用组件,封装通用的逻辑和样式,让团队成员在项目中可以更简单、一致地使用标签页功能。
问题出现:意外的 React 警告
在完成 CompTabs 组件的初步封装后,代码看似运行正常,但在开发过程中,控制台出现了一个令人困惑的 React 警告:
Warning: React does not recognize the `changeLogic` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `changelogic` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
这个警告表明,React 检测到一个名为 changeLogic 的 prop 被传递到了一个 DOM 元素上,而 React 不认识这个属性。
问题分析:深入探究问题根源
我的组件封装实现
我的 CompTabs 组件封装代码大致如下:
tsx
interface baseProps extends TabsProps {
items: TabsProps['items'],
defaultActiveKey: string,
changeLogic?: (activeKey: string) => void;
}
export default function CompTabs(props: baseProps) {
const { items = [], defaultActiveKey = '', changeLogic } = props || {};
const [activeKey, setActiveKey] = useState<string>(defaultActiveKey);
useEffect(() => {
if (defaultActiveKey) setActiveKey(defaultActiveKey);
}, [defaultActiveKey])
const onChange = (active: string) => {
setActiveKey(active);
changeLogic?.(active);
}
return (
<Tabs
{...props} // 这里将所有 props 传递给 Tabs 组件
defaultActiveKey={defaultActiveKey}
activeKey={activeKey}
destroyOnHidden
items={items}
onChange={onChange}
className="tabs-custom"
/>
)
}
组件使用方式
在项目中,我是这样使用这个封装后的组件的:
tsx
<CompTabs
changeLogic={tabsChange}
defaultActiveKey={"1"}
items={itemTabs}
tabPosition="left"
size="small"
/>
问题本质
表面上看,我使用 changeLogic 的方式没有任何问题。它是我为 CompTabs 组件明确定义的一个可选回调函数 prop,用于在标签页切换时执行项目特定的逻辑。
真正的问题在于第 19 行代码:<Tabs {...props} ... />
当我使用 {...props} 将所有接收到的 props 传递给底层的 antd Tabs 组件时,我无意中也将 changeLogic 这个自定义 prop 传递给了它。而 antd 的 Tabs 组件(或者它内部的实现)很可能:
- 不认识
changeLogic这个 prop - 它不是 antd Tabs 组件官方文档中定义的标准 prop - 可能错误地将这个 prop 传递给了某个 DOM 元素 - 在 React 中,如果你将自定义 prop 传递给 DOM 元素(如 div、span 等),React 会发出警告
当 Tabs 组件内部实现将接收到的所有 props 都传递给某个底层 DOM 元素时,changeLogic 这个自定义函数 prop 就被传递到了 DOM 元素上,从而触发了 React 的警告。
解决方案:精准传递 Props
问题定位确认
经过仔细分析和排查,我确认了问题的具体原因:我通过 {...props} 无差别地传递了所有 props 给底层的 Tabs 组件,包括那些只为我的 CompTabs 组件设计、而 Tabs 组件并不需要的自定义 props(如 changeLogic)。
解决方案实施:使用 restProps 模式
为了解决这个问题,我采用了 React 开发中常见的 restProps 模式,即明确区分哪些 props 是要传递给底层组件的,哪些是只供当前组件使用的。
修改后的代码如下:
tsx
export default function CompTabs(props: baseProps) {
// 从 props 中解构出我需要的 props
const { items = [], defaultActiveKey = '', changeLogic, ...restProps } = props || {};
const [activeKey, setActiveKey] = useState<string>(defaultActiveKey);
useEffect(() => {
if (defaultActiveKey) setActiveKey(defaultActiveKey);
}, [defaultActiveKey])
const onChange = (active: string) => {
setActiveKey(active);
changeLogic?.(active);
}
return (
<Tabs
{...restProps} // 只传递那些 Tabs 组件认识的标准 props
defaultActiveKey={defaultActiveKey}
activeKey={activeKey}
destroyOnHidden
items={items}
onChange={onChange}
className="tabs-custom"
/>
)
}
解决方案详解
-
解构时分离 props:
tsxconst { items = [], defaultActiveKey = '', changeLogic, ...restProps } = props || {};通过解构赋值,我将
items、defaultActiveKey和changeLogic这几个我明确需要处理的 props 单独提取出来,而其他所有 props 则通过...restProps收集起来。 -
精准传递 props 给 Tabs 组件:
tsx<Tabs {...restProps} // 标准的、Tabs 组件认识的 props defaultActiveKey={defaultActiveKey} activeKey={activeKey} destroyOnHidden items={items} onChange={onChange} className="tabs-custom" />我不再使用
{...props}无差别传递所有 props,而是使用{...restProps}只传递那些标准的、Tabs 组件能够识别的 props,同时明确列出其他必要的配置项。 -
保留自定义逻辑 :
我仍然可以在组件内部使用
changeLogic这个自定义 prop 来实现项目特定的标签页切换逻辑,但它不会被错误地传递给底层的 Tabs 组件或 DOM 元素。
为什么这种解决方案有效?
这种解决方案之所以有效,是因为它遵循了 React 的最佳实践:
- 明确性:明确知道哪些 props 是给当前组件使用的,哪些是给子组件使用的
- 隔离性:将自定义逻辑与底层组件的实现隔离开来,避免不必要的干扰
- 兼容性:确保只传递底层组件能够理解和处理的 props,避免将自定义 props 传递给不认识它们的组件或 DOM 元素
经验总结与最佳实践
通过这次问题解决过程,我总结了几个在 React 组件二次封装中的重要经验和最佳实践:
1. 谨慎使用 {...props}
虽然 {...props} 提供了一种方便的方式来传递所有 props,但它也带来了潜在的风险,特别是当你封装的组件层次较多时。建议明确知道你正在传递哪些 props,并尽可能避免无差别地传递所有 props。
2. 使用 rest 模式分离 props
采用解构 + rest 模式(const { specificProp, ...rest } = props) 是处理 props 传递的更好方式,它让你能够清晰地分离不同用途的 props,并精准控制哪些 props 应该传递给哪些组件。
3. 二次封装要有明确目的
组件二次封装应该有明确的目的和价值,如:
- 统一项目中的通用逻辑和行为
- 简化复杂组件的使用方式
- 添加项目特定的样式或行为
- 封装重复性的代码模式
在我的案例中,封装 CompTabs 的目的是统一项目中标签页的使用方式,简化常见模式的实现,提高开发效率。
4. 为二次封装组件明确定义接口
为你的二次封装组件定义清晰的 TypeScript 接口或 PropTypes,明确哪些是组件支持的 props,哪些是内部使用的 props。这不仅有助于代码的可维护性,也能帮助团队成员更好地理解和使用你的组件。
5. 充分测试封装组件
二次封装组件虽然基于成熟的第三方组件,但仍需要进行充分的测试,确保在各种使用场景下都能正常工作,不会出现意外的 props 传递或行为异常。
结语
React 组件二次封装是提高项目开发效率、统一代码风格、降低维护成本的强大技术手段。通过这次对 antd Tabs 组件的二次封装经历,我不仅解决了遇到的 React 警告问题,更重要的是学到了在组件封装过程中需要注意的关键点和最佳实践。
希望我的这次经验分享能够帮助其他开发者在进行类似组件封装工作时,避免类似的陷阱,编写出更加健壮、可维护的 React 组件。记住,在追求开发效率的同时,也要关注代码的质量和可维护性,这样才能真正发挥组件封装的价值。
推荐更多阅读内容
Ant Design Form.useWatch 实战指南:从原理到最佳实践
Git Bash 中如何切换到 Tag:查看、切换与安全开发指南
Ant Design Popover 中overlayInnerStyle被废弃?来看看正确的替代方案
React 中的 forwardRef 和 useRef,到底有啥区别?一文让你彻底搞懂
深入理解 TypeScript 中继承 Ant Design 组件 Props 的正确姿势
语义化版本 2.0.0 解读:Ant Design 的版本控制之道
React Ant Design 5.27.4 版本更新日志解读
为什么 Ant Design 将 visible 变更为 open:一次 API 设计的思想升级