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 中,我们通常通过props和state来管理 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:深度对比
| 特性 | useState | useRef | 普通变量 (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
如果你可以通过state和props实现功能,优先使用它们。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 节点。这符合最小暴露原则。
