🍳 受控组件和非受控组件,我纠结了一整天,最后用"房东和租客"讲明白了

写在前面:今天学了一个 React 表单的核心概念------受控组件(Controlled)和非受控组件(Uncontrolled) 。一开始我完全懵了:同样是输入框,怎么还分"受控""非受控"?老师说:"受控组件是 React 管着输入框的值,非受控组件是输入框自己管自己的值------React 需要用的时候才去'抢'过来。" 听完我想到一个比喻:受控 = 房东管着房租(状态),非受控 = 租客自己管钱,房东要用钱时才去问。 一整个下午,我们把两种组件都写了一遍,还做了一个注册表单,终于搞明白了。


一、什么是"受控"?先看两种写法

1.1 受控组件(ControlledInput)

ControlledInput.jsx

jsx 复制代码
import { useState } from 'react';

function ControlledInput() {
    const [value, setValue] = useState('');

    return (
        <>
            ControlledInput
            <input
                type="text"
                value={value}                 // ← 值是 React 状态控制的
                onChange={(e) => {
                    setValue(e.target.value);  // ← 输入时更新状态
                }}
            />
        </>
    );
}

受控组件的特征:value + onChange 成对出现。

  • value={value}:输入框显示的值,来自 React 的 state
  • onChange:用户输入时,把新值写回 state

数据流:

scss 复制代码
用户输入
  ↓ onChange
setValue(e.target.value)   // 更新 React 状态
  ↓ 重新渲染
<input value={value}>      // React 把新值"塞回"输入框

状态(React)和界面(输入框)永远一致------输入框的值完全由 React 控制。

1.2 非受控组件(UncontrolledInput)

UncontrolledInput.jsx

jsx 复制代码
import { useRef } from 'react';

function UncontrolledInput() {
    const inputRef = useRef(null);

    function handleClick() {
        console.log(inputRef.current.value);  // 需要时再"抢"值
    }

    return (
        <>
            UncontrolledInput
            <input type="text" ref={inputRef} />
            <button onClick={handleClick}>获取输入值</button>
        </>
    );
}

非受控组件的特征:用 ref 获取值,没有 valueonChange

  • 输入框的值由浏览器 DOM 自己管理
  • React 不管它的值是什么。
  • 需要时通过 inputRef.current.value 从 DOM 里"抢"出来。

数据流:

arduino 复制代码
用户输入
  ↓ 浏览器自己处理
输入框的值存在 DOM 里(React 不知道)
  ↓ 点击按钮
inputRef.current.value  // 从 DOM 里读取

二、"房东和租客"比喻

2.1 受控组件 = 房东管房租

perl 复制代码
受控组件:
房租(输入值)由房东(React state)管理
  ↓
租客(用户)交钱 → 房东记账(setState)→ 房东统一管理
  ↓
每次要查账,直接看房东的账本(state),永远准确

好处:React 随时知道输入框的值(状态即真相)。

2.2 非受控组件 = 租客自己管钱

复制代码
非受控组件:
钱(输入值)由租客(DOM)自己管
  ↓
房东(React)不知道租客有多少钱
  ↓
房东要用钱时,才去问租客(inputRef.current.value)

好处:代码简单,不需要每次输入都更新状态;坏处:React 不知道当前值。

2.3 对比表

对比 受控组件 非受控组件
值的来源 React state DOM 自己
获取值 直接读 state ref.current.value
每次输入 触发 setState(重新渲染) 不触发渲染
代码量 多(value + onChange) 少(ref 一行)
实时校验 ✅ 可以(状态即值) ❌ 不方便
适用场景 表单校验、联动、复杂表单 简单输入、评论框

三、实战:评论框(非受控)

CommentBox.jsx

jsx 复制代码
import { useRef } from 'react';

function CommentBox() {
    const textareaRef = useRef(null);

    function handleSubmit() {
        const comment = textareaRef.current.value;
        if (!comment) return;
        console.log(comment);  // 拿到评论内容
    }

    return (
        <>
            <textarea
                placeholder="输入评论..."
                ref={textareaRef}
            />
            <button onClick={handleSubmit}>提交评论</button>
        </>
    );
}

评论框为什么适合非受控?

  • 用户输入评论时,不需要实时校验
  • 只有点击"提交"时才需要值。
  • ref 在提交时读取,避免了每次输入都 setState 的性能浪费

老师说:

"非受控------用 useRef 获取值。简单输入场景,不需要实时响应。"


四、实战:注册表单(受控)

RegisterForm.jsx

jsx 复制代码
import { useState } from 'react';

function RegisterForm() {
    // 用一个对象管理整个表单
    const [form, setForm] = useState({
        username: '',
        password: '',
    });

    // 通用 onChange:用 name 属性动态更新对应的字段
    const handleChange = (e) => {
        setForm({
            ...form,
            [e.target.name]: e.target.value,  // 计算属性名!
        });
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log(form);  // 直接拿到完整表单数据
    };

    return (
        <div>
            <input
                name='username'
                value={form.username}
                onChange={handleChange}
                placeholder="请输入用户名"
                type="text"
            />
            <input
                name='password'
                value={form.password}
                onChange={handleChange}
                placeholder="请输入密码"
                type="password"
            />
            <button type="submit" onClick={handleSubmit}>提交</button>
        </div>
    );
}

4.1 为什么要用受控表单?

注册表单的痛点:提交时需要校验用户名是否为空、密码长度是否够。

非受控表单:提交时去 DOM 一个个读值,还要手动校验,麻烦。

受控表单

arduino 复制代码
用户输入
  ↓ onChange
[e.target.name]: e.target.value   // 自动更新到 form 对象
  ↓
form 就是"实时真相"------React 随时知道所有字段的值
  ↓ 提交时
console.log(form)  // 直接拿到 { username, password }

4.2 计算属性名的妙用

jsx 复制代码
[e.target.name]: e.target.value

这是 ES6 的"计算属性名"语法:

  • 输入框 name="username" → 更新 form.username
  • 输入框 name="password" → 更新 form.password

一个 handleChange 通吃所有字段------不用为每个字段写一个 onChange!

4.3 不可变更新

jsx 复制代码
setForm({
    ...form,                    // 展开旧值(保留其他字段)
    [e.target.name]: e.target.value,  // 只更新当前字段
});

...form 展开运算符保留其他字段,只改当前输入框对应的字段------这是 React 不可变数据更新的标准写法。


五、什么时候用哪种?

5.1 决策指南

场景 选哪种 为什么
注册/登录表单(要校验) 受控 状态即真相,方便实时校验
搜索框(要联动) 受控 输入即触发搜索
评论框(提交才用值) 非受控 简单,省性能
文件上传(value 只读) 非受控 受控组件管不了 file 输入
富文本编辑器 非受控 用 ref 获取内容

5.2 老师的话

老师说:

"受控组件------响应式状态控制 input,收集用户的输入。非受控------用 useRef,输入框自己管值。"

一句话:需要 React 实时知道值(校验、联动)→ 受控;只是最后取一次值 → 非受控。


六、总结

概念 说明
受控组件 value + onChange,值由 React state 控制
非受控组件 ref 获取值,值由 DOM 自己管理
受控数据流 输入 → setState → 重新渲染 → 塞回输入框
非受控数据流 输入 → DOM 存着 → 需要时 ref 读取
计算属性名 [e.target.name]: value 动态更新对象字段
不可变更新 {...form, [name]: value} 保留其他字段
选择标准 要实时校验联动 → 受控;只要最终值 → 非受控

受控组件和非受控组件没有绝对的好坏------受控适合需要 React 实时掌握值的复杂表单,非受控适合只需要最终取值的简单场景。理解两者的区别,才能在合适的场景用对的方式。


写在最后

今天最大的收获,是终于分清了受控和非受控。以前写表单全靠"运气"------要么 value 没写 onChange 输入框变灰,要么用 ref 到处取值。现在明白了:受控组件用 state 管值,适合校验联动;非受控组件用 ref 取值,适合简单输入。 而且计算属性名 [e.target.name] 这个技巧太香了------一个 onChange 通吃整个表单。

下次面试官问你:"受控组件和非受控组件有什么区别?"

你可以淡定地说:

"受控组件和非受控组件的核心区别是'值由谁管理'。受控组件用 value + onChange,输入框的值由 React state 控制,用户输入触发 setState 更新状态,再通过 value 回显------React 实时知道输入框的值,适合需要校验、联动的场景(如注册表单)。非受控组件用 ref(如 useRef),输入框的值由浏览器 DOM 自己管理,需要时通过 ref.current.value 读取,React 不参与值的实时管理------适合只需最终取值的场景(如评论框)。选择标准:需要实时知道值 → 受控;只要最终值 → 非受控。受控表单常用 [e.target.name]: value 计算属性名 + ...form 展开运算符实现通用 onChange。"

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。

相关推荐
mmsx1 小时前
一个黑边 Bug 修了两版:自己算矩阵直接黑屏,借库重建只用了一行 setZoom
android·前端
丑过三八线1 小时前
001 -【FastAPI 入门教程】 FastAPI Helloword
前端·chrome·fastapi
weixin_BYSJ19871 小时前
springboot小区物业管理小程序---附源码45555
java·javascript·spring boot·python·django·flask·php
禁止摆烂_才浅1 小时前
Axios 高频面试题
前端·面试·axios
何时梦醒1 小时前
TypeScript 工具类型一篇讲透:Pick、Omit、Partial、Exclude、Record、ReturnType、keyof
前端·面试·typescript
用户2181697049301 小时前
Flutter (十九) Tabbar
前端
缓冲中请稍后1 小时前
前端HTTP请求完全指南:从基础到LLM接口调用实战
前端·面试
半个落月1 小时前
React 受控组件与非受控组件详解:从输入框到表单校验
前端·react.js
BreezeJiang1 小时前
写了 display:flex,为什么三栏布局还没完成?
前端·css