为 children (插槽) 添加属性
一、应用场景
封装组件时,常常会遇到需要给一组子元素添加相同的属性。
例如给单选框添加 size
属性控制单选框大小,使用时如下:
<Radio size="small">选项一</Radio>
当使用单选框组时,给 <RadioGroup>
添加 size
属性,需要让每一项 <Radio>
都加上 size
属性:
<Radio.Group size="small">
<Radio value="1">选项一</Radio>
<Radio value="2">选项二</Radio>
<Radio value="3">选项三</Radio>
</Radio.Group>
在封装组件时,相当于将 size
属性传入每一项 children:
const RadioGroup: React.FC<RadioGroupProps> = (props) => {
const {
children,
size,
...others
} = props
return (
<div className='i-radio-group'>
{children}
</div>
)
}
而 React 提供的 cloneElement
和 Context
都能用来解决这个问题。
二、使用 cloneElement
先使用 React.Children.map
对 children 进行遍历,然后利用 cloneElement
的第二个参数为该项添加属性:
const RadioGroup: React.FC<RadioGroupProps> = (props) => {
const {
children,
size,
...others
} = props
return (
<div className='i-radio-group'>
{React.Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return null;
}
const childProps = {
size,
...others
};
return React.cloneElement(child, childProps);
})}
</div>
)
}
这样,使用 RadioGroup
时,给 RadioGroup
添加的属性就会传递给每一项 children 了:
<Radio.Group size="small">
<Radio value="1">选项一</Radio>
<Radio value="2">选项二</Radio>
<Radio value="3">选项三</Radio>
</Radio.Group>
当单项子项也使用相同属性时,例如:
<Radio.Group size="small">
<Radio value="1">选项一</Radio>
<Radio value="2" size="large">选项二</Radio>
<Radio value="3">选项三</Radio>
</Radio.Group>
这时子组件自身的属性 size 是否会对父组件 RadioGroup
的 size 属性进行覆盖,取决于父组件 RadioGroup
中对 childProps 的操作:
- 子组件属性优先级更高
- 父组件属性优先级更高
<div className='i-radio-group'>
{React.Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return null;
}
const childProps = {
size,
...(child as React.ReactElement).props,
...others
};
return React.cloneElement(child, childProps);
})}
</div>
<div className='i-radio-group'>
{React.Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return null;
}
const childProps = {
...(child as React.ReactElement).props, // 这段在前或没有这一段
size,
...others
};
return React.cloneElement(child, childProps);
})}
</div>
另一种对插槽传值的方式是 Context。
三、使用 Context
Context
提供了一个局部的全局作用域,使用 Context
则无需再手动的逐层传递 props。
这里使用 createContext + useContext 实现跨级传参,给每一项子元素添加全局属性的同时,又照顾到单项子元素添加相同属性的优先级问题。
1、以 RadioGroup + Radio 为例
首先在 <RadioGroup>
使用 createContext
:
export interface RadioContextValue {
inject: (props: RadioProps) => RadioProps;
}
export const RadioContext = React.createContext<RadioContextValue>(null as any);
const RadioGroup: React.FC<RadioGroupProps> = (props) => {
const {
children,
size = "small",
...others
} = props
// 注入每一项的 context
const context: RadioContextValue = {
// 将 props 注入每一项子节点的方法
inject: (singleRadioProps: RadioProps) => {
return {
size,
...singleRadioProps,
};
},
};
return (
<RadioContext.Provider value={context}>
<div className='i-radio-group'>
{children}
</div>
</RadioContext.Provider>
)
}
然后在 <Radio>
中使用 useContext
:
const Radio: React.FC<RadioProps> & { Group: React.ElementType } = (props) => {
// 存在包裹组时从 Context 注入全局属性
const context = useContext(RadioContext);
const newProps = context ? context.inject(props) : props;
const {
children,
className,
style,
size = "small",
...others
} = newProps;
return (
<label
className={classNames(
`i-radio`,
size && `i-${type}--size-${size}`,
className,
)}
style={{ ...style }}
{...others}
>
...
</label>
);
};
2、以 Tabs + TabsItem 为例
首先在 <Tabs>
使用 createContext
:
export interface TabsItemAddProps extends TabsItemProps {
/**
* 选项卡风格类型
* @default normal
*/
type?: 'normal' | 'card';
}
export interface TabsContextValue {
inject: (props: TabsItemProps) => TabsItemAddProps;
}
export const TabsContext = React.createContext<TabsContextValue>(null as any);
const Tabs: React.FC<TabsProps> & { Item: React.ElementType } = (props) => {
const {
children,
type = 'normal'
} = props
// 注入每一项的 context
const context: TabsContextValue = {
// 将 props 注入每一项子节点的方法
inject: (singleTabsProps: TabsItemProps) => {
return {
type,
...singleTabsProps,
};
},
};
return (
<TabsContext.Provider value={context}>
<div className='i-tabs'>
{children}
</div>
</TabsContext.Provider>
)
}
然后在 <TabsItem>
中使用 useContext
:
const TabsItem: React.FC<TabsItemProps> = (props) => {
// 从 Tabs Context 注入全局属性
const context = useContext(TabsContext);
const newProps: TabsItemAddProps = context ? context.inject(props) : props;
const {
children,
className,
style,
// 以下为 context 结合或传入
type = 'normal',
} = newProps;
return (
<label
className={classNames(
`i-tabs-item`,
type && `i-tabs-item--type-${type}`,
className,
)}
style={{ ...style }}
>
...
</label>
);
};