React useRef使用:从基础到实战应用
一、为什么需要 useRef
在 React 函数组件中管理状态主要依靠 useState,但当我们需要操作 DOM 元素或保存可变值时,state 的更新机制反而会成为阻碍。此时 useRef 的价值就体现出来了:
- 持久化存储:ref 对象在组件的整个生命周期中保持不变
- 直接访问:可以绕过 React 的渲染机制直接操作 DOM
- 性能优化:避免不必要的重新渲染
- 副作用管理:配合 useEffect 使用更灵活
与 state 的关键区别在于,修改 ref 的 current 属性不会触发组件重新渲染,这是理解 useRef 的核心要点。
二、基本使用方法
2.1 创建 ref 对象
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
// ...
}
2.2 访问 ref 的三种方式
- 字符串方式(已废弃)
// 不推荐使用
<input ref="myInput" />
- 回调函数方式
<input ref={(el) => { this.inputEl = el; }} />
- useRef Hook(推荐)
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</>
);
}
三、核心使用场景
3.1 DOM 元素操作
function ImageLoader() {
const imgRef = useRef(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (imgRef.current) {
imgRef.current.onload = () => setLoading(false);
}
}, []);
return (
<div>
{loading && <div>Loading...</div>}
<img
ref={imgRef}
src="large-image.jpg"
alt="Large content"
style={{ display: loading ? 'none' : 'block' }}
/>
</div>
);
}
3.2 保存可变值
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
3.3 获取 previous props/state
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
四、高级应用场景
4.1 与第三方库集成
function ChartWrapper() {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
if (chartRef.current && !chartInstance.current) {
chartInstance.current = new Chart(chartRef.current, {
type: 'line',
data: chartData,
options: { /*...*/ }
});
}
return () => {
if (chartInstance.current) {
chartInstance.current.destroy();
chartInstance.current = null;
}
};
}, []);
return <canvas ref={chartRef} />;
}
4.2 自定义 Hook 中的应用
function useHover() {
const [isHovered, setIsHovered] = useState(false);
const ref = useRef(null);
useEffect(() => {
const node = ref.current;
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
if (node) {
node.addEventListener('mouseenter', handleMouseEnter);
node.addEventListener('mouseleave', handleMouseLeave);
}
return () => {
node.removeEventListener('mouseenter', handleMouseEnter);
node.removeEventListener('mouseleave', handleMouseLeave);
};
}, []);
return [ref, isHovered];
}
// 使用示例
function HoverComponent() {
const [hoverRef, isHovered] = useHover();
return (
<div ref={hoverRef}>
{isHovered ? 'Hovering!' : 'Hover me'}
</div>
);
}
五、性能优化技巧
5.1 避免重复创建 ref
// 正确 ✅
const ref = useRef(new HeavyObject());
// 错误 ❌ 每次渲染都会创建新实例
const ref = useRef(null);
if (!ref.current) {
ref.current = new HeavyObject();
}
5.2 结合 useCallback 使用
function MeasurableComponent() {
const [width, setWidth] = useState(0);
const measuredRef = useCallback(node => {
if (node) {
setWidth(node.getBoundingClientRect().width);
}
}, []);
return <div ref={measuredRef}>Width: {width}px</div>;
}
5.3 批量 DOM 操作
function VirtualList() {
const listRef = useRef(null);
const itemRefs = useRef(new Map());
const scrollToItem = (index) => {
const itemNode = itemRefs.current.get(index);
if (itemNode) {
listRef.current.scrollTo({
top: itemNode.offsetTop,
behavior: 'smooth'
});
}
};
return (
<div ref={listRef} className="list">
{items.map((item, index) => (
<div
key={item.id}
ref={node => {
if (node) {
itemRefs.current.set(index, node);
} else {
itemRefs.current.delete(index);
}
}}
>
{item.content}
</div>
))}
</div>
);
}
六、常见问题解决方案
6.1 ref 为 null 的情况
function SafeRefComponent() {
const ref = useRef(null);
useEffect(() => {
// 使用可选链操作符
console.log(ref.current?.value);
// 或者使用定时器
const timer = setTimeout(() => {
if (ref.current) {
ref.current.focus();
}
}, 100);
return () => clearTimeout(timer);
}, []);
return <input ref={ref} />;
}
6.2 转发 refs
const FancyInput = React.forwardRef((props, ref) => {
return <input ref={ref} className="fancy" {...props} />;
});
function ParentComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <FancyInput ref={inputRef} />;
}
6.3 类组件中的使用
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
focusInput = () => {
this.inputRef.current.focus();
};
render() {
return <input ref={this.inputRef} />;
}
}
七、最佳实践指南
-
优先使用 useRef 而非 createRef
在函数组件中始终使用 useRef,类组件中使用 createRef -
避免在渲染期间修改 ref
ref 的修改应放在事件处理或 useEffect 中 -
区分可变值与不可变值
需要触发渲染的值用 state,否则用 ref -
注意内存泄漏
及时清理定时器、事件监听器等资源 -
配合 TypeScript 使用
const inputRef = useRef<HTMLInputElement>(null);
-
性能敏感操作优先使用 ref
如动画帧、大量计算等
八、实战案例:表单验证增强
function AdvancedForm() {
const formRef = useRef(null);
const [errors, setErrors] = useState({});
const validateField = (name, value) => {
// 复杂验证逻辑
if (name === 'email' && !/\S+@\S+\.\S+/.test(value)) {
return 'Invalid email format';
}
return null;
};
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(formRef.current);
const newErrors = {};
for (const [name, value] of formData.entries()) {
const error = validateField(name, value);
if (error) newErrors[name] = error;
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
} else {
// 提交表单
}
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<div>
<label>Email:</label>
<input name="email" />
{errors.email && <span className="error">{errors.email}</span>}
</div>
{/* 其他表单字段 */}
<button type="submit">Submit</button>
</form>
);
}
正文到此结束
相关文章
热门推荐
评论插件初始化中...