Radio单选组件开发中的常见问题与最佳实践
先看效果,再看代码
做表单的时候,Radio 单选框几乎是绕不开的组件。别看它简单,真用起来各种细节问题一大堆。我之前在一个问卷项目里,光是 Radio 的选中状态和数据同步就折腾了大半天。今天分享几个亲测有效的写法,直接上代码,少废话。
最基础的用法,HTML 原生写法其实就够用了,但要注意 name 必须一致,不然就不是“单选”了:
<label>
<input type="radio" name="gender" value="male" /> 男
</label>
<label>
<input type="radio" name="gender" value="female" /> 女
</label>
但这种写法在现代项目里基本没人手写了,尤其是 React/Vue 项目。我更推荐用受控组件的方式,把状态交给 JS 管理,这样后续处理表单、校验、提交都方便。
React 里怎么写才不翻车
在 React 中,我习惯封装一个 RadioGroup 组件,避免每个页面都重复写一堆 useState 和 onChange。下面是我现在项目里用的简化版:
import { useState } from 'react';
function RadioGroup({ options, value, onChange }) {
return (
<div>
{options.map(option => (
<label key={option.value} style={{ display: 'block', margin: '8px 0' }}>
<input
type="radio"
name="radio-group"
value={option.value}
checked={value === option.value}
onChange={(e) => onChange(e.target.value)}
/>
{option.label}
</label>
))}
</div>
);
}
// 使用
function App() {
const [selected, setSelected] = useState('option1');
const options = [
{ value: 'option1', label: '选项一' },
{ value: 'option2', label: '选项二' },
{ value: 'option3', label: '选项三' },
];
return (
<RadioGroup
options={options}
value={selected}
onChange={setSelected}
/>
);
}
这个写法最大的好处是:逻辑清晰,复用性强。而且 checked 是明确控制的,不会出现“点了没反应”的玄学问题。
踩坑提醒:这三点一定注意
我在多个项目里反复踩过这些坑,列出来帮你省点时间:
- 不要用
defaultChecked混合受控和非受控:React 里一旦用了value或checked,就必须全程受控。如果你用了defaultChecked,又在后面动态改状态,控制台会警告你,而且行为可能不符合预期。 - label 和 input 的关联要完整:虽然上面代码里我把
input包在label里,这是最简单的做法。但如果你因为 UI 需要把它们分开(比如用图标代替默认圆点),一定要用id+for关联,否则点击文字无法选中。亲测移动端 Safari 对这个特别敏感。 - <样式重置别偷懒:不同浏览器对 radio 的默认样式差异很大,尤其在移动端。我建议直接用 CSS 重置,或者用伪元素自定义。别指望靠原生样式跨端一致。
举个自定义样式的例子,用 Tailwind 写的(实际项目里可能还要加 focus、disabled 状态):
<label class="flex items-center cursor-pointer">
<input type="radio" name="choice" class="sr-only" />
<span class="w-4 h-4 border-2 rounded-full border-gray-400 mr-2"></span>
<span>自定义选项</span>
</label>
这里用 sr-only 隐藏原生 input,用 span 模拟外观。关键是要保留 input 的可访问性,不能直接删掉 input。
这个场景最好用:动态选项 + 异步加载
有时候选项不是写死的,而是从接口拉的。比如用户选择“国家”,然后根据国家动态加载“省份”。这时候要注意:**初始值可能不在选项列表里**。
我之前遇到一个 bug:用户编辑表单时,后端返回的已选值是 “province_999”,但新接口返回的省份列表里没有这个值(可能是历史数据),结果 Radio 就全没选中,用户以为没选,其实只是选项变了。
解决方案很简单:在设置 state 之前,先判断选项是否存在。如果不存在,可以设为 null 或者保留原值(看业务需求):
useEffect(() => {
fetch('/api/provinces')
.then(res => res.json())
.then(data => {
setOptions(data);
// 如果当前选中的值在新选项里,就保留;否则清空
if (data.some(opt => opt.value === currentSelected)) {
setSelected(currentSelected);
} else {
setSelected(null); // 或者设为默认值
}
});
}, [country]);
这个细节很容易被忽略,但上线后会被用户疯狂吐槽“我的选择怎么没了”。
高级技巧:用 Radio 实现 Tab 切换
别笑,这招我真用过。有些轻量级的 Tab 切换,用 Radio 比写一堆状态管理还清爽,尤其适合静态内容切换。
<div class="radio-tabs">
<input type="radio" name="tab" id="tab1" checked />
<input type="radio" name="tab" id="tab2" />
<label for="tab1">Tab 1</label>
<label for="tab2">Tab 2</label>
<div class="tab-content">
<div class="tab-pane">内容1</div>
<div class="tab-pane">内容2</div>
</div>
</div>
.radio-tabs input[type="radio"] {
display: none;
}
.tab-content .tab-pane {
display: none;
}
#tab1:checked ~ .tab-content .tab-pane:nth-child(1),
#tab2:checked ~ .tab-content .tab-pane:nth-child(2) {
display: block;
}
纯 CSS 实现,零 JS,适合 SEO 友好的静态页。当然,复杂交互还是得上 JS,但这种小场景用起来贼爽。
最后说两句
Radio 看似简单,但细节决定体验。我现在的项目里,所有表单组件都做了统一封装,包括 Radio、Checkbox、Select,确保状态管理、校验、无障碍都一致。如果你还在每个页面手写,建议早点抽离。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多(比如和 Formik、Zod 结合做表单校验),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论