🎭 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的受控与非受控组件有了更深入的理解。记住,没有绝对的好坏,只有适合的场景!就像有人喜欢吃火锅,有人喜欢吃烧烤,最重要的是知道自己想吃什么,以及为什么想吃。

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

相关推荐
独立开阀者_FwtCoder7 分钟前
放弃 JSON.parse(JSON.stringify()) 吧!试试现代深拷贝!
前端·javascript·github
爱喝水的小周1 小时前
AJAX vs axios vs fetch
前端·javascript·ajax
Jinxiansen02111 小时前
unplugin-vue-components 最佳实践手册
前端·javascript·vue.js
几道之旅1 小时前
介绍electron
前端·javascript·electron
周胡杰1 小时前
鸿蒙arkts使用关系型数据库,使用DB Browser for SQLite连接和查看数据库数据?使用TaskPool进行频繁数据库操作
前端·数据库·华为·harmonyos·鸿蒙·鸿蒙系统
31535669131 小时前
ClipReader:一个剪贴板英语单词阅读器
前端·后端
玲小珑1 小时前
Next.js 教程系列(十一)数据缓存策略与 Next.js 运行时
前端·next.js
qiyue772 小时前
AI编程专栏(三)- 实战无手写代码,Monorepo结构框架开发
前端·ai编程
轻语呢喃2 小时前
React智能前端:从零开始的识图学单词项目(一)
javascript·react.js·aigc
断竿散人2 小时前
JavaScript 异常捕获完全指南(下):前端框架与生产监控实战
前端·javascript·前端框架