React学习之自定义tab组合组件
前言
以前工作中一直使用的vue开发,最近开始写react的项目,发现好多基础都不会,导致在项目开发中踩坑。这个系列我打算,从学习中实践,直到最后完全掌握react。
自定义Tab
我们平时用的React库,Tab组件和子组件都是组合使用的,像element的React库,Tabs.Item是套在Tabs组件里面的,在React的写法里面,组件是作为children传递到父组件里面的。像这种组合的写,父组件怎么拿到子组件的状态,从而产生联动。

既然能够拿到children,那么通过children就可以监听到子组件的状态变化了。这里要用到一个React的核心api,cloneElement。这个api克隆的是组件,不是元素。
javascript
const newElement = React.cloneElement(
element, // 要克隆的目标元素(必须是合法的 React 元素,比如 <TabItem />)
[props], // 可选:要新增/覆盖的 props(会和原元素 props 合并)
[...children] // 可选:要替换的子元素(不传则保留原元素的子元素)
);
实现代码
通过cloneElement这个方法我们可以对组件进行处理,从而监听子组件的状态,实现联动。以下是完整代码。
javascript
import React, { useState, useCallback } from 'react';
import './custom-test-tab.css';
export const TabItem = ({
children,
label = '',
value,
onHandleClick,
onClick = () => { },
active = false
}) => {
const renderClass = () => {
return `tab-item ${active ? 'active' : ''}`;
}
const handleClick = (e) => {
onHandleClick && onHandleClick(value, e);
onClick(e);
};
return (
<div className={renderClass()} onClick={handleClick}>
{ label || children }
</div>
);
};
export const Tab = function ({ children, active = '', onChange = () => { } }) {
const [activeTab, setActiveTab] = useState(active);
// 2. 修复handleClick参数逻辑 + 补充依赖数组
const handleClick = useCallback((value) => {
if (value === undefined) return; // 防止空值
setActiveTab(value);
onChange(value);
}, [onChange]); // 依赖数组补充onChange,避免闭包问题
const createdTabs = () => {
return React.Children.map(children, (child) => {
// 安全校验:只处理TabItem类型的子组件
if (!React.isValidElement(child) || child.type !== TabItem) {
return child;
}
return React.cloneElement(child, {
onHandleClick: handleClick, // 直接传递函数,无需包装
active: activeTab === child.props.value,
});
});
};
return (
<div className={'custom-test-tab'}>
{createdTabs()}
</div>/
);
};
// 3. 确保Tab.Item正确挂载
Tab.Item = TabItem;
export default Tab;
使用自定义的Tab
javascript
import { useState} from "react";
import { Tab, TabItem } from "../components/custom-test-tab";
export const TestTab = () => {
const [active, setActive] = useState('苹果');
const onChange = (value) => {
setActive(value);
};
const tabList = [
{ label: "苹果", value: "苹果", bgColor: "#FF5252" }, // 苹果 - 红色
{ label: "香蕉", value: "香蕉", bgColor: "#FFC107" }, // 香蕉 - 黄色
{ label: "橘子", value: "橘子", bgColor: "#FF9800" }, // 橘子 - 橙色
{ label: "葡萄", value: "葡萄", bgColor: "#9C27B0" }, // 葡萄 - 紫色
{ label: "凤梨", value: "凤梨", bgColor: "#4CAF50" }, // 凤梨 - 绿色
];
const ContentView = ({ active }) => {
const item = tabList.find(item => item.value === active);
if (!item) return null;
return (
<div style={
{
backgroundColor: item.bgColor,
width: "100%",
height: '200px'
}
}>
{active === "苹果" && <div>苹果</div>}
{active === "香蕉" && <div>香蕉</div>}
{active === "橘子" && <div>橘子</div>}
{active === "葡萄" && <div>葡萄</div>}
{active === "凤梨" && <div>凤梨</div>}
</div>
);
};
return (
<div>
<h1>自定义组件</h1>
<Tab onChange={onChange} active={active}>
{
tabList.map((item, index) => (
<TabItem key={index} label={item.label} value={item.value}>
{item.label}
</TabItem>
))
}
</Tab>
<Tab onChange={onChange} active={active}>
{
tabList.map((item, index) => (
<Tab.Item key={index} label={item.label} value={item.value}>
{item.label}
</Tab.Item>
))
}
</Tab>
<ContentView active={active} />
</div>
);
};
export default TestTab;
渲染效果

总结
证明封装的Tab组件是可以的,通过这个Tab组件的封装,不止是学会了封装这个组件,还通过这个过程学会了,组合组件封装的过程。