HookReact 18+
useRef: 引用 DOM 和值
useRef 让你能够引用一个不需要渲染的值,或者直接访问 DOM 元素
什么是 useRef?
useRef 返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
TypeScript
import { useRef } from 'react';
const ref = useRef(initialValue);
主要用途
- 访问 DOM 元素
- 保存不需要触发重新渲染的可变值
- 存储定时器 ID
- 存储之前的 props 或 state
ref 不会触发重新渲染
改变 ref 的值不会触发组件重新渲染。这是 ref 与 state 的主要区别。
访问 DOM 元素
React 支持一个特殊的属性 ref,可以附加到任何组件或元素上。
基本示例: 聚焦输入框
TypeScript
import { useRef, useEffect } from 'react';
function Form() {
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后自动聚焦
inputRef.current.focus();
}, []);
return (
<div>
<label>
姓名:
<input ref={inputRef} type="text" />
</label>
</div>
);
}
示例: 滚动到元素
TypeScript
function ChatMessages() {
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [/* 依赖项 */]);
return (
<div>
{/* 消息列表 */}
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
{/* 用于滚动定位的元素 */}
<div ref={messagesEndRef} />
</div>
);
}
ref 只能在某些元素上使用
- 原生 HTML 元素: ✅ 可以使用 ref
- 函数组件: ❌ 需要使用
forwardRef - 类组件: ✅ 可以使用 ref
使用 forwardRef 暴露 ref
TypeScript
import { forwardRef } from 'react';
// 使用 forwardRef 让父组件可以访问子组件的 DOM
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
function Form() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>聚焦输入框</button>
</>
);
}
ref 回调
TypeScript
function Form() {
const inputRef = useCallback((node) => {
if (node) {
// node 是 DOM 元素
node.focus();
}
}, []);
return <input ref={inputRef} />;
}
保存可变值
ref 可以保存任何可变值,类似于类组件的实例属性。改变
ref.current 不会触发重新渲染。
示例:定时器 ID
TypeScript
import { useState, useEffect, useRef } from 'react';
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<div>已经运行: {seconds} 秒</div>
<button onClick={stop}>停止</button>
</div>
);
}
示例:存储之前的值
TypeScript
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>当前: {count}</p>
<p>之前: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
ref vs state
| 特性 | ref | state |
|---|---|---|
| 改变后 | 不触发重新渲染 | 触发重新渲染 |
| 访问方式 | ref.current | 直接访问状态值 |
| 异步更新 | 同步更新 | 异步批处理 |
| 用途 | DOM 引用、定时器 ID | 渲染数据 |
useRef vs useState
问题:状态过期
TypeScript
// ❌ 使用 state 导致问题
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// 这里看到的 count 始终是 0(闭包陷阱)
console.log(count);
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组
return <div>{count}</div>;
}
TypeScript
// ✅ 使用 ref 解决
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
useEffect(() => {
const timer = setInterval(() => {
// 使用 ref 获取最新值
countRef.current += 1;
setCount(countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
何时使用 ref 而不是 state?
- 当值的改变不需要触发重新渲染时
- 当需要在 effect 中读取最新值时
- 当存储与渲染无关的数据时(如定时器 ID)
useImperativeHandle:暴露自定义实例
useImperativeHandle 可以自定义暴露给父组件的 ref 值。
TypeScript
import { useRef, useImperativeHandle, forwardRef } from 'react';
// 子组件
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 自定义暴露给父组件的方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
},
getValue: () => {
return inputRef.current.value;
},
}));
return <input ref={inputRef} {...props} />;
});
// 父组件
function Form() {
const inputRef = useRef();
const handleClick = () => {
// 调用子组件暴露的方法
inputRef.current.focus();
inputRef.current.clear();
const value = inputRef.current.getValue();
console.log('输入值:', value);
};
return (
<>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>聚焦并清空</button>
</>
);
}
谨慎使用
useImperativeHandle 应该谨慎使用,因为它破坏了组件的封装性。
大多数情况下,应该通过 props 来控制组件。
常见使用场景
1. 集成第三方库
TypeScript
function Chart({ data }) {
const canvasRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
if (canvasRef.current) {
// 初始化图表库
chartRef.current = new Chart(canvasRef.current, {
type: 'bar',
data: data,
});
}
return () => {
// 清理图表实例
if (chartRef.current) {
chartRef.current.destroy();
}
};
}, []);
// 更新图表数据
useEffect(() => {
if (chartRef.current) {
chartRef.current.data = data;
chartRef.current.update();
}
}, [data]);
return <canvas ref={canvasRef} />;
}
2. 检测元素是否可见
TypeScript
function LazyImage({ src, alt }) {
const imgRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef} style={{ minHeight: '200px' }}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div>加载中...</div>
)}
</div>
);
}
3. 可变值的计数器
TypeScript
function ClickLogger() {
const [logs, setLogs] = useState([]);
const clickCountRef = useRef(0);
const handleClick = () => {
clickCountRef.current += 1;
setLogs(prev => [
...prev,
`点击 #${clickCountRef.current} at ${new Date().toLocaleTimeString()}`,
]);
};
return (
<div>
<button onClick={handleClick}>记录点击</button>
<ul>
{logs.map((log, index) => (
<li key={index}>{log}</li>
))}
</ul>
</div>
);
}
4. 防抖和节流
TypeScript
function useDebounce(callback, delay) {
const timeoutRef = useRef();
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const debouncedCallback = useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
return debouncedCallback;
}
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedSearch = useDebounce((value) => {
console.log('搜索:', value);
}, 500);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return <input value={query} onChange={handleChange} />;
}
常见错误
1. 在渲染中读取 ref.current
TypeScript
// ❌ 错误:在渲染中读取 ref 会导致问题
function Counter() {
const countRef = useRef(0);
return <div>{countRef.current}</div>; // 不会更新
}
// ✅ 正确:使用 state
function Counter() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
2. 在渲染中修改 ref.current
TypeScript
// ❌ 错误:每次渲染都会执行
function Component() {
const ref = useRef(0);
ref.current += 1; // 每次渲染都增加
return <div>{ref.current}</div>;
}
// ✅ 正确:在 effect 中修改
function Component() {
const ref = useRef(0);
useEffect(() => {
ref.current += 1;
});
return <div>{ref.current}</div>;
}
3. 忘记检查 ref.current 是否存在
TypeScript
// ❌ 错误:可能导致 null 错误
useEffect(() => {
inputRef.current.focus(); // 如果 ref 为空会报错
}, []);
// ✅ 正确:检查是否存在
useEffect(() => {
inputRef.current?.focus(); // 使用可选链
}, []);
4. ref 在首次渲染前是 null
TypeScript
function Component() {
const ref = useRef(null);
console.log(ref.current); // null(首次渲染)
useEffect(() => {
console.log(ref.current); // DOM 元素(渲染后)
}, []);
return <div ref={ref} />;
}
最佳实践
- 1. 只在必要时使用 ref
大多数情况下,应该使用 state 而不是 ref。ref 只用于特定场景
- 2. 使用可选链访问 ref
使用
ref.current?.method()避免可能的 null 错误 - 3. 在 useEffect 中访问 DOM
ref 在首次渲染前是 null,应该在 effect 中访问
- 4. 清理 ref 中的资源
如果 ref 中存储了需要清理的资源(如定时器、订阅),记得在清理函数中清理
- 5. 避免过度使用 useImperativeHandle
大多数情况下应该通过 props 来控制组件,而不是直接操作 ref
下一步
现在你已经了解了 useRef,可以继续学习:
这篇文章有帮助吗?
Previous / Next
Related Links