React Hooks :useRef、useState 与受控/非受控组件全解析

Hooks 函数之 useRef 与 useState,受控组件与非受控组件

大家好呀~ 今天咱们来聊聊 React 中非常重要的两个 Hooks 以及表单处理的两种方式 ------useRef、useState,还有受控组件与非受控组件。这些可是 React 开发中的基础又核心的知识点哦,掌握它们能让咱们的代码更优雅、更高效~ 😊

一、useRef 📌

1. 什么是 useRef

useRef 是 React 中用于持久化数据(跨组件渲染周期保存值)和直接操作 DOM 元素的 Hook,核心作用是 "在组件生命周期内保持一个可变的引用"。简单说,它就像一个 "保险箱",可以存东西,而且不管组件怎么重新渲染,这个 "保险箱" 本身不会变,里面的东西却能随时拿出来用~

2. useRef 的特性

  • 返回值 :调用 useRef(initialValue) 会返回一个不可变的容器对象(ref 对象),该对象只有一个属性 current,用于存储实际值。
  • 跨渲染周期保持引用:useRef 返回的 ref 容器对象在组件的整个生命周期内引用地址不变(即使组件多次重新渲染,它还是同一个对象)。
  • 修改 current 不会触发重渲染 :直接修改 refContainer.current 的值,不会导致组件重新渲染,这是它与 useState 的核心区别。

代码举例:

咱们来看 App2.jsx 中的例子:

jsx

复制代码
import { useRef, useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0); // 响应式状态,修改会触发重渲染
  console.log('组件渲染了...'); // 每次count变化都会打印,证明重渲染
  
  // 创建ref对象,初始值为null
  const inputRef = useRef(null); 
  console.log('ref容器对象:', inputRef); // 每次渲染打印的都是同一个对象(引用不变)

  // 组件挂载完成后执行(类似vue的onMounted)
  useEffect(() => {
    console.log('input DOM元素:', inputRef.current); // 此时current已指向input元素
    inputRef.current.focus(); // 操作DOM,让输入框自动聚焦
  }, []);

  return (
    <>
      {/* 将ref对象绑定到input元素 */}
      <input ref={inputRef} />
      <p>count: {count}</p>
      {/* 点击按钮修改count,触发组件重渲染 */}
      <button onClick={() => setCount(count + 1)}>count ++</button>
    </>
  )
}

代码效果详解:

  • 当点击 count ++ 按钮时,count 状态变化会触发组件重渲染,控制台会打印 "组件渲染了..."。
  • 但每次打印的 inputRef 都是同一个对象(引用地址不变),这体现了 "跨渲染周期保持引用" 的特性。
  • useEffect 钩子在组件挂载后执行,此时 inputRef.current 已经指向了 input DOM 元素,通过 focus() 方法实现了自动聚焦,展示了 useRef 操作 DOM 的能力。
  • 即使多次修改 inputRef.current(比如手动修改它的值),也不会触发组件重渲染,这和 useState 完全不同~

注意观察,光标自动聚焦于输入框、组件渲染了而inputRef容器对象始终不变、修改inputRef.current的值不会触发重新渲染:

3. useRef 与 useState 比较 💡

特性 useRef useState
本质 用于存储 "持久化的可变值"(非响应式) 用于管理组件的响应式状态
触发重渲染 修改 current 不会触发重渲染 调用 setXxx 会触发重渲染
跨渲染周期 容器对象引用不变,current 可修改 每次渲染会创建新的状态变量(值可能相同)
适用场景 操作 DOM、存储定时器 ID、缓存数据等 管理影响 UI 展示的状态(如表单输入、开关状态等)

hooks函数中的useState 在我前面的文章里讲解过,不清楚或者感兴趣的小伙伴可以去翻出来看看。

咱们再看一个对比案例:

jsx

复制代码
// App.jsx(用普通变量存定时器ID,失败案例)
export default function App() {
  let intervalId = null; // 每次渲染都会重置为null
  const [count, setCount] = useState(0);

  function start() {
    intervalId = setInterval(() => { console.log('tick~~~'); }, 1000);
  }

  function stop() {
    clearInterval(intervalId); // 无法停止!因为intervalId已被重置
  }

  return <>{/* 按钮省略 */}</>;
}

注意观察:点击开始按钮,系统自动分配一个ID值,点击停止,可以停止因为此时ID值未被修改;再次点击开始按钮,点击count++,然后再点击停止,此时无法停止,因为ID因count值修改触发useState响应式状态修改导致重新渲染而被重置了。

复制代码
// App2.jsx(用useRef存定时器ID,成功案例)
export default function App() {
  const intervalId = useRef(null); // 跨渲染周期保持引用
  const [count, setCount] = useState(0);

  function start() {
    intervalId.current = setInterval(() => { console.log('tick~~~'); }, 1000);
  }

  function stop() {
    clearInterval(intervalId.current); // 成功停止!因为current保留了定时器ID
  }

  return <>{/* 按钮省略 */}</>;
}

注意观察:点击开始按钮,系统自动分配一个ID值,点击停止,可以停止因为此时ID值未被修改;再次点击开始按钮,点击count++,然后再点击停止,此时亦可以停止,因为useRef 声明的ID不会因组件渲染而重置,即ID值仍未被修改。

这个例子完美体现了两者的区别:普通变量会在组件重渲染时重置,而 useRef 的 current 能持久化存储值,这就是为什么管理定时器、计时器这类跨渲染周期的值时,useRef 是更好的选择~

二、受控组件与非受控组件 📝

在 React 中,表单元素(如 input、textarea、select 等)的状态管理方式分为受控组件和非受控组件,两者的核心区别在于状态由谁来管理。

1. 受控组件(Controlled Components)

(1)定义

组件的状态(value)由 React 的状态(useState)管理,表单元素的值完全受 React 控制。就像老板(React 状态)直接指挥员工(表单元素),员工做什么都得听老板的~

(2)工作原理
  • 通过 value 属性将 React 状态与表单元素绑定(单向绑定)。
  • 通过 onChange 事件监听输入变化,实时更新 React 状态。
  • 表单元素的值始终与 React 状态保持一致,形成 "数据驱动视图"。
(3)特点
  • 状态完全由 React 控制 :表单的值始终和 value 状态保持一致,不会出现 "失控" 的情况。
  • 实时可操作性 :可以随时通过修改 value 状态来改变表单值(比如重置输入、强制填写格式等)。
  • 适合场景:需要实时验证(如输入长度限制)、表单联动(如一个输入框变化影响另一个)、实时展示输入内容的场景。

2. 非受控组件(Uncontrolled Components)

(1)定义

组件的状态由 DOM 自身管理,React 不直接控制表单元素的值,而是通过 ref 访问 DOM 元素获取值。就像老板(React)不直接指挥员工(表单元素),但需要时会去 "查岗"(通过 ref 获取值)~

(2)工作原理
  • 使用 useRef 创建一个 ref 对象,绑定到表单元素。
  • 不通过 onChange 实时更新状态,而是在需要时(如提交表单)通过 ref.current.value 读取 DOM 中的值。
(3)特点
  • 状态由 DOM 管理:输入的值直接存在 DOM 中,React 不跟踪实时变化。
  • 按需获取值:只有在需要时(如提交、点击按钮)才通过 ref 读取值,减少了状态更新的频率。
  • 适合场景:一次性读取值(如表单提交)、性能敏感场景(避免频繁状态更新)、文件上传(input type="file" 必须用非受控方式)。

3. 受控组件与非受控组件比较 🆚

维度 受控组件 非受控组件
状态管理者 React 状态(useState) DOM 自身
获取值方式 直接从状态变量获取 通过 ref.current.value 获取
实时性 实时更新状态,可即时响应 不实时更新,按需读取
适用场景 实时验证、表单联动、动态反馈 一次性提交、文件上传、性能优化

如何选择?

  • 大部分场景优先用受控组件,因为它能更好地体现 React "数据驱动" 的思想,状态可控性更强。
  • 当需要操作文件(file input)、追求性能(减少状态更新)或只需要一次性获取值时,用非受控组件更合适。

4.实战演练:

废话不多说,先看代码:

jsx

复制代码
// 从 React 库中导入所需的两个 Hook:
// useState:用于创建和管理组件的响应式状态
// useRef:用于创建 DOM 元素引用,实现对原生 DOM 的直接访问
import {
  useState,
  useRef
} from 'react'

// 定义并导出 App 函数式组件,作为整个表单的根组件
export default function App() {

  // 初始化受控组件的响应式状态
  // value:存储第一个输入框的输入内容,初始值为空字符串
  // setValue:更新 value 状态的方法,调用后会触发组件重渲染,同步更新关联的 UI
  const [value, setValue] = useState('');

  // 创建一个 ref 引用对象,初始值为 null
  // 后续将绑定到第二个输入框,用于获取该输入框的原生 DOM 元素
  const inputRef = useRef(null);

  // 定义表单提交的处理函数,e 是浏览器原生的表单提交事件对象
  const doLogin = (e) => {
    // 阻止浏览器表单提交的默认行为(默认会刷新页面,破坏 React 单页应用的运行状态)
    e.preventDefault();
    // 通过 ref 引用对象的 current 属性,获取第二个输入框的 DOM 元素,读取其 value 属性(用户输入内容)并打印
    console.log(inputRef.current.value);
  }

  // 渲染组件的 UI 结构
  return (
    // 表单标签,绑定 onSubmit 事件,提交时触发 doLogin 处理函数
    <form onSubmit={doLogin}>
      <div>
        {/* 实时渲染受控组件的状态值 value,直观展示输入内容与 React 状态的同步效果 */}
        {value}
        
        {/* 第一个输入框:受控组件(由 React 状态管理输入值) */}
        <input
          type="text"  // 输入框类型为文本输入
          value={value}  // 将 React 状态 value 绑定到输入框的 value 属性,让状态控制输入框的显示内容
          onChange={(e) => setValue(e.target.value)}  // 绑定输入变化事件
          // 输入变化时,获取输入框当前值(e.target.value),调用 setValue 更新 React 状态 value
          // 实现「输入变化 → 更新状态 → 更新 UI」的闭环,保持状态与输入框内容一致
        />

        {/* 第二个输入框:非受控组件(由 DOM 自身管理输入值) */}
        {/* 不绑定 value 属性和 onChange 事件,仅通过 ref 属性绑定 inputRef,用于后续获取 DOM 元素的值 */}
        <input type="text" ref={inputRef} />
      </div>
      {/* 表单提交按钮,type="submit" 点击后会触发表单的 onSubmit 事件 */}
      <button type="submit">登录</button>
    </form>
  )
}

代码效果解释:

这段代码在一个简单的登录表单中,同时实现了受控组件非受控组件两种 React 表单处理模式:

其中第一个输入框是受控组件,它通过 useState 定义响应式状态 value,以 value={value} 实现 React 状态对输入框显示值的单向绑定,又通过 onChange 事件监听输入变化,实时调用 setValue(e.target.value) 将 DOM 输入值同步到 React 状态中,输入框的所有状态完全由 React 管控,还能通过页面上渲染 {value} 实时预览输入内容。

而第二个输入框是非受控组件,它不依赖 React 状态管理,仅通过 useRef 创建的 inputRef 绑定到 DOM 元素,输入内容直接存储在 DOM 自身的 value 属性中,不在输入过程中做实时状态同步(代码中的非受控组件仅通过 ref 绑定 DOM 元素,输入内容只存储在 DOM 自身的 value 属性中,没有触发任何 React 状态更新,也不会触发组件重渲染,因此无法直接像受控组件那样实时展示输入内容。),仅在表单提交(触发 doLogin 函数)时,通过 inputRef.current.value 按需读取 DOM 中的输入值,同时表单提交时通过 e.preventDefault() 阻止了浏览器默认的页面刷新行为,清晰对比了 React 两种表单处理方式的核心差异与实现逻辑。

三、面试官会问 🤔

  1. useRef 和 useState 的核心区别是什么? 答:useRef 存储的是 "非响应式" 的持久化值,修改 current 不会触发重渲染;useState 管理的是响应式状态,调用 setter 会触发重渲染。useRef 适合存储跨渲染周期的非 UI 相关数据(如 DOM、定时器 ID),useState 适合管理影响 UI 的状态。
  2. 为什么 useRef 能跨组件渲染周期保存值? 答:因为 useRef 返回的 ref 容器对象在组件整个生命周期内引用地址不变,即使组件重新渲染,这个对象也不会被重新创建,所以 current 属性存储的值能被持久化。
  3. 受控组件和非受控组件的区别是什么?分别适合什么场景? 答:受控组件由 React 状态管理值,通过 valueonChange 绑定,适合实时验证、表单联动;非受控组件由 DOM 管理值,通过 ref 获取,适合一次性读取、文件上传等场景。
  4. 如何用 useRef 操作 DOM 元素? 答:先用 useRef(null) 创建 ref 对象,然后通过 ref 属性绑定到 DOM 元素,当组件挂载后,ref.current 就会指向该 DOM 元素,进而可以调用 DOM 方法(如 focus()scrollIntoView())。

四、结语 🌟

今天咱们详细学习了 useRef、useState 以及受控组件和非受控组件的知识。useRef 就像一个 "持久化的工具箱",帮我们存储跨渲染周期的数据和操作 DOM;而受控与非受控组件则是表单处理的两种思路,各有适用场景。

其实这些知识点并不难,关键是多动手实践~ 比如用受控组件做一个带实时验证的登录表单,用 useRef 实现一个自动聚焦的搜索框,相信练习之后大家会理解得更透彻!😘

相关推荐
释怀不想释怀2 小时前
vue(登录,退出,浏览器本地存储机制)
前端·javascript·vue.js·ajax·html
wh_xia_jun2 小时前
vue 3极简教程草稿(未完成)
前端·javascript·vue.js
3824278272 小时前
Edge开发者工具:保留日志与禁用缓存详解
java·前端·javascript·python·selenium
web小白成长日记2 小时前
什么是margin重叠,如何解决
前端·css·html·css3
凌乱风雨12112 小时前
从源码角度解析C++20新特性如何简化线程超时取消
前端·算法·c++20
两个西柚呀2 小时前
每日前端面试题-css塌陷
前端·css
IT_陈寒2 小时前
Vite 5大实战优化技巧:让你的开发效率提升200%|2025前端工程化指南
前端·人工智能·后端
C_心欲无痕2 小时前
react - createPortal魔法传送门
javascript·vue.js·react.js
未来之窗软件服务3 小时前
幽冥大陆(八十八 ) 操作系统应用封装技术C#自解压 —东方仙盟练气期
java·前端·c#·软件打包·仙盟创梦ide·东方仙盟·阿雪技术观