两个用途
// 1. 拿 DOM 节点
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus();
// 2. 存任意值(不触发 re-render)
const idRef = useRef(0);
idRef.current++; // 改它不会让组件重新渲染
ref vs state
| state | ref | |
|---|---|---|
| 变化触发 re-render | ✅ | ❌ |
| 渲染中读 | 不变(render 内是快照) | 总是最新 |
| 适合 | UI 显示的值 | 不需要显示、需要跨 render 存的值 |
判断标准:这个值变了 UI 要更新吗?要 → state;不要 → ref。
常见 ref 场景
计时器 ID:
const timerRef = useRef<NodeJS.Timeout | null>(null);
const start = () => {
timerRef.current = setInterval(...);
};
const stop = () => {
if (timerRef.current) clearInterval(timerRef.current);
};
保存上一个值:
function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => { ref.current = value; });
return ref.current;
}
避免重复执行:
const initedRef = useRef(false);
useEffect(() => {
if (initedRef.current) return;
initedRef.current = true;
// 真正只想跑一次的初始化
}, []);
最新值(解 stale closure):
const valueRef = useRef(value);
useEffect(() => { valueRef.current = value; });
// 在回调里读 valueRef.current 总是最新
拿 DOM 操作的常见动作
inputRef.current?.focus();
inputRef.current?.scrollIntoView();
inputRef.current?.select();
videoRef.current?.play();
canvasRef.current?.getContext('2d');
forwardRef
子组件转发 ref 给内部 DOM:
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});
// 父组件
<Input ref={myRef} />
React 19+:函数组件直接可以接收
ref作为普通 prop——不再需要forwardRef包装。新项目优先用 19+ 写法。
useImperativeHandle
让父组件能调子组件暴露的方法:
const FancyInput = forwardRef((props, ref) => {
const localRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => localRef.current?.focus(),
clear: () => { if (localRef.current) localRef.current.value = ''; },
}));
return <input ref={localRef} />;
});
// 父组件
const r = useRef<{ focus: () => void; clear: () => void }>(null);
r.current?.focus();
慎用——大部分时候用 props + state 就行。它是命令式接口,逆 React 数据流。
坑
- render 中读写 ref:技术上能跑,但是反模式。ref 操作放在事件回调或 effect 里。
- ref 的初始值:
useRef(initial)在每次 render 都创建对象,但只在第一次保留。给 ref 传函数没用——它不会调用。 - 回调 ref:
<div ref={(node) => { ... }} />——节点变化时调用,可以做更细的控制。
实战例子:点击外部关闭
function useClickOutside(handler: () => void) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const onClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) handler();
};
document.addEventListener('mousedown', onClick);
return () => document.removeEventListener('mousedown', onClick);
}, [handler]);
return ref;
}
// 用法
const popoverRef = useClickOutside(() => setOpen(false));
<div ref={popoverRef}>...</div>