Skip to main content

为 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 提供的 cloneElementContext 都能用来解决这个问题。

二、使用 cloneElement

先使用 React.Children.mapchildren 进行遍历,然后利用 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 是否会对父组件 RadioGroupsize 属性进行覆盖,取决于父组件 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>

另一种对插槽传值的方式是 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>
);
};