首页
useRef
useRef 用于创建可变引用,访问 DOM 元素或存储不触发渲染的值。
useRef Hook 详解
useRef 用于创建可变引用,主要用于访问 DOM 元素和存储不触发渲染的值。
// 基本语法
// DOM 引用 const inputRef = useRef<HTMLInputElement>(null); // 存储可变值 const countRef = useRef(0); // 访问 inputRef.current?.focus(); countRef.current += 1;
示例 1: DOM 元素引用
输入框操作
视频控制
说明: useRef 可以获取 DOM 元素的引用,直接调用其原生方法。
示例 2: useRef vs useState
组件渲染次数: 1
useRef (不触发渲染)
0
数字不会变化,打开控制台查看
useState (触发渲染)
0
每次点击都会重新渲染
关键区别:
- useRef 修改值不会触发重新渲染
- useState 修改值会触发重新渲染
- useRef 的值在渲染之间保持不变
示例 3: 秒表(定时器管理)
00:00.0
为什么用 useRef 存储 intervalId?
- 定时器 ID 不需要显示在 UI 上
- 修改它不需要触发重新渲染
- 需要在多次渲染间保持同一个引用以便清除
示例 4: 保存上一次的值
当前值
0
上一次值
-
当前: React
上一次: -
usePrevious 自定义 Hook:
function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}示例 5: 避免闭包陷阱
测试方法:
- 点击任意一个按钮
- 在 3 秒内修改输入框内容
- 观察弹出的消息是旧值还是新值
示例 6: forwardRef + useImperativeHandle
关键点:
- forwardRef 允许父组件传递 ref 给子组件
- useImperativeHandle 自定义暴露给父组件的方法
- 可以隐藏实现细节,只暴露需要的接口
API 文档
useRef Hook 详解
什么是 useRef?
useRef 是 React 的一个基础 Hook,用于创建一个可变的引用对象,该对象在组件的整个生命周期内保持不变。
const ref = useRef(initialValue);
useRef 的两大用途
用途 1:访问 DOM 元素
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</>
);
}
用途 2:存储可变值(不触发重新渲染)
function Timer() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(`点击了 ${countRef.current} 次`);
// 注意:UI 不会更新!
};
return <button onClick={handleClick}>点击</button>;
}
基本语法
const ref = useRef<T>(initialValue);
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
initialValue | T | 初始值,只在首次渲染时使用 |
返回值
返回一个 MutableRefObject:
{
current: T // 可读写的值
}
useRef vs useState
| 特性 | useRef | useState |
|---|---|---|
| 更新时重新渲染 | ❌ 不会 | ✅ 会 |
| 值保持最新 | ✅ 立即更新 | ❌ 下次渲染 |
| 适用场景 | DOM 引用、定时器 ID | UI 状态 |
| 在渲染中读取 | ⚠️ 不推荐 | ✅ 推荐 |
使用场景
场景 1:访问 DOM 元素
function VideoPlayer() {
const videoRef = useRef<HTMLVideoElement>(null);
const play = () => videoRef.current?.play();
const pause = () => videoRef.current?.pause();
return (
<div>
<video ref={videoRef} src="/video.mp4" />
<button onClick={play}>播放</button>
<button onClick={pause}>暂停</button>
</div>
);
}
场景 2:存储定时器 ID
function Stopwatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const start = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
const reset = () => {
stop();
setTime(0);
};
// 清理
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<p>{time} 秒</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
<button onClick={reset}>重置</button>
</div>
);
}
场景 3:保存上一次的值
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>当前: {count},之前: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
场景 4:跟踪组件是否已挂载
function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
function AsyncComponent() {
const isMounted = useIsMounted();
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(result => {
// 只有组件还挂载时才更新状态
if (isMounted.current) {
setData(result);
}
});
}, []);
return <div>{data}</div>;
}
场景 5:避免闭包陷阱
function Chat() {
const [message, setMessage] = useState('');
const messageRef = useRef(message);
// 保持 ref 与 state 同步
useEffect(() => {
messageRef.current = message;
}, [message]);
const sendDelayedMessage = () => {
setTimeout(() => {
// 使用 ref 获取最新值,而不是闭包中的旧值
alert(`发送消息: ${messageRef.current}`);
}, 3000);
};
return (
<div>
<input
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button onClick={sendDelayedMessage}>
3秒后发送
</button>
</div>
);
}
转发 ref(forwardRef)
当需要让父组件访问子组件的 DOM 时:
// 子组件
const FancyInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} className="fancy" {...props} />;
});
// 父组件
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
聚焦
</button>
</div>
);
}
常见错误
错误 1:在渲染期间读写 ref
// ❌ 错误:在渲染期间修改 ref
function Counter() {
const countRef = useRef(0);
countRef.current += 1; // 不要这样做!
return <div>{countRef.current}</div>;
}
// ✅ 正确:在事件处理或 useEffect 中修改
function Counter() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
};
return <button onClick={handleClick}>点击</button>;
}
错误 2:期望 ref 变化触发渲染
// ❌ 错误:期望 UI 更新
function Counter() {
const countRef = useRef(0);
return (
<div>
<p>{countRef.current}</p> {/* 不会更新! */}
<button onClick={() => countRef.current++}>+1</button>
</div>
);
}
// ✅ 正确:需要 UI 更新时使用 useState
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
错误 3:忘记 null 检查
// ❌ 可能出错
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
const focus = () => {
inputRef.current.focus(); // TypeScript 报错
};
}
// ✅ 正确:使用可选链
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
const focus = () => {
inputRef.current?.focus();
};
}
与 useImperativeHandle 配合
自定义暴露给父组件的实例值:
interface InputHandle {
focus: () => void;
clear: () => void;
}
const CustomInput = forwardRef<InputHandle, Props>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}));
return <input ref={inputRef} {...props} />;
});
// 父组件
function Parent() {
const inputRef = useRef<InputHandle>(null);
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => inputRef.current?.clear()}>清空</button>
</div>
);
}
最佳实践
1. 选择正确的工具
// 需要触发渲染?用 useState
const [count, setCount] = useState(0);
// 只需要存储值?用 useRef
const countRef = useRef(0);
2. 类型安全
// DOM 元素初始化为 null
const divRef = useRef<HTMLDivElement>(null);
// 普通值提供类型
const countRef = useRef<number>(0);
// 可能为 undefined 的值
const valueRef = useRef<string | undefined>(undefined);
3. 清理定时器
function Timer() {
const timerRef = useRef<NodeJS.Timeout>();
useEffect(() => {
timerRef.current = setInterval(() => {
// ...
}, 1000);
// 清理函数
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
}
总结
| 场景 | 使用 |
|---|---|
| 访问 DOM 元素 | useRef<HTMLElement>(null) |
| 存储定时器 ID | useRef<NodeJS.Timeout>() |
| 保存上一次的值 | useRef<T>() |
| 避免闭包陷阱 | useRef 配合 useEffect |
| 存储不触发渲染的值 | useRef<T>(initialValue) |
useRef 是 React 中非常基础但强大的 Hook,正确使用它可以解决很多与 DOM 操作和值持久化相关的问题。