React:useRef 超详细教程、forwardRef 详解、useImperativeHandle详解

📅 2026/7/5 12:53:03 👁️ 阅读次数 📝 编程学习
React:useRef 超详细教程、forwardRef 详解、useImperativeHandle详解

文章目录

  • 一、React useRef 超详细教程
    • 1. 什么是 useRef?
    • 2. 场景一:访问 DOM 节点(最常见)
      • 代码示例:自动聚焦输入框
    • 3. 场景二:存储“不需要 UI 感知”的变量
    • 4. useRef vs useState:深度对比
    • 5. 核心注意事项(避坑指南)
      • ① 不要在渲染期间读写 .current
      • ② 只有在必要时才使用 Ref
      • ③ Ref 无法在函数组件上直接使用
    • 6. 总结
  • 二、 forwardRef 详解:打破组件黑盒
    • 1. 为什么需要 forwardRef?
    • 2. 如何使用 forwardRef
    • 3. 进阶用法:结合 useImperativeHandle

一、React useRef 超详细教程

在 React 的世界里,useState负责驱动 UI 更新,而useRef则是那个“静默的观察者”。它非常强大,但如果用错了,会让你的代码变得难以维护。

这篇教程将带你深度拆解useRef的核心逻辑、应用场景以及它与useState的本质区别。


1. 什么是 useRef?

useRef返回一个可变的ref 对象,其.current属性被初始化为传入的参数。它有两个核心特性:

  • 跨渲染持久化:在组件的整个生命周期内,这个对象保持不变。
  • 更新不触发重新渲染:修改.current的值不会导致组件重新渲染(这是它与useState最大的区别)。

2. 场景一:访问 DOM 节点(最常见)

在 React 中,我们通常通过propsstate来管理 UI,但有时你需要直接操作底层的 DOM 元素(例如:聚焦输入框、滚动到特定位置、调用浏览器 API)。

代码示例:自动聚焦输入框

import{useRef}from'react';functionTextInputWithFocusButton(){// 1. 初始化 ref,初始值为 nullconstinputEl=useRef(null);constonButtonClick=()=>{// 3. 通过 .current 访问真实的 DOM 节点// 当组件挂载后,inputEl.current 将指向真实的 <input> 元素if(inputEl.current){inputEl.current.focus();}};return(<><input ref={inputEl}type="text"/><button onClick={onButtonClick}>聚焦输入框</button></>);}

3. 场景二:存储“不需要 UI 感知”的变量

有时候你需要记录一些数据,这些数据在改变时不应该触发页面刷新。比如计时器 ID、前一次的 Props 值,或者记录某种操作的次数。

代码示例:秒表计时器

import{useState,useRef}from'react';functionStopwatch(){const[startTime,setStartTime]=useState(null);const[now,setNow]=useState(null);// 使用 useRef 存储 interval ID,因为改变它不需要更新 UIconstintervalRef=useRef(null);functionhandleStart(){setStartTime(Date.now());setNow(Date.now());clearInterval(intervalRef.current);// 将计时器 ID 存入 refintervalRef.current=setInterval(()=>{setNow(Date.now());},10);}functionhandleStop(){// 停止计时,直接从 ref 中取 ID,不会引起额外的渲染clearInterval(intervalRef.current);}letsecondsPassed=0;if(startTime!=null&&now!=null){secondsPassed=(now-startTime)/1000;}return(<><h1>时间:{secondsPassed.toFixed(3)}</h1><button onClick={handleStart}>开始</button><button onClick={handleStop}>停止</button></>);}

4. useRef vs useState:深度对比

特性useStateuseRef普通变量 (let/const)
返回值[state, setState]{ current: ... }变量本身
修改方式调用setState(newValue)直接修改ref.current = newValue直接重新赋值
触发渲染触发组件 Re-render不会触发渲染不会触发渲染
持久性渲染间持久化(重绘后值保留)渲染间持久化(重绘后值保留)无法持久化(每次函数执行都会重置)
用途存储驱动 UI 显示的数据(状态)存储 DOM 节点、Timer ID 或不需要展示在页面上的逻辑变量临时计算、函数内部的局部逻辑
同步/异步状态更新通常是异步的(在闭包中读取旧值)修改是同步的,值立即改变同步修改

5. 核心注意事项(避坑指南)

① 不要在渲染期间读写 .current

React 期望组件是纯函数。如果你在 return 之前直接修改 ref.current,可能会导致难以预测的 Bug。

❌ 错误写法:

functionMyComponent(){constmyRef=useRef(0);myRef.current=myRef.current+1;// 严禁在渲染过程中修改return<div>{myRef.current}</div>;}
  • ✅ 正确写法:
    useEffect或事件处理函数(Event Handlers)中操作。

② 只有在必要时才使用 Ref

如果你可以通过stateprops实现功能,优先使用它们。Ref 相当于 React 的“紧急出口”,过度使用会让你的应用逻辑变得难以追踪。

import{useRef,useEffect,useState}from'react';functionMyComponent(){constmyRef=useRef(0);const[count,setCount]=useState(0);useEffect(()=>{// ✅ 正确:在渲染完成后执行副作用myRef.current=myRef.current+1;console.log("当前 Ref 的值是:",myRef.current);});return(<div><p>Ref 值(仅在控制台查看最新):{myRef.current}</p><button onClick={()=>setCount(c=>c+1)}>重新渲染组件</button></div>);}

③ Ref 无法在函数组件上直接使用

如果你想给一个函数组件添加ref属性,会报错。

  • 原因:函数组件没有实例。
  • 解决方案:使用forwardRefAPI 将 ref 转发到子组件内部的 DOM。

6. 总结

  • useRef就像一个“盒子”,你在里面放任何东西,React 都会帮你存着,直到组件销毁。
  • 它是操作DOM的官方指定通道。
  • 它是存储“静默变量”(不影响 UI 的变量)的绝佳地点。
  • 关键结论:改 ref 不会刷页面!

二、 forwardRef 详解:打破组件黑盒

在 React 中,组件就像一个黑盒。默认情况下,你不能从父组件直接获取子组件内部的 DOM 节点或组件实例。这种限制是为了保证组件的封装性。

forwardRef(引用转发)就是为了打破这种限制,允许组件像传递普通 Props 一样,将 ref 转发给其子节点。

1. 为什么需要 forwardRef?

假设你封装了一个基础按钮组件 MyButton:

functionMyButton(props){return<button className="btn">{props.children}</button>;}

如果你想在父组件中让这个按钮自动聚焦:

constbtnRef=useRef(null);// ...<MyButton ref={btnRef}>点击</MyButton>

结果: btnRef.current 会是 null。
原因: React 默认不会把 ref 作为一个 prop 传给组件。ref 属性被 React 特殊处理了,就像 key 一样,不会出现在 props 对象中。


2. 如何使用 forwardRef

forwardRef 接受一个渲染函数,该函数接收两个参数:props 和 ref。

import{forwardRef}from'react';constMyButton=forwardRef((props,ref)=>{return(<button ref={ref}className="btn">{props.children}</button>);});
functionParent(){constbtnRef=useRef(null);consthandleClick=()=>{// 成功获取子组件内部的 button 节点btnRef.current.focus();};return(<MyButton ref={btnRef}onClick={handleClick}>Focus Me</MyButton>);}

3. 进阶用法:结合 useImperativeHandle

有时候,你不想把整个 DOM 节点暴露给父组件,而只想暴露特定的方法(例如:只允许父组件调用 focus,但不允许修改样式)。

这时需要配合 useImperativeHandle Hook:

import{forwardRef,useRef,useImperativeHandle}from'react';constFancyInput=forwardRef((props,ref)=>{constinputRef=useRef();// 自定义暴露给父组件的实例值useImperativeHandle(ref,()=>({focus:()=>{inputRef.current.focus();},shake:()=>{console.log("正在抖动输入框...");}}));return<input ref={inputRef}/>;});

父组件: 现在 ref.current 只有 { focus, shake } 这两个方法,而拿不到真实的 DOM 节点。这符合最小暴露原则。