文章目录
- [一、React useRef 超详细教程](#一、React useRef 超详细教程)
-
- [1. 什么是 useRef?](#1. 什么是 useRef?)
- [2. 场景一:访问 DOM 节点(最常见)](#2. 场景一:访问 DOM 节点(最常见))
- [3. 场景二:存储"不需要 UI 感知"的变量](#3. 场景二:存储“不需要 UI 感知”的变量)
- [4. useRef vs useState:深度对比](#4. useRef vs useState:深度对比)
- [5. 核心注意事项(避坑指南)](#5. 核心注意事项(避坑指南))
-
- [① 不要在渲染期间读写 .current](#① 不要在渲染期间读写 .current)
- [② 只有在必要时才使用 Ref](#② 只有在必要时才使用 Ref)
- [③ Ref 无法在函数组件上直接使用](#③ Ref 无法在函数组件上直接使用)
- [6. 总结](#6. 总结)
- [二、 forwardRef 详解:打破组件黑盒](#二、 forwardRef 详解:打破组件黑盒)
-
- [1. 为什么需要 forwardRef?](#1. 为什么需要 forwardRef?)
- [2. 如何使用 forwardRef](#2. 如何使用 forwardRef)
- [3. 进阶用法:结合 useImperativeHandle](#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)。
代码示例:自动聚焦输入框
javascript
import { useRef } from 'react';
function TextInputWithFocusButton() {
// 1. 初始化 ref,初始值为 null
const inputEl = useRef(null);
const onButtonClick = () => {
// 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 值,或者记录某种操作的次数。
代码示例:秒表计时器
javascript
import { useState, useRef } from 'react';
function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
// 使用 useRef 存储 interval ID,因为改变它不需要更新 UI
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
// 将计时器 ID 存入 ref
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
// 停止计时,直接从 ref 中取 ID,不会引起额外的渲染
clearInterval(intervalRef.current);
}
let secondsPassed = 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。
❌ 错误写法:
javascript
function MyComponent() {
const myRef = useRef(0);
myRef.current = myRef.current + 1; // 严禁在渲染过程中修改
return <div>{myRef.current}</div>;
}
- ✅ 正确写法:
在useEffect或事件处理函数(Event Handlers)中操作。
② 只有在必要时才使用 Ref
如果你可以通过 state 和 props 实现功能,优先使用它们。Ref 相当于 React 的"紧急出口",过度使用会让你的应用逻辑变得难以追踪。
javascript
import { useRef, useEffect, useState } from 'react';
function MyComponent() {
const myRef = 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:
javascript
function MyButton(props) {
return <button className="btn">{props.children}</button>;
}
如果你想在父组件中让这个按钮自动聚焦:
javascript
const btnRef = useRef(null);
// ...
<MyButton ref={btnRef}>点击</MyButton>
结果: btnRef.current 会是 null。
原因: React 默认不会把 ref 作为一个 prop 传给组件。ref 属性被 React 特殊处理了,就像 key 一样,不会出现在 props 对象中。
2. 如何使用 forwardRef
forwardRef 接受一个渲染函数,该函数接收两个参数:props 和 ref。
javascript
import { forwardRef } from 'react';
const MyButton = forwardRef((props, ref) => {
return (
<button ref={ref} className="btn">
{props.children}
</button>
);
});
javascript
function Parent() {
const btnRef = useRef(null);
const handleClick = () => {
// 成功获取子组件内部的 button 节点
btnRef.current.focus();
};
return (
<MyButton ref={btnRef} onClick={handleClick}>
Focus Me
</MyButton>
);
}
3. 进阶用法:结合 useImperativeHandle
有时候,你不想把整个 DOM 节点暴露给父组件,而只想暴露特定的方法(例如:只允许父组件调用 focus,但不允许修改样式)。
这时需要配合 useImperativeHandle Hook:
javascript
import { forwardRef, useRef, useImperativeHandle } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 自定义暴露给父组件的实例值
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
shake: () => {
console.log("正在抖动输入框...");
}
}));
return <input ref={inputRef} />;
});
父组件: 现在 ref.current 只有 { focus, shake } 这两个方法,而拿不到真实的 DOM 节点。这符合最小暴露原则。