🎭 React受控与非受控组件:一场"控制权"的争夺战

嘿,各位前端小伙伴!今天我们来聊一个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}>&lt;</button>
                <div>{date.getFullYear()} 年 {date.getMonth() + 1} 月</div>
                <button onClick={handleNextMouth}>&gt;</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

日历组件的非受控特性分析

这个日历组件完美展示了非受控组件的设计思想:

  1. 外部初始化,内部管理 :通过defaultValue接受初始日期,但内部使用useState管理状态
  2. 回调通知机制 :日期变化时通过onChange回调通知父组件,而非由父组件直接控制
  3. 内部状态自治:月份切换、日期选择等逻辑完全在组件内部处理
  4. 最小知识原则:父组件不需要知道日历的内部实现细节,只需关心初始值和变化结果

使用方式:

jsx 复制代码
function App() {
    return (
        <Calender 
            defaultValue={new Date()}  // 初始值
            onChange={(newDate) => {  // 变化通知
                alert(newDate.toLocaleDateString())
            }}
        />
    )
}

日历效果图:

⚖️ 优缺点大PK

特性 受控组件 非受控组件
数据流向 单向数据流,可预测 直接操作DOM,较灵活
重新渲染 每次输入都会触发 初始渲染后通常不会因输入触发
实时验证 容易实现 较难实现
代码量 较多 较少
适合场景 复杂表单,实时验证 简单表单,文件上传

💡 最佳实践建议

"通常情况下,不推荐使用受控模式,它会带来组件的重新渲染"。这个观点需要辩证看待:

  1. 不要害怕重新渲染:React的重渲染没你想象的那么昂贵,大多数情况下性能差异可以忽略不计
  2. 优先考虑受控组件:特别是表单验证、多输入联动等场景,受控组件提供更好的可预测性
  3. 非受控组件适用场景
    • 简单表单
    • 文件上传(input type="file")
    • 集成非React库
    • 需要保留原始DOM行为
  4. 混合使用:像用户的日历组件那样,内部使用state管理(非受控),同时提供回调通知外部(类似受控)

🧠 总结

希望通过这篇文章,你对React的受控与非受控组件有了更深入的理解。记住,没有绝对的好坏,只有适合的场景!就像有人喜欢吃火锅,有人喜欢吃烧烤,最重要的是知道自己想吃什么,以及为什么想吃。

祝大家编码愉快,控制权掌握在自己手中!🚀

相关推荐
顾辰逸you2 分钟前
uniapp--咸虾米壁纸项目(一)
前端·微信小程序
方方洛16 分钟前
电子书阅读器:epub电子书文件的解析
前端·产品·电子书
idaibin17 分钟前
Rustzen Admin 前端简单权限系统设计与实现
前端·react.js
GISer_Jinger23 分钟前
Trae Solo模式生成一个旅行足迹App
前端·javascript
zhangbao90s24 分钟前
Intl API:浏览器原生国际化API入门指南
前端·javascript·html
艾小码26 分钟前
构建现代前端工程:Webpack/Vite/Rollup配置解析与最佳实践
前端·webpack·node.js
跟橙姐学代码31 分钟前
Python 集合:人生中最简单的真理,只有一次
前端·python·ipython
复苏季风32 分钟前
站在2025 年 来看,现在应该怎么入门CSS
前端·css
pepedd86434 分钟前
深度解剖 Vue3 架构:编译时 + 运行时的协作
前端·vue.js·trae
一枚前端小能手36 分钟前
🧪 改个代码就出Bug的恐惧,前端测试来帮忙
前端·测试