彻底搞懂 React useRef:从自动聚焦到非受控表单的完整指南

useRef 详解:从自动聚焦到非受控表单,彻底掌握 React 的"持久引用"

在 React 的世界里,useState 是大家耳熟能详的主角------它负责管理状态、驱动界面更新。但还有一个低调却不可或缺的角色:useRef 。它不像 useState 那样会触发重新渲染,却在很多关键场景中默默支撑着应用的正常运行。今天,我们就用生活化的比喻和真实代码,带你彻底理解 useRef 的两大核心用途。


一、什么是 useRef?它和 useState 有什么区别?

✨ 基本定义

jsx 复制代码
const refContainer = useRef(initialValue);
  • useRef 返回一个可变的引用对象 ,其结构为 { current: initialValue }
  • 这个对象在组件的整个生命周期内保持不变(同一个引用)。
  • 修改 ref.current 不会触发组件重新渲染

与 useState 对比

特性 useState useRef
是否可变 是(通过 setter 更新) 是(直接赋值 ref.current = ...
是否触发重渲染 ✅ 是 ❌ 否
用途 管理需要反映在 UI 上的状态 存储不需要触发更新的值 / 获取 DOM 元素
初始值是否参与依赖 是(用于 useEffect 等) 否(.current 变化不会被 React 感知)

💡
useState 是"公告栏"------内容一变,全村都知道;
useRef 是"私人笔记本"------你写多少字,别人看不见,但你自己随时能查。


🎯 场景一:让输入框"自动聚焦"------挂载后立刻获得焦点

想象一下你打开一个登录页面,光标已经自动停在用户名输入框里,不用你手动点一下------是不是很贴心?这种体验背后,就离不开 useRef

来看这段代码:

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

export default function App() {
  const inputRef = useRef(null); // 创建一个"引用盒子"

  useEffect(() => {
    inputRef.current.focus(); // 页面加载完,立刻聚焦
  }, []);

  return (
    <>
      <input ref={inputRef} type="text" />
    </>
  );
}

运行展示:

🔍 它是怎么工作的?

  • useRef(null) 创建了一个持久存在的对象 ,它的 .current 属性初始为 null
  • <input ref={inputRef} /> 被渲染时,React 会自动把真实的 DOM 元素(比如 <input> 标签)赋值给 inputRef.current
  • useEffect(组件挂载后执行)中,我们调用 inputRef.current.focus(),就像对这个输入框说:"嘿,准备好接收输入吧!"

当我们在此基础上添加响应式状态时:

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

export default function App(){
  const [count, setCount] = useState(0) // 响应式状态
  
    const inputRef = useRef(null) //初始值为空
    console.log(inputRef.current);
    console.log(count);
    
    useEffect(() => {
  console.log(inputRef.current);
      inputRef.current.focus()
    }, [])
  return(
    <>
    <input ref={inputRef} type="text" />
    {count}
    <button type="button" onClick={() => setCount(count + 1)}>增加</button>
    </>
  )
}

运行程序:

我们可以发现,程序先是输出了ref和count的初始值null和0,此时返回 JSX,React 准备将 <input ref={inputRef} /> 挂载到 DOM。

React 将 JSX 渲染为真实 DOM。 此时,<input> 元素被创建,并且 React 自动将该 DOM 元素赋值给 inputRef.current。输出 < input type="text" >

当我们点击增加按钮时,触发重新渲染,程序输出1和< input type="text" >,为什么useRef的.current不是输出null ,那是因为useRef.current 一旦被 React 赋值,就会一直保留该值,直到组件卸载或手动修改。

📱 适用场景

  • 登录/注册页的首字段自动聚焦
  • 移动端减少用户点击次数,提升体验
  • 表单弹窗打开后自动定位到第一个输入框

总结

useStateuseRef 的核心区别:useState 管理响应式状态,更新会触发组件重新渲染;而 useRef 创建一个可变且持久的引用对象,其 .current 值在首次渲染时为 null(DOM 尚未挂载),挂载后指向真实 DOM 元素,后续渲染中保持引用不变,且修改它不会引起重渲染。


⏱️ 场景二:存储定时器 ID------避免"失联"的定时任务

再来看一个经典问题:为什么我启动了定时器,却无法停止它?

如果你这样写:

jsx 复制代码
// 此处省去导入
export default function App(){
    let intervalId = null
    const [count,setCount] = useState(0)
    function start(){
        intervalId = setInterval(()=>{
        console.log('tick~~~')
    },1000)
    console.log(intervalId);
}
useEffect(() =>{
 console.log(intervalId);
},[count])
function stop(){
    clearInterval(intervalId)
}

return(
 <>
  <button onClick={start}>开始</button>
  <button onClick={stop}>停止</button>
  {count}
  <button type="button" onClick={() => setCount(count + 1)}>增加</button>
 </>
)

当我们点击开始按钮时:

定时器开始每秒打印 'tick~~~'此时组件没有重新渲染 ,点击停止时,clearInterval(id) 成功清除定时器。

而当我们点击增加按钮时,就会出现这种情况:

tick~~~ 持续输出,定时器无法清理! 那是因为当我们点击增加按钮时,组件会重新渲染,React 会重新调用 App 组件函数(即重新渲染),intervalId 都会被重置为 nullclearInterval(null) 无效, 真正的定时器 ID 已经"丢失" ,无法被清除 → 'tick~~~' 持续输出!

问题在于:每次 count 变化导致组件重新渲染时,let intervalId = null 会被重新执行,之前的定时器 ID 就"丢失"了。

✅ 正确做法:用 useRef 保存 ID

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

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);
  }

  return (
    <>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      {count}
      <button onClick={() => setCount(count + 1)}>增加</button>
    </>
  );
}

🧩 为什么 useRef 能解决?

  • useRef 返回的对象在整个组件生命周期中始终是同一个对象
  • 即使组件多次重新渲染,timerId.current 依然保留上次的值。
  • 因此,stop() 总能拿到正确的定时器 ID。

上述两个场景体现了useRef 提供一个跨渲染保持不变的可变容器 ,适合存储 DOM 引用或副作用相关的标识(如定时器 ID),且修改它不会引起组件重新渲染


📝 受控 vs 非受控:表单数据的两种获取方式

React 表单有两种处理思路:受控组件非受控组件 。它们的核心区别在于:谁在掌控表单的值

1️⃣ 受控组件(Controlled Component)------"一切尽在掌握"

当表单元素的值由 React 状态(state)驱动时,这个表单元素就是一个受控组件

  • 表单元素的值由 React state 控制。
  • 必须配合 onChange 更新状态。
jsx 复制代码
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); // { username: '...', password: '...' }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="username"
        value={form.username}
        onChange={handleChange}
        placeholder="请输入用户名"
      />
      <input
        name="password"
        value={form.password}
        onChange={handleChange}
        placeholder="请输入密码"
        type="password"
      />
      <button type="submit">注册</button>
    </form>
  );
}
受控组件的核心原则

"有 value,必有 onChange。"

否则输入框会被 React "锁住",用户无法输入

如果没有onChange,会发生什么?

  1. 初始时 form.username = '',输入框为空 ✅
  2. 用户输入 "alice" → 浏览器尝试把输入框值改为 "alice"
  3. 但 React 在渲染时又强制把 value 设回 '' (因为 form.username 没变!)
  4. 结果:输入框"卡住",用户无法输入任何内容!

为什么选择受控组件?

  • 数据完全受控,便于校验、格式化、联动(如确认密码)
  • 符合 React 单向数据流理念

2️⃣ 非受控组件(Uncontrolled Component)------"用时再取"

  • 表单元素自己管理值(像传统 HTML)。
  • 通过 useRef 在需要时读取 .current.value
jsx 复制代码
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);
  };

  return (
    <div>
      <textarea ref={textareaRef} placeholder="输入评论..." />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}
  • 优点:性能略高(无状态更新),代码更简洁。
  • 适用场景:评论框、文件上传、一次性提交的简单表单。

核心区别:表单数据由谁"掌控"?

对比维度 受控组件(Controlled) 非受控组件(Uncontrolled)
数据来源 React 的 state DOM 元素自身(原生 HTML 行为)
如何更新值 通过 onChange 同步到 state 用户直接操作 DOM,React 不干预
如何读取值 直接读取 state 通过 ref.current.value 获取
是否需要 value + onChange ✅ 必须配对使用 ❌ 不需要(通常只用 ref
是否触发 re-render 每次输入都触发 无状态变化,不触发
适合场景 需要实时校验、格式化、联动的复杂表单(如登录、注册、设置页) 一次性提交、简单输入(如评论框、搜索框、文件上传)
优点 - 数据流清晰 - 易于验证/转换/联动 - 符合 React 响应式理念 - 代码简洁 - 性能略高(无频繁 setState) - 接近原生 HTML 习惯
缺点 - 代码量稍多 - 频繁输入可能引发多余渲染(可通过防抖优化) - 无法实时响应输入 - 难以实现动态校验或字段联动 - 违背"状态驱动 UI"原则

✨ 总结:useRef 的核心价值

用途 说明 示例
1. 访问 DOM 元素 获取真实 DOM,调用原生方法 .focus(), .scrollIntoView()
2. 持久存储可变值 保存不触发重渲染的数据 定时器 ID、WebSocket 实例
3. 构建非受控组件 一次性读取表单值 评论框、文件上传

记住一句话:

需要界面跟着变?用 useState
只想悄悄存个东西或操作 DOM?用 useRef

useRef 虽不张扬,却是 React 开发中不可或缺的"幕后英雄"。

掌握它,你就能在"优雅的 React"和"灵活的 DOM"之间自由切换,写出既健壮又高效的代码!

相关推荐
爱敲代码的边芙2 小时前
秋招面试准备(后端开发)
面试·职场和发展
nwsuaf_huasir2 小时前
积分旁瓣电平-matlab函数
前端·javascript·matlab
韭菜炒大葱2 小时前
React Hooks :useRef、useState 与受控/非受控组件全解析
前端·react.js·前端框架
Cache技术分享2 小时前
280. Java Stream API - Debugging Streams:如何调试 Java 流处理过程?
前端·后端
微爱帮监所写信寄信2 小时前
微爱帮监狱寄信写信小程序信件内容实时保存技术方案
java·服务器·开发语言·前端·小程序
沛沛老爹2 小时前
Web开发者实战A2A智能体交互协议:从Web API到AI Agent通信新范式
java·前端·人工智能·云原生·aigc·交互·发展趋势
这是个栗子3 小时前
【Vue代码分析】vue方法的调用与命名问题
前端·javascript·vue.js·this
全栈前端老曹3 小时前
【前端路由】Vue Router 动态导入与懒加载 - 使用 () => import(‘...‘) 实现按需加载组件
前端·javascript·vue.js·性能优化·spa·vue-router·懒加载
Zyx20073 小时前
构建现代 React 应用:从项目初始化到路由与数据获取
前端