嘿,各位前端小伙伴!今天我们来聊一个React中让很多新手头疼的话题------受控组件(Controlled Components)和非受控组件(Uncontrolled Components)。这俩货就像一对性格迥异的兄弟,一个循规蹈矩,一个放荡不羁。让我们通过实际代码例子,揭开它们神秘的面纱!
🤔 什么是受控与非受控组件?
想象你去餐厅吃饭:
- 受控组件就像高级餐厅的点餐系统,你每选一道菜,服务员都会重复确认:"您点了鱼香肉丝对吗?需要微辣还是中辣?"
- 非受控组件则像自助餐,拿了就走,没人管你拿了多少,只有在结账时才知道你到底吃了啥
在React中,这个"控制权"体现在表单数据由谁管理:
- 受控组件:表单数据由React组件的state管理
- 非受控组件:表单数据由DOM本身管理
📊 代码实例对比
让我们看看用户提供的两个典型例子:
非受控组件示例
jsx
function App() {
console.log('组件渲染');
function onChange(event) {
console.log(event.target.value);
}
return (
<input type="text" defaultValue={'hello'} onChange={onChange}/>
)
}
这个input使用defaultValue
设置初始值,通过onChange
监听变化,但没有使用state存储值。就像你把行李寄存了,只能通过收据(回调函数)知道行李情况,但不能直接打开箱子。
受控组件示例
jsx
function App() {
const [value, setValue] = useState('hello')
console.log('组件渲染');
function onChange(event) {
console.log(event.target.value);
setValue(event.target.value.toUpperCase())
}
return (
<input type="text" value={value} onChange={onChange}/>
)
}
这个input使用value={value}
绑定到state,每次输入都会触发onChange
更新state。这就像实时同步的云文档,你每输入一个字,都会立即保存到云端。
📅 实战案例:日历组件(完整实现)
用户提供的日历组件是一个很好的非受控组件例子,让我们详细看看它的实现:
jsx
import { useState } from 'react'
import './index.css'
function Calender(props) {
const { defaultValue, onChange } = props
// 使用defaultValue初始化状态,而非直接控制
const [date, setDate] = useState(defaultValue)
// 月份切换逻辑
const handlePrevMouth = () => {
setDate(new Date(date.getFullYear(), date.getMonth() - 1, 1))
}
const handleNextMouth = () => {
setDate(new Date(date.getFullYear(), date.getMonth() + 1, 1))
}
// 获取当月天数
const daysOfMonth = (year, month) => {
return new Date(year, month + 1, 0).getDate()
}
// 获取当月第一天是星期几
const firstDayOfMonth = (year, month) => {
return new Date(year, month, 1).getDay()
}
// 渲染日期网格
const renderDates = () => {
const days = []
const daysCount = daysOfMonth(date.getFullYear(), date.getMonth())
const firstDay = firstDayOfMonth(date.getFullYear(), date.getMonth())
// 添加月初空白单元格
for (let i = 0; i < firstDay; i++) {
days.push(<div key={`empty-${i}`} className='empty'></div>)
}
// 添加日期单元格
for (let i = 1; i <= daysCount; i++) {
const clickHandler = () => {
const curDate = new Date(date.getFullYear(), date.getMonth(), i)
setDate(curDate)
// 通过回调通知父组件,而非直接受控
onChange(curDate)
}
// 高亮显示当前选中日期
if (i === date.getDate()) {
days.push(
<div key={`day-${i}`} className='day selected' onClick={clickHandler}>
{i}
</div>
)
} else {
days.push(
<div key={`day-${i}`} className='day' onClick={clickHandler}>
{i}
</div>
)
}
}
return days
}
return (
<div className="calender">
<div className="header">
<button onClick={handlePrevMouth}><</button>
<div>{date.getFullYear()} 年 {date.getMonth() + 1} 月</div>
<button onClick={handleNextMouth}>></button>
</div>
<div className="days">
<div className="day">日</div>
<div className="day">一</div>
<div className="day">二</div>
<div className="day">三</div>
<div className="day">四</div>
<div className="day">五</div>
<div className="day">六</div>
{renderDates()}
</div>
</div>
)
}
export default Calender
日历组件的非受控特性分析
这个日历组件完美展示了非受控组件的设计思想:
- 外部初始化,内部管理 :通过
defaultValue
接受初始日期,但内部使用useState
管理状态 - 回调通知机制 :日期变化时通过
onChange
回调通知父组件,而非由父组件直接控制 - 内部状态自治:月份切换、日期选择等逻辑完全在组件内部处理
- 最小知识原则:父组件不需要知道日历的内部实现细节,只需关心初始值和变化结果
使用方式:
jsx
function App() {
return (
<Calender
defaultValue={new Date()} // 初始值
onChange={(newDate) => { // 变化通知
alert(newDate.toLocaleDateString())
}}
/>
)
}
日历效果图:

⚖️ 优缺点大PK
特性 | 受控组件 | 非受控组件 |
---|---|---|
数据流向 | 单向数据流,可预测 | 直接操作DOM,较灵活 |
重新渲染 | 每次输入都会触发 | 初始渲染后通常不会因输入触发 |
实时验证 | 容易实现 | 较难实现 |
代码量 | 较多 | 较少 |
适合场景 | 复杂表单,实时验证 | 简单表单,文件上传 |
💡 最佳实践建议
"通常情况下,不推荐使用受控模式,它会带来组件的重新渲染"。这个观点需要辩证看待:
- 不要害怕重新渲染:React的重渲染没你想象的那么昂贵,大多数情况下性能差异可以忽略不计
- 优先考虑受控组件:特别是表单验证、多输入联动等场景,受控组件提供更好的可预测性
- 非受控组件适用场景 :
- 简单表单
- 文件上传(input type="file")
- 集成非React库
- 需要保留原始DOM行为
- 混合使用:像用户的日历组件那样,内部使用state管理(非受控),同时提供回调通知外部(类似受控)
🧠 总结
希望通过这篇文章,你对React的受控与非受控组件有了更深入的理解。记住,没有绝对的好坏,只有适合的场景!就像有人喜欢吃火锅,有人喜欢吃烧烤,最重要的是知道自己想吃什么,以及为什么想吃。
祝大家编码愉快,控制权掌握在自己手中!🚀