React实现单选组件的N种方式

直接用通过props + options/cloneElement + children

RadioGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function RadioGroup (props) {
const { options, value, onChange } = props;
return (
<div>
{children && React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) {
return child;
}
return React.cloneElement(child, {
...child.props,
key: child.props.value,
checked={item.value === value}
onChange={(checked, val) => {
onChange(val);
}}
});
})}
{!children && options && options.map((item) =>
<Radio
key={item.value}
value={item.value}
checked={item.value === value}
onChange={(checked, val) => {
onChange(val);
}}
/>
)}
</div>
);
}

Radio

1
2
3
4
5
6
7
8
function Radio (props) {
const { value, onChange, checked } = props;
return (
<div onClick={() => onChange(!checked, value)}>
{checked ? '已选中' : '未选中'}
</div>
);
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const options = [
{ label: '选项一',value: 1 },
{ label: '选项二',value: 2 },
];

function App () {
const [value, setValue] = useState(null);
const onChange = (val) => {
setValue(val);
};
return (
<div>
<RadioGroup value={value} onChange={onChange} options={options} />
</div>
);
// 或者
return (
<div>
<RadioGroup value={value} onChange={onChange}>
{options.map((item) => <Radio key={item.value} value={item.value} />)}
</RadioGroup>
</div>
);
}

优点

1.显式数据流

  • 通过 options 或 children 直接控制子组件,数据来源清晰可见
  • 每个Radio 的 checked 和 onChange 由父组件显式注入,父子关系一目了然

2.灵活性高

  • 同时支持 options 配置和 children 动态插入两种模式
  • 可通过cloneElement 动态扩展子组件功能(如添加额外 Props)

3.组件复用性

  • Radio组件不依赖父级逻辑,可独立复用(如单独使用一个 Radio 按钮)

缺点

1.侵入性强

  • 使用cloneElement 会覆盖子组件原有 Props,可能引发命名冲突(如子组件已有 checked 属性)
  • 需要手动处理React.Children.map 和 isValidElement,代码复杂度较高

2.性能隐患

  • 每次渲染都会重新克隆子元素,可能触发不必要的子组件更新
  • 对大型列表(如 1000+ 个Radio)性能较差

3.类型丢失

  • 克隆后的子组件可能丢失 TypeScript 类型提示,需手动声明合并后的 Props 类型‘

通过context + options/children的方式

context

1
const RadioGroupContext = createContext(null);

RadioGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function RadioGroup (props) {
const { options, value, onChange } = props;
return (
<RadioGroupContext value={{
value,
onChange,
}}>
{children}
{!children && options && options.map((item) =>
<Radio key={item.value} value={item.value} />
)}
</RadioGroupContext>
);
}

Radio

1
2
3
4
5
6
7
8
9
10
function Radio (props) {
const { value } = props;
const { value: val, onChange } = useContext(RadioGroupContext);
const checked = value === val;
return (
<div onClick={() => onChange(value)}>
{checked ? '已选中' : '未选中'}
</div>
);
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const options = [
{ label: '选项一',value: 1 },
{ label: '选项二',value: 2 },
];

function App () {
const [value, setValue] = useState(null);
const onChange = (val) => {
setValue(val);
};
return (
<div>
<RadioGroup value={value} onChange={onChange} options={options} />
</div>
);
// 或者
return (
<div>
<RadioGroup value={value} onChange={onChange} />
{options.map((item) => <Radio key={item.value} value={item.value} />)}
</RadioGroup>
</div>
);
}

优点

1.跨层级通信

  • 天然支持深层嵌套组件(如RadioGroup -> Wrapper -> Radio)
  • 子组件无需通过中间层传递 Props

2.代码简洁

  • 消除 Props 逐层传递的冗余代码
  • 子组件通过useContext 直接消费状态,逻辑集中管理

3.动态扩展

  • 支持动态插入子组件(如根据条件渲染不同的Radio)
  • 对options 和 children 两种模式统一处理

缺点

1.组件耦合

  • Radio必须存在于 RadioGroup 上下文中,无法独立使用
  • 脱离上下文的Radio 会直接报错(需额外处理默认值)

2.调试困难

  • Context 数据流在 DevTools 中不如 Props 直观
  • 难以追踪状态变化的来源(尤其是多层 Context 嵌套时)

3.性能优化复杂

  • Context 变化会触发所有消费组件的重渲染
  • 需配合memo 或 useMemo 手动优化