useState 会重渲染,useRef 却沉默如谜:React 双雄背后的哲学对决

108 阅读8分钟

useRef vs useState:React 中“沉默的守护者”与“高调的响应者”

在 React 的声明式世界里,UI 是状态的函数——状态变,界面随之更新。而驱动这一机制的核心,正是我们熟悉的 useState。但你是否注意到,有些数据明明在变化,却不会引起任何重新渲染?它们安静地存在于组件之中,不惊动 React 的调度系统,却在关键时刻发挥作用。

这就是 useRef 的领地。

尽管 useStateuseRef 都能“存值”,但它们的设计哲学截然不同:

  • useState 是 React 数据流的心脏,每一次更新都会触发 UI 同步;
  • useRef 则是 React 提供的“脱围机制” (escape hatch),让你在必要时绕过声明式规则,直接与 DOM 或外部系统交互。

更有趣的是,这两种能力也延伸出了表单处理的两种范式:受控组件非受控组件——一个由 React 全面掌控,一个以 DOM 为单一数据源。

本文将通过清晰的对比、真实的代码示例和生动的比喻,带你深入理解:

  • 它们各自的本质与适用边界;
  • 何时该用“高调的响应者”,何时该请出“沉默的守护者”;
  • 如何在实际项目中做出合理选择,写出更符合 React 思维的代码。

准备好了吗?让我们揭开这对“双生 Hook”的神秘面纱。


一、初识两位“角色”

🎭 useState:高调的响应者

useState 是 React 中最广为人知的状态管理工具。当你调用 setCount(count + 1),组件会立即重新渲染,UI 随之更新。

它就像一位“高调的响应者”——一旦数据变化,立刻通知整个舞台(组件)进行重演(re-render)。

const [count, setCount] = useState(0);

关于useState的介绍在这里就不赘述了,在我之前的文章中也有详细介绍过。

特点:响应式、触发重渲染、用于驱动 UI 变化。


🕵️ useRef:沉默的守护者

ref是React提供的一种脱围机制,何为脱围机制?脱围” (也叫“逃逸”或“绕过 React 的声明式机制”)指的是绕过 React 的常规数据流和渲染机制,直接与 DOM 或其他外部系统进行交互

使用useRef定义的数据发生改变不会触发页面的重新渲染,useRef会返回这样一个对象

{
    current:""//你向useRef传递的值
}

你可以用 ref.current 属性访问该 ref 的当前值。这个值是有意被设置为可变的,意味着你既可以读取它也可以写入它。就像一个 React 追踪不到的、用来存储组件信息的秘密“口袋”

相比之下,useRef 更像一位“沉默的守护者”。它也能保存值,但不会触发组件重渲染。无论你如何修改 ref.current,React 都视而不见——除非你主动在副作用或事件处理器中读取或操作它。

const inputRef = useRef(null);

特点:非响应式、持久化引用、不触发重渲染、适合存储“中间状态”或 DOM 引用。


二、对比:相同点与关键差异

特性useRefuseState
返回值返回一个对象 { current: initialValue },你可以直接修改 .current返回一个数组 [value, setValue],其中 setValue 是唯一修改状态的方式
是否触发重渲染❌ 不会触发重渲染✅ 会触发组件重新渲染
可变性✅ 可变:可在渲染外任意修改 .current 值⚠️ “不可变”:必须通过 setter 修改,触发调度机制
渲染期间访问⚠️ 不应在渲染期间读取或写入 ref.current,否则可能导致 UI 与实际值不一致✅ 可随时读取,但每次渲染都有自己的“state 快照”

💡 补充说明

  • ref 的“当前值”不是“状态” :它只是一个普通的 JavaScript 对象,没有参与 React 的 diff 和 re-render 流程。
  • state 是“快照式”的:每次渲染都会生成一份独立的 state 快照,确保 UI 与数据同步。
  • ref 是“持久引用” :即使组件重新渲染,ref.current 依然指向同一个对象,适合保存定时器 ID、DOM 元素等跨周期数据。

三、实战场景解析

场景 1:自动聚焦输入框(访问 DOM)

const inputRef = useRef(null);

useEffect(() => {
  inputRef.current.focus(); // 直接操作 DOM
}, []);

return <input ref={inputRef} />;

这里必须使用 useRef

  • 因为我们需要直接访问 DOM 元素
  • 如果用 useState 存储 DOM 引用,不仅无法实现,还会因不必要的状态更新导致性能浪费。

🔍 比喻useRef 就像一把“钥匙”,默默保管着通往真实 DOM 的门锁,只有你需要时才拿出来用,不会惊动整个房间(组件)。


场景 2:保存定时器 ID(持久化可变值)

import { 
    useRef,
    useState ,
    useEffect,
 } from 'react'
export default function App(){
    // let intervalId =null;//count改变就被覆盖
    let intervalId =useRef(null);//count改变不会被覆盖
    const [count,setCount] = useState(0);
  
    function start(){
        intervalId.current = setInterval(()=>{
            console.log('tick');
        },1000)
        console.log(intervalId);
        
    }
    function stop(){
        clearInterval(intervalId.current)
    }
    return(
        <>
        <button onClick={start}>开始</button>
        <button onClick={stop}>停止</button>
        {count}
     <button type="button" onClick={()=>setCount(count+1)}>count+1</button>

        </>
    )
}

为什么不用普通变量 let intervalId = null
因为每次组件 re-render(比如 count 改变),变量都会被重置为 null,导致无法清除之前的定时器!所以就无法像我们预料的那样点击停止后关闭定时器

useRef 提供了一个跨渲染周期持久存在的可变容器,就像一个挂在外墙上的工具箱——无论房间(组件)如何翻新(re-render),里面的工具(.current)始终可用。

💡 何时该用 useRef 存值

当且仅当满足:

  1. 该值不用于计算 JSX(即不影响 UI);
  2. 你需要在多次渲染之间保持其引用不变

否则,请优先考虑 useState


四、受控 vs 非受控组件 —— 表单的两种哲学

在 React 中处理表单时,我们常面临一个选择:
让 React 完全掌控表单状态(受控),还是 以 DOM 为单一数据源,仅在需要时读取(非受控)?
这背后其实是两种不同的数据流哲学。

🎯 受控组件:React 主导一切

//<LoginForm />
const [form, setForm] = useState({ username: '', password: '' });

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

return (
  <>
    <input
      type="text"
      name="username"
      value={form.username}
      onChange={handleChange}
    />
    <input
      type="password"
      name="password"
      value={form.password}
      onChange={handleChange}
    />
  </>
);
  • 输入框的 value 由状态 form.username 完全控制
  • 用户每次输入 → 触发 onChange → 更新状态 → 触发重渲染 → 显示新值;
  • 数据流是单向的:状态 → UI,用户输入 → 状态更新。如果不添加onChange事件,我们甚至无法在表单中进行输入。

优点

  • 实时校验(如密码强度提示)
  • 表单联动(如根据用户名自动填充邮箱)
  • 状态可预测,便于调试和测试

💡 比喻:受控组件就像一位“严格教练”,学员(输入框)的每一个动作都必须经过他的指令(状态)才能执行。


🕊️ 非受控组件:以 DOM 为单一数据源

//<CommentBox />
const textareaRef = useRef(null);

const handleSubmit = () => {
  const comment = textareaRef.current.value; // 直接读取 DOM 值
  textareaRef.current.value = ''; // 手动清空
};

return <textarea ref={textareaRef} />;
  • <textarea> 没有绑定 value,其值由 DOM 自身维护;
  • 提交时通过 ref.current.value 一次性读取当前值;
  • React 不参与输入过程的状态同步。

优点

  • 代码更简洁(无需为每个字段写状态和 handler)
  • 性能略优(避免频繁 setState
  • 适合简单表单或一次性操作(如评论、搜索框)

⚠️ 注意:非受控组件无法实现实时验证或动态联动,因为 React “看不见”输入过程。

💡 比喻:非受控组件像一位“自由学生”,平时自己练习(DOM 管理值),只在考试时(提交)把结果交给老师(React)。


🔍 如何选择?

场景推荐方式
复杂表单(校验、联动、动态字段)✅ 受控组件(useState + onChange
简单输入(如评论、一次性搜索)✅ 非受控组件(useRef + ref.current.value
文件上传 <input type="file">⚠️ 必须用非受控(因 value 是只读的)
表单中同时包含简单字段和复杂字段混合使用:简单字段用非受控,复杂字段用受控

📌 关键原则

  • 如果你需要响应用户的每一次输入 → 用受控;
  • 如果你只关心最终提交的值 → 用非受控。

五、何时该用谁?(完整决策指南)

需求推荐方案
数据变化需要更新 UIuseState(受控组件)
需要访问或操作 DOM 元素useRef
存储临时值(如 timer ID、WebSocket 实例)useRef
表单需实时校验、联动或动态控制✅ 受控组件(useState
简单表单,只需提交时读值✅ 非受控组件(useRef
避免因频繁更新导致性能问题useRef(当值无需触发渲染时)

六、总结:工具无好坏,场景定乾坤

  • useState 是 UI 的“指挥官” :它掌控界面状态,一有风吹草动就下令重绘。
  • useRef 是后台的“工程师” :它默默维护那些不需要观众(用户)知道的细节,比如 DOM 引用、定时器、滚动位置等。
  • 受控组件追求精确控制,适合复杂交互;
  • 非受控组件追求简洁高效,适合轻量场景。

正如真实项目中往往同时存在 <CommentBox />(非受控)和 <LoginForm />(受控),高明的开发者懂得:不是所有问题都需要“响应式”答案

🌟 终极心法(来自 React 哲学):
优先使用声明式编程useState + 受控组件)来描述 UI 应该是什么样子;
仅在必要时使用命令式操作useRef + 非受控组件)来处理 React 无法声明式表达的场景。
在“描述意图”与“直接操作”之间,找到最符合 React 思维的平衡。


📚 参考资料脱围机制(Escape Hatches) – React 中文文档