深入理解 React 中的 useRef:从 DOM 引用到受控/非受控组件的设计哲学

29 阅读6分钟

深入理解 React 中的 useRef:从 DOM 引用到受控/非受控组件的设计哲学

在 React 函数组件的世界里,useRef 看似简单,却承载着连接“声明式 UI”与“命令式操作”的关键桥梁作用。它不仅是获取 DOM 元素的工具,更是实现非受控组件、管理可变状态、绕过闭包陷阱的核心手段。

而要真正掌握 useRef 的价值,必须将其置于 受控组件(Controlled Components)非受控组件(Uncontrolled Components) 的对比框架中理解。本文将系统讲解:

  • useRef 的本质与特性
  • 受控 vs 非受控组件的核心区别
  • 何时该用哪种模式?

一、useRef 是什么?——一个持久化的“容器”

const ref = useRef(initialValue);
  • 返回一个对象 { current: initialValue }
  • 引用地址在整个组件生命周期中不变
  • 修改 ref.current 不会触发重渲染
  • 适用于存储不需要驱动 UI 更新的数据

🧠 你可以把 useRef 理解为函数组件中的“实例属性”(类似类组件中的 this.xxx)。

useRef 与 useState:React 状态体系的“双子星”

在函数组件中,useStateuseRef 共同构成了管理数据的两大支柱,但它们的职责截然不同:

特性useStateuseRef
目的管理响应式状态,驱动 UI 更新存储非响应式可变值,不触发重渲染
更新方式必须通过 setter 函数(如 setValue直接赋值(ref.current = newValue
重渲染✅ 触发❌ 不触发
适用场景用户输入、UI 状态、条件渲染DOM 引用、定时器 ID、上一次值、闭包逃逸

关键区别示例

const [count, setCount] = useState(0);     // 修改后组件重新渲染
const countRef = useRef(0);                // 修改后组件静默更新

// 正确用法
setCount(count + 1);               // ✅ 触发 UI 更新
countRef.current = countRef.current + 1; // ✅ 仅内部记录,不影响 UI

💡 记住

  • 如果这个值的变化需要用户看到 → 用 useState
  • 如果这个值只是程序内部使用 → 用 useRef

二、受控组件 vs 非受控组件:React 表单的两种范式

1. 受控组件(Controlled Component)

定义:表单元素的值由 React 状态(state)完全控制。

import { useState } from 'react';

export default function LoginForm() {
  const [form, setForm] = useState({ username: '', password: '' });

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(form); // 状态即表单数据
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={form.username}
        onChange={handleChange}
        placeholder="用户名"
      />
      <input
        name="password"
        type="password"
        value={form.password}
        onChange={handleChange}
        placeholder="密码"
      />
      <button type="submit">登录</button>
    </form>
  );
}

优点

  • 数据完全由状态驱动,符合 React 单向数据流
  • 易于实现实时校验输入联动(如密码强度提示)
  • 可预测、可调试、可测试

缺点

  • 每次输入都触发重渲染(虽有优化,但仍有成本)
  • 代码略显冗长(需写 onChange + value

💡 适用场景:需要实时反馈、复杂校验、动态表单(如多步骤表单、字段依赖)

为什么必须用 useState
因为:

  • 输入框的显示内容必须随状态变化而更新;
  • 任何校验、联动、重置逻辑都依赖于状态的一致性;
  • React 的单向数据流要求“状态是唯一真相源”。

2. 非受控组件(Uncontrolled Component)

定义:表单元素的值由 DOM 自身管理,React 通过 ref 在需要时读取其值。

import { useRef } from 'react';

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

  const handleSubmit = () => {
    const comment = textareaRef.current?.value;
    if (!comment) return alert('请输入评论');
    console.log(comment); // 从 DOM 直接读取
  };

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

优点

  • 无需为每个输入绑定状态,性能更优(尤其在大型表单中)
  • 代码简洁,适合一次性读取场景
  • 与原生 HTML 行为一致,学习成本低

缺点

  • 无法实现实时校验输入联动
  • 调试困难(值不在 React 状态树中)
  • 不符合 React 声明式理念(混合了命令式操作)

⚠️ 注意:这里不能用 useState,因为:

  • 我们不关心输入过程中的中间值;
  • 不希望每次按键都触发重渲染;
  • 最终只需一次读取(如提交时)。

💡 适用场景:简单表单、文件上传(<input type="file"> 必须是非受控)、一次性提交(如搜索框、评论框)


三、useRef 的三大核心应用场景

场景 1:实现非受控组件(DOM 值读取)

这是 useRef 最直接的用途——让 React “放手”表单控制权,仅在提交时介入

const inputRef = useRef(null);

const onSubmit = (e) => {
  e.preventDefault();
  console.log(inputRef.current.value); // 读取当前值
};

⚠️ 注意:<input type="file"> 必须是非受控的,因为其 value 是只读的。


场景 2:在受控组件中辅助命令式操作

即使使用受控组件,有时仍需命令式操作,比如自动聚焦

const [value, setValue] = useState('');
const inputRef = useRef(null);

useEffect(() => {
  inputRef.current?.focus(); // 聚焦,但值仍由 state 控制
}, []);

return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} />;

这里,value 是受控的,但 focus() 是命令式的——两者可以共存


场景 3:混合模式 —— 同一表单中同时使用受控与非受控

export default function App() {
  const [username, setUsername] = useState(''); // 受控
  const passwordRef = useRef(null);             // 非受控

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('用户名:', username);
    console.log('密码:', passwordRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="用户名(受控)"
      />
      <input
        ref={passwordRef}
        type="password"
        placeholder="密码(非受控)"
      />
      <button type="submit">登录</button>
    </form>
  );
}

✅ 这种混合模式在实际项目中很常见:关键字段用受控(如用户名),敏感字段用非受控(如密码)以减少状态暴露


四、如何选择?受控 or 非受控?—— useState 还是 useRef?

需求推荐方案使用的 Hook
实时校验邮箱格式受控组件useState
提交评论(无需预览)非受控组件useRef
密码强度提示受控组件useState
文件上传非受控组件useRef
表单重置功能受控组件(更易实现)useState
高频输入(如搜索建议)受控 + 防抖useState + useRef(存最新值)

🎯 经验法则

  • 表单需要即时反馈 → 用受控
  • 表单只需最终提交 → 用非受控
  • 不确定?优先用受控,它是 React 推荐的模式

五、常见误区与最佳实践

❌ 误区1:在非受控组件中设置 value 属性

// 错误!这会让 input 变成“只读”
<input ref={ref} value="固定值" />

应使用 defaultValue

<input ref={ref} defaultValue="初始值" />

❌ 误区2:用 useRef 存储需要驱动 UI 的数据

const countRef = useRef(0);
// ...
{countRef.current} // ❌ 不会更新 UI!

✅ 正确做法:用 useState

✅ 最佳实践:封装自定义 Hook 统一管理

// useInput.js
import { useState } from 'react';

export const useInput = (initialValue) => {
  const [value, setValue] = useState(initialValue);
  return {
    value,
    onChange: (e) => setValue(e.target.value),
    reset: () => setValue(initialValue)
  };
};

// 使用
const username = useInput('');
<input {...username} />;

六、总结:useState 是“大脑”,useRef 是“手脚”

  • useState 是 React 响应式系统的核心,负责维护 UI 状态的一致性;

  • useRef 是 React 对命令式世界的妥协与赋能,用于处理 DOM、存储瞬态数据;

  • 在表单设计中:

    • 受控 = useState 主导 → 数据驱动、可预测;
    • 非受控 = useRef 主导 → 性能优先、简单直接;
  • 两者不是对立,而是互补:复杂应用往往同时使用二者。

🌟 终极建议
“Use state for what the user sees, refs for what the program needs.”
—— 用户看到的用状态,程序需要的用引用。

掌握这一原则,你就能在 React 的声明式世界与现实的命令式需求之间,游刃有余。