云计算百科
云计算领域专业知识百科平台

面试必问的 React 受控组件,我用 5 个实战场景一次讲透(附完整可运行代码)

为什么 value 写了却不生效?为什么 onChange 不写就变成只读输入框?为什么表单校验要写一堆代码?这三个问题,是无数前端面试官的「送命题」。

如果你正在做表单、写输入框,或者准备面试,这篇就是为你写的。
在这里插入图片描述

看完你能解决 3 个问题:

  • 受控组件和非受控组件到底有什么区别,一句话讲清
  • 什么时候该用受控,什么时候该用非受控,不再纠结
  • 表单校验、动态取值这些实战需求,直接抄代码就能用
  • 全程短句、短段落,代码完整可复制,建议先收藏再慢慢看。

    一、先说痛点:你是不是也这样写过?

    刚学 React 的时候,很多人写输入框是这么干的:

    // 错误示范:value 写死了,输入框永远改不了
    const BadInput = () => {
    const [value, setValue] = useState("")
    return <input type="text" value={value} />
    }

    结果运行一看:输入框点都点不动,键盘敲半天,一个字都输不进去。

    你以为是浏览器坏了,查半天才发现——你没写 onChange。

    这就是受控组件的核心逻辑:数据在哪,展示就在哪;想改数据,必须走 setState。

    二、什么是受控组件?3 行代码讲清楚

    受控组件,就是 组件的显示值完全由 React 状态控制。

    用一个 useState 存值,再用 onChange 更新值,形成一个闭环。

    import { useState } from "react";

    // 受控组件:值由 state 控制,改值必须走 setState
    const ControlledInput = () => {
    const [value, setValue] = useState("");

    return (
    <div>
    <input
    type="text"
    value={value}
    onChange={(e) => setValue(e.target.value)}
    />
    </div>
    );
    };

    export default ControlledInput;

    核心只有 3 个点:

    • value 来自 state,数据源唯一
    • onChange 里调用 setValue,更新数据
    • 输入框展示的值,永远是 state 里最新的值

    这就是 React 的单向数据流:数据流向 UI,UI 事件再改数据,UI 永远不能自己改自己。

    三、什么是非受控组件?useRef 才是主角

    非受控组件,就是 值存在 DOM 自己身上,React 不去管它。

    你想取值?用 useRef 拿到 DOM 节点,直接读 .value。

    import { useRef } from "react";

    // 非受控组件:值存在 DOM 里,需要时用 ref 取
    const UncontrolledInput = () => {
    const inputRef = useRef(null);

    const handleClick = () => {
    console.log(inputRef.current.value); // 点按钮时才取值
    };

    return (
    <div>
    <input type="text" ref={inputRef} />
    <button onClick={handleClick}>点击获取值</button>
    </div>
    );
    };

    export default UncontrolledInput;

    注意看区别:

    • 没有 value,只有 ref
    • 输入过程中 React 完全不管,DOM 自己记着
    • 你想拿值的时候,ref.current.value 一把梭

    一句话记住:受控组件是「React 管数据」,非受控组件是「DOM 自己管数据,React 要用时再取」。

    四、实战场景 1:评论框 —— 非受控的典型用法

    开发博客评论区,用户输入完点提交,你只需要拿到这段文字去调接口。

    这种「只在提交那一刻取值」的场景,非受控组件简直不要太爽——不用每次输入都触发 re-render。

    import { useRef } from "react";

    // 非受控:textarea 的值不经过 React,提交时才读取
    const CommentBox = () => {
    const textareaRef = useRef(null);

    const handleSubmit = () => {
    const comment = textareaRef.current.value;
    if (!comment) return;
    console.log(comment); // 这里一般是调接口上传
    };

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

    export default CommentBox;

    这个场景为什么选非受控?

    • 评论内容不需要实时展示、实时校验
    • 不需要因为用户打字就频繁更新组件
    • 代码量少,ref 一行搞定

    踩坑提醒:ref.current 只有在组件挂载后才有值。在渲染阶段直接读 ref.current,拿到的永远是 null。取值必须放在事件回调或 useEffect 里。

    五、实战场景 2:登录表单 —— 受控的典型用法

    接下来是面试官最爱考的:实时表单校验。

    需求是这样的:

    • 用户名、密码不能为空
    • 长度不能小于 3
    • 有错误就提示,全合法才能点提交

    这种「输入一个字符,就要立刻校验、立刻提示」的场景,必须用受控组件。因为只有数据经过 React,你才能每敲一个字符就校验一次。

    import { useState } from "react";

    const LoginForm = () => {
    const [form, setForm] = useState({ username: "", password: "" });
    const [error, setError] = useState({});

    // 校验规则:返回错误信息,空字符串表示通过
    const validate = (name, value) => {
    let msg = "";
    if (name === "username") {
    if (!value) {
    msg = "用户名为空";
    } else if (value.length < 3) {
    msg = "用户名长度不能小于3";
    }
    }
    if (name === "password") {
    if (!value) {
    msg = "密码为空";
    } else if (value.length < 3) {
    msg = "密码长度不能小于3";
    }
    }
    setError((prev) => ({
    …prev,
    [name]: msg,
    }));
    };

    const handleChange = (e) => {
    const { name, value } = e.target;
    setForm({
    …form,
    [name]: value,
    });
    validate(name, value);
    };

    // 表单合法:用户名密码都有值,且都没有错误
    const isValid = form.username && form.password && !error.username && !error.password;

    const handleSubmit = (e) => {
    e.preventDefault();
    if (!isValid) return;
    console.log(form); // 这里一般是调登录接口
    };

    return (
    <form>
    <h2>登录</h2>
    <div>
    <label>用户名</label>
    <input
    type="text"
    name="username"
    value={form.username}
    placeholder="请输入用户名"
    onChange={handleChange}
    />
    {error.username && <span className="error">{error.username}</span>}
    </div>
    <div>
    <label>密码</label>
    <input
    type="text"
    name="password"
    value={form.password}
    placeholder="请输入密码"
    onChange={handleChange}
    />
    {error.password && <span className="error">{error.password}</span>}
    </div>
    <button type="submit" onClick={handleSubmit} disabled={!isValid}>
    提交
    </button>
    </form>
    );
    };

    export default LoginForm;

    这段代码值得反复看的 3 个细节:

  • name 属性 + 同名 state 字段:两个输入框共用一个 handleChange,靠 name 区分,不用写两遍
  • 实时校验:onChange 里既更新 form,又调用 validate,错误信息跟着输入同步刷新
  • 提交拦截:disabled={!isValid} 让不合法的表单根本提交不了
  • 踩坑提醒:别忘了 e.preventDefault()。 不写的话,点提交页面会刷新,你 console 出来的数据瞬间就没了——这个坑踩过的人都懂。

    六、受控 vs 非受控,一张表看懂

    对比项受控组件非受控组件
    数据存放位置 React state DOM 节点自身
    取值方式 state 变量 ref.current.value
    更新方式 onChange 触发 setState 浏览器自动,无需干预
    实时校验 / 实时联动 支持,简单 不支持,很麻烦
    每次输入都 re-render 不会
    代码量 多一些 少一些
    典型场景 表单校验、实时搜索、联动 评论区、文件上传、一次性表单

    记住这个判断口诀:

    • 要实时拿到值做事情(校验、联动、搜索)→ 用受控
    • 只要最终拿到值做事情(提交、上传)→ 用非受控

    七、踩坑合集:这 4 个坑,90% 的人都踩过

    坑 1:写了 value 不写 onChange,输入框变只读

    // 错误:输入框锁死,无法输入
    <input value={value} />

    // 正确:value 必须配 onChange
    <input value={value} onChange={(e) => setValue(e.target.value)} />

    坑 2:受控组件想设默认值,用 defaultValue,不是 value

    // 错误:用 value 给受控组件设默认值,会锁死输入框
    <input value="默认文案" onChange={handleChange} />

    // 正确:受控组件想有初始值,直接给 state 初始值
    const [value, setValue] = useState("默认文案");

    // 正确:非受控组件想有默认值,用 defaultValue
    <input defaultValue="默认文案" ref={inputRef} />

    坑 3:非受控组件取不到值?多半是时机不对

    ref.current 在组件挂载前是 null。一定要在事件回调或 useEffect 里取,不能在渲染函数里直接读。

    坑 4:不要一个组件里受控和非受控混用

    一个输入框,一会儿用 value,一会儿用 defaultValue,React 会警告,行为也混乱。要么受控到底,要么非受控到底。

    八、到底怎么选?给你一个决策清单

    别死记硬背,遇到输入类组件,按这个清单走:

    • 需要实时校验 / 实时提示错误?→ 受控
    • 需要根据输入实时联动(选类型加载选项)?→ 受控
    • 需要重置表单 / 初始化清空?→ 受控(改 state 就行)
    • 只想提交时拿一次值?→ 非受控
    • 数据量大、频繁输入,不想每次 re-render?→ 非受控
    • 接入第三方非 React 库(如富文本编辑器)?→ 非受控(ref 直接接管 DOM)

    大方向记住:表单优先受控,简单取值用非受控。

    九、总结

    回到开头那三个「送命题」,现在你都会了:

  • 受控 vs 非受控:受控是 React 管数据(state),非受控是 DOM 自己管数据(ref)
  • 什么时候用谁:要实时就受控,要终值就非受控;表单场景优先受控
  • 表单校验怎么写:value + onChange + name 分发 + 实时校验,五步搞定
  • 面试官再问,你就按这三句话回答,稳了。

    最后:源码在这里

    这篇文章里所有代码,我都放在一个完整的 Vite + React demo 里了,直接 npm install && npm run dev 就能跑。

    仓库里有 5 个组件,从最简单的受控/非受控输入框,到带校验的登录表单,每个都能直接复制进你的项目:

    • ControlledInput.jsx:受控组件最小示例
    • UncontrolledInput.jsx:非受控组件最小示例
    • CommentBox.jsx:非受控实战(评论框)
    • LoginForm.jsx:受控实战(登录表单 + 实时校验)
    • RegisterForm.jsx:注册表单(受控写法延伸)

    建议收藏这篇文章,等面试或写表单前翻出来看一遍,比临时查文档快 10 倍。

    如果你觉得有用,点个赞让我知道,下一篇我会拆解为什么受控组件更适合配合表单库(比如 antd)使用,以及封装通用表单组件的实战技巧。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 面试必问的 React 受控组件,我用 5 个实战场景一次讲透(附完整可运行代码)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!