React:useRef 超详细教程、forwardRef 详解、useImperativeHandle详解
文章目录一、React useRef 超详细教程1. 什么是 useRef2. 场景一访问 DOM 节点最常见代码示例自动聚焦输入框3. 场景二存储“不需要 UI 感知”的变量4. useRef vs useState深度对比5. 核心注意事项避坑指南① 不要在渲染期间读写 .current② 只有在必要时才使用 Ref③ Ref 无法在函数组件上直接使用6. 总结二、 forwardRef 详解打破组件黑盒1. 为什么需要 forwardRef2. 如何使用 forwardRef3. 进阶用法结合 useImperativeHandle一、React useRef 超详细教程在 React 的世界里useState负责驱动 UI 更新而useRef则是那个“静默的观察者”。它非常强大但如果用错了会让你的代码变得难以维护。这篇教程将带你深度拆解useRef的核心逻辑、应用场景以及它与useState的本质区别。1. 什么是 useRefuseRef返回一个可变的ref 对象其.current属性被初始化为传入的参数。它有两个核心特性跨渲染持久化在组件的整个生命周期内这个对象保持不变。更新不触发重新渲染修改.current的值不会导致组件重新渲染这是它与useState最大的区别。2. 场景一访问 DOM 节点最常见在 React 中我们通常通过props和state来管理 UI但有时你需要直接操作底层的 DOM 元素例如聚焦输入框、滚动到特定位置、调用浏览器 API。代码示例自动聚焦输入框import{useRef}fromreact;functionTextInputWithFocusButton(){// 1. 初始化 ref初始值为 nullconstinputEluseRef(null);constonButtonClick(){// 3. 通过 .current 访问真实的 DOM 节点// 当组件挂载后inputEl.current 将指向真实的 input 元素if(inputEl.current){inputEl.current.focus();}};return(input ref{inputEl}typetext/button onClick{onButtonClick}聚焦输入框/button/);}3. 场景二存储“不需要 UI 感知”的变量有时候你需要记录一些数据这些数据在改变时不应该触发页面刷新。比如计时器 ID、前一次的 Props 值或者记录某种操作的次数。代码示例秒表计时器import{useState,useRef}fromreact;functionStopwatch(){const[startTime,setStartTime]useState(null);const[now,setNow]useState(null);// 使用 useRef 存储 interval ID因为改变它不需要更新 UIconstintervalRefuseRef(null);functionhandleStart(){setStartTime(Date.now());setNow(Date.now());clearInterval(intervalRef.current);// 将计时器 ID 存入 refintervalRef.currentsetInterval((){setNow(Date.now());},10);}functionhandleStop(){// 停止计时直接从 ref 中取 ID不会引起额外的渲染clearInterval(intervalRef.current);}letsecondsPassed0;if(startTime!nullnow!null){secondsPassed(now-startTime)/1000;}return(h1时间{secondsPassed.toFixed(3)}/h1button onClick{handleStart}开始/buttonbutton 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. 核心注意事项避坑指南① 不要在渲染期间读写 .currentReact 期望组件是纯函数。如果你在 return 之前直接修改 ref.current可能会导致难以预测的 Bug。❌ 错误写法functionMyComponent(){constmyRefuseRef(0);myRef.currentmyRef.current1;// 严禁在渲染过程中修改returndiv{myRef.current}/div;}✅ 正确写法在useEffect或事件处理函数Event Handlers中操作。② 只有在必要时才使用 Ref如果你可以通过state和props实现功能优先使用它们。Ref 相当于 React 的“紧急出口”过度使用会让你的应用逻辑变得难以追踪。import{useRef,useEffect,useState}fromreact;functionMyComponent(){constmyRefuseRef(0);const[count,setCount]useState(0);useEffect((){// ✅ 正确在渲染完成后执行副作用myRef.currentmyRef.current1;console.log(当前 Ref 的值是:,myRef.current);});return(divpRef 值仅在控制台查看最新:{myRef.current}/pbutton onClick{()setCount(cc1)}重新渲染组件/button/div);}③ Ref 无法在函数组件上直接使用如果你想给一个函数组件添加ref属性会报错。原因函数组件没有实例。解决方案使用forwardRefAPI 将 ref 转发到子组件内部的 DOM。6. 总结useRef就像一个“盒子”你在里面放任何东西React 都会帮你存着直到组件销毁。它是操作DOM的官方指定通道。它是存储“静默变量”不影响 UI 的变量的绝佳地点。关键结论改 ref 不会刷页面二、 forwardRef 详解打破组件黑盒在 React 中组件就像一个黑盒。默认情况下你不能从父组件直接获取子组件内部的 DOM 节点或组件实例。这种限制是为了保证组件的封装性。forwardRef引用转发就是为了打破这种限制允许组件像传递普通 Props 一样将 ref 转发给其子节点。1. 为什么需要 forwardRef假设你封装了一个基础按钮组件 MyButtonfunctionMyButton(props){returnbutton classNamebtn{props.children}/button;}如果你想在父组件中让这个按钮自动聚焦constbtnRefuseRef(null);// ...MyButton ref{btnRef}点击/MyButton结果 btnRef.current 会是 null。原因 React 默认不会把 ref 作为一个 prop 传给组件。ref 属性被 React 特殊处理了就像 key 一样不会出现在 props 对象中。2. 如何使用 forwardRefforwardRef 接受一个渲染函数该函数接收两个参数props 和 ref。import{forwardRef}fromreact;constMyButtonforwardRef((props,ref){return(button ref{ref}classNamebtn{props.children}/button);});functionParent(){constbtnRefuseRef(null);consthandleClick(){// 成功获取子组件内部的 button 节点btnRef.current.focus();};return(MyButton ref{btnRef}onClick{handleClick}Focus Me/MyButton);}3. 进阶用法结合 useImperativeHandle有时候你不想把整个 DOM 节点暴露给父组件而只想暴露特定的方法例如只允许父组件调用 focus但不允许修改样式。这时需要配合 useImperativeHandle Hookimport{forwardRef,useRef,useImperativeHandle}fromreact;constFancyInputforwardRef((props,ref){constinputRefuseRef();// 自定义暴露给父组件的实例值useImperativeHandle(ref,()({focus:(){inputRef.current.focus();},shake:(){console.log(正在抖动输入框...);}}));returninput ref{inputRef}/;});父组件 现在 ref.current 只有 { focus, shake } 这两个方法而拿不到真实的 DOM 节点。这符合最小暴露原则。