彻底搞懂 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"之间自由切换,写出既健壮又高效的代码!

相关推荐
恋猫de小郭1 分钟前
社区版 Dart macros?一个可以解决 JSON 序列化的Fluter “宏编程”第三方包
android·前端·flutter
马丁玩编程36 分钟前
GitHub 3.5k Star 之后,Ragent AI 框架新版本来了
后端·面试·github
2601_9657984736 分钟前
Contractor Web Setup: Deploying Bathrooms & Kitchens Theme Fast
前端·web3·php·wordpress
Dxy123931021640 分钟前
Python XPath position() 完整使用指南,避坑合集(lxml适用)
前端·javascript·python
名字还没想好☜1 小时前
React 19 useOptimistic 实战:点赞评论先更新 UI,失败自动回滚
前端·javascript·react.js·ui·react·useoptimistic
晴天161 小时前
HarmonyOS 和 React 对比-Day22
前端·华为·harmonyos
程序员爱钓鱼1 小时前
Rust Trait详解:定义共享行为与抽象接口
后端·面试·rust
程序员爱钓鱼1 小时前
Go 编程实战:切片 Slice——灵活的动态数据集合
后端·面试·go
AlienZHOU7 小时前
DeepSeek Harness 插件:HTML 实时可视化编辑
前端·agent·deepseek
欧阳天羲9 小时前
前端性能优化实战:从白屏到秒开
前端·性能优化