第 12 章:useRef
学习目标
- 理解 useRef 的两种用法:DOM 引用 vs 跨渲染存值
- 掌握 ref 操作 DOM(聚焦、滚动、播放)
- 用 ref 解决闭包陷阱和定时器存储
- 避开 useRef 的 3 个常见坑
一、useRef 是什么?
useRef 返回一个可变的 ref 对象,.current 属性可读写:
tsx
const ref = useRef<T>(initialValue);
ref.current; // 读
ref.current = x; // 写特点:
- 修改 ref 不会触发重渲染
- 同一个 ref 在多次渲染间保持同一个引用
- 常用于 DOM 引用 / 跨渲染存值
💡 useRef = 跨 render 持久存在的可变盒子——普通
let每次 render 都重置,没法跨 render 存值。定时器 id / DOM / "不想重渲染的计数器" 都用 ref。
二、用法 1:引用 DOM
tsx
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus(); // ⚠️ current 可能为 null
}
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦输入框</button>
</>
);
}⚠️ 坑 1:
ref.current在 mount 之前是null,组件首次渲染时不能依赖它;用可选链?.。
三、常见 DOM 操作
tsx
// 滚动到元素
function ScrollTo() {
const ref = useRef<HTMLDivElement>(null);
return (
<>
<button onClick={() => ref.current?.scrollIntoView({ behavior: 'smooth' })}>滚动</button>
<div ref={ref}>目标元素</div>
</>
);
}
// 自动聚焦
function AutoFocus() {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => { ref.current?.focus(); }, []);
return <input ref={ref} />;
}
// 视频控制
function Video() {
const ref = useRef<HTMLVideoElement>(null);
return (
<>
<video ref={ref} src="..." />
<button onClick={() => ref.current?.play()}>播放</button>
<button onClick={() => ref.current?.pause()}>暂停</button>
</>
);
}四、用法 2:跨渲染存值(不触发重渲染)
tsx
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef<number | null>(null);
function start() {
intervalRef.current = window.setInterval(() => {
setCount(c => c + 1);
}, 1000);
}
function stop() {
if (intervalRef.current) clearInterval(intervalRef.current);
}
return (
<>
<p>{count}s</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</>
);
}为什么不用 useState?
- 存定时器 id 不需要触发 UI 更新
- 用
useState会浪费渲染
五、解决闭包陷阱
tsx
function Comp() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
// 始终能读到最新值
useEffect(() => {
const id = setInterval(() => {
console.log('最新 count:', countRef.current);
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// 每次渲染同步更新 ref
useEffect(() => {
countRef.current = count;
}, [count]);
return <div>{count}</div>;
}六、forwardRef + useImperativeHandle
让父组件通过 ref 调用子组件暴露的方法:
tsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
interface MyInputRef {
focus: () => void;
clear: () => void;
}
const MyInput = forwardRef<MyInputRef, { defaultValue?: string }>(({ defaultValue }, ref) => {
const innerRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => innerRef.current?.focus(),
clear: () => {
if (innerRef.current) innerRef.current.value = '';
},
}));
return <input ref={innerRef} defaultValue={defaultValue} />;
});
// 父组件使用
function Parent() {
const ref = useRef<MyInputRef>(null);
return (
<>
<MyInput ref={ref} defaultValue="hello" />
<button onClick={() => ref.current?.focus()}>聚焦</button>
<button onClick={() => ref.current?.clear()}>清空</button>
</>
);
}⚠️ 坑 2:
forwardRef在 React 19 已经合并到普通 ref,新代码可以省去forwardRef,但旧项目还在用。💡 useImperativeHandle ≈ Vue3 的
defineExpose——两者都是子组件主动声明"我给父组件用啥 API"。
React Vue3 暴露 API 给父 useImperativeHandledefineExpose父拿子实例 ref.currentcompRef.value💡 forwardRef 签名拆解:第一个泛型 = ref 类型,第二个 = props 类型;第一个参数 = props 对象,第二个参数 = ref——
forwardRef<RefType, PropsType>((props, ref) => {...})。💡 forwardRef 是"让 ref 进得来",useImperativeHandle 是"控制 ref 给啥"——React 19 简化了前者,但后者仍要用——只暴露你想给的 API,保护内部 DOM。
七、回调 ref
tsx
function CallbackRef() {
const [height, setHeight] = useState(0);
// 元素挂载/卸载时调用,参数是 DOM 节点或 null
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node) setHeight(node.getBoundingClientRect().height);
}, []);
return <div ref={measuredRef}>内容</div>;
}八、useRef vs useState
| 维度 | useRef | useState |
|---|---|---|
| 触发重渲染 | ❌ | ✅ |
| 跨渲染共享 | ✅ | ✅(通过 state 本身) |
| 适合场景 | DOM / 定时器 / 上次值 | UI 展示数据 |
九、本章小结
| 要点 | 关键 |
|---|---|
| 两种用法 | DOM 引用 / 跨渲染存值 |
.current | 读写,不触发渲染 |
?. 可选链 | ref.current?.focus() 防 null |
| 闭包陷阱 | ref 存最新值,定时器/事件里读 |
forwardRef | React 18 仍用,React 19 可省 |
| vs useState | ref 适合不需要渲染 UI 的值 |
动手练习
- 自动聚焦:挂载时自动聚焦输入框
- 滚动按钮:点按钮滚动到指定 div
- 计时器:用 ref 存定时器 id,清理时不依赖 stale state
- 找坑:以下代码哪里有问题?tsx
function Bad() { const ref = useRef(0); return <div>{ref.current}</div>; // 改 ref 后 UI 不更新 }
下一章:第 13 章:useContext →