直接放答案:
js
const newElement = React.cloneElement(props.children, {
extraProp: "Some extra prop",
});
然而在实际工作中情况会更复杂,尤其是在封装容器类组件时会用到这个功能。下面我由浅入深,分三个级别,介绍实际中如何应对此场景越来越复杂的需求。
第一级
将配置的JSON(数组)传给组件
比如children是个list,且需要将每个list项设置相对应的属性。这个常见于封装可遍历组件时,比如封装一个Tabs组件,拿MUI来举例:
MUI官方实现的代码是这样的:
jsx
// Page.tsx
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
<Tab label="第一项" {...a11yProps(0)} />
<Tab label="第二项" {...a11yProps(1)} />
<Tab label="第三项" {...a11yProps(2)} />
</Tabs>
</Box>
<TabPanel value={value} index={0}>
<div>第一项</div>
</TabPanel>
<TabPanel value={value} index={1}>
<div>第二项</div>
</TabPanel>
<TabPanel value={value} index={2}>
<div>第三项</div>
</TabPanel>
这时,我们可以把这个组件封装成这样:
jsx
// MyTab.tsx
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{props.items.map((item, index) => (
<Tab key={...} label={item.label} {...a11yProps(index)} />
)}
</Tabs>
{props.items.map((item, index) => (
<TabPanel key={...} value={value} index={index}>
{item.content}
</TabPanel>
)}
</Box>
这样,对于这个封装好的组件,我们可以这样用:
jsx
// Page.tsx
<MyTabs
items={[
{
label: "第一项",
content: <div>第一项</div>
},
{
label: "第二项",
content: <div>第二项</div>
},
{
label: "第三项",
content: <div>第三项</div>
},
]}
/>
这样确实是一种方式, AntDesign也就是这么做的。但是如果使用者想要自由选择TabPanel
或CustomTabPanel
呢?
第二级
利用
React.Children.toArray
和React.Children.map
功能处理传进来的children
。
如果我们允许MyTabs
组件接受整个TabPanel
或CustomTabPanel
,那我们该如何将value
和index
传给这个TabPanel
或CustomTabPanel
呢?
所以这里使用者需要像这样写:
jsx
// Page.tsx
<MyTabs labels={["第一项", "第二项", "第三项"]}>
<TabPanel>
<div>第一项</div>
</TabPanel>
<CustomTabPanel>
<div>第二项</div>
</CustomTabPanel>
<TabPanel>
<div>第三项</div>
</TabPanel>
</MyTabs>
那么相应的在MyTabs.tsx
应该这样封装:
jsx
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{props.items.map((item, index) => (
<Tab key={...} label={item.label} {...a11yProps(index)} />
)}
</Tabs>
{React.Children.map(props.children, (child, index) => (
React.cloneElement(child, { // <== 此处用文章开头的方法设置value和index
value,
index
})
))}
</Box>
然后现实工作中,真的就到此为止了嘛?
案例三
利用
fiber
对象里的type.name
来判断是什么组件。
如果children不是一个可遍历的重复的元素,而是各不相同的元素,该怎么办呢? 比如封装一个卡片组件,像这样子:
MUI官方给出的示例代码是这样的:
jsx
// Page.tsx
<MyCard sx={{ maxWidth: 345 }}>
<CardMedia
sx={{ height: 140 }}
image="/static/images/cards/contemplative-reptile.jpg"
title="green iguana"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Lizard
</Typography>
<Typography variant="body2" color="text.secondary">
Lizards are a widespread group of squamate reptiles, with over 6,000
species, ranging across all continents except Antarctica
</Typography>
</CardContent>
<CardActions>
<Button size="small">Share</Button>
<Button size="small">Learn More</Button>
</CardActions>
</MyCard>
如果我们自己开发这样的Card
组件,让使用者可以自由地将CardMedia
, CardContent
, CardAction
等内容作为children
传进来,该怎么实现呢?
实际上上面children
通过toArray
方法遍历得到的一个个child
,就是一个个fiber
对象,打印出来就是这样:
看到那个type
里地name
了吗?我们可以用这个来判断是什么组件。
所以我们可以这样封装:
jsx
MyCard.tsx
<Box>
<SomeHeader>...</SomeHeader>
{React.Children.map(props.children, (child, index) => {
if (child.type.name === "CardActions") { // <=== 如果条件较多,用switch
return (
{/* 比如可以把CardActions包起来 */}
<SomeWrapper>
<Divider /> {/* <=== 比如可以在CardActions上面加条分割线 */}
React.cloneElement(child, {
someProps: "someProps", // <=== 同时用文章开头的方法加一些属性
})
</SomeWrapper>
);
}
return child;
})}
</Box>
完美!这样我们可以对传进来的children,根据它是什么组件,做不同的变化。
结尾
这样对于React组件里传进来的children,我们可以自如地处理了。当然一定还有极端复杂的案例,我们需要花时间具体了解React.Children还有哪些用法,和fiber里的值代表什么,这里就不再钻下去了。