说说对 React refs 的理解?应用场景?
一、是什么
Refs(引用)是 React 提供的一种"逃生舱"机制,允许开发者直接访问 DOM 元素或组件实例,绑定不会触发重新渲染的可变值。
在 React 的声明式编程模型中,绝大多数交互都可以通过 state 和 props 来完成。但某些场景确实需要直接操作 DOM 或持有可变引用,这时就需要 refs。
Refs 的核心特性:
- 修改 ref 的值不会触发组件重新渲染
- ref 的值在组件的整个生命周期内保持不变(同一个对象引用)
- 可以在渲染之间持久化存储任意可变值
二、创建和使用 Refs
useRef(函数组件)
useRef 是函数组件中创建 ref 的标准方式:
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="自动聚焦" />;
}
useRef 返回一个 { current: T } 对象。将这个对象赋给 JSX 元素的 ref 属性后,React 会在挂载时将 DOM 节点赋值给 current,卸载时设为 null。
createRef(类组件)
在类组件中使用 React.createRef():
class TextInput extends React.Component {
inputRef = React.createRef<HTMLInputElement>();
componentDidMount() {
this.inputRef.current?.focus();
}
render() {
return <input ref={this.inputRef} placeholder="自动聚焦" />;
}
}
注意:createRef 每次调用都会创建新的 ref 对象,在函数组件中不应使用(每次渲染都会重新创建)。
回调 Refs
回调 ref 是一个函数,React 在挂载时传入 DOM 节点,卸载时传入 null:
function MeasuredComponent() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
<div ref={measuredRef} style={{ padding: 20 }}>
<p>这段内容的高度会被测量</p>
<p>可以包含动态内容</p>
</div>
<p>内容高度:{height}px</p>
</div>
);
}
回调 ref 的优势在于可以在 ref 设置时立即执行逻辑,适合需要在元素挂载时进行测量或初始化的场景。
三、forwardRef——转发 Ref
默认情况下,函数组件不接受 ref prop。要让父组件访问子组件内部的 DOM 元素,需要使用 forwardRef:
import { forwardRef, useRef } from 'react';
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary';
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
function Button({ children, variant = 'primary' }, ref) {
return (
<button ref={ref} className={`btn btn-${variant}`}>
{children}
</button>
);
}
);
function Toolbar() {
const btnRef = useRef<HTMLButtonElement>(null);
const handleFocusButton = () => {
btnRef.current?.focus();
};
return (
<div>
<Button ref={btnRef} variant="primary">
提交
</Button>
<button onClick={handleFocusButton}>聚焦提交按钮</button>
</div>
);
}
在 React 19 中,ref 可以作为普通 prop 传递,不再强制需要 forwardRef:
function Button({ children, ref }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) {
return <button ref={ref}>{children}</button>;
}
四、useImperativeHandle——自定义暴露的实例值
useImperativeHandle 允许子组件自定义通过 ref 暴露给父组件的值,而不是直接暴露 DOM 节点:
import { useRef, useImperativeHandle, forwardRef, useState } from 'react';
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (time: number) => void;
getCurrentTime: () => number;
}
const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>(
function VideoPlayer({ src }, ref) {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play() {
videoRef.current?.play();
},
pause() {
videoRef.current?.pause();
},
seek(time: number) {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
getCurrentTime() {
return videoRef.current?.currentTime ?? 0;
},
}));
return <video ref={videoRef} src={src} />;
}
);
function VideoApp() {
const playerRef = useRef<VideoPlayerHandle>(null);
return (
<div>
<VideoPlayer ref={playerRef} src="/video.mp4" />
<div className="controls">
<button onClick={() => playerRef.current?.play()}>播放</button>
<button onClick={() => playerRef.current?.pause()}>暂停</button>
<button onClick={() => playerRef.current?.seek(0)}>重播</button>
</div>
</div>
);
}
这种模式在封装复杂组件(如编辑器、播放器、表单)时非常有用,它限制了父组件对子组件内部的访问权限。
五、常见应用场景
1. 管理焦点
function SearchForm() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === '/' && document.activeElement !== inputRef.current) {
e.preventDefault();
inputRef.current?.focus();
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, []);
return (
<div className="search-form">
<input ref={inputRef} placeholder='按 "/" 快速搜索' />
</div>
);
}
2. 测量 DOM 元素
function AutoResizeTextarea() {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleInput = () => {
const el = textareaRef.current;
if (el) {
el.style.height = 'auto';
el.style.height = `${el.scrollHeight}px`;
}
};
return (
<textarea
ref={textareaRef}
onInput={handleInput}
style={{ overflow: 'hidden', resize: 'none' }}
placeholder="输入内容,高度自动调整..."
/>
);
}
3. 集成第三方 DOM 库
import { useRef, useEffect } from 'react';
function ChartComponent({ data }: { data: DataPoint[] }) {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<EChartsInstance | null>(null);
useEffect(() => {
if (containerRef.current) {
chartRef.current = echarts.init(containerRef.current);
}
return () => {
chartRef.current?.dispose();
};
}, []);
useEffect(() => {
chartRef.current?.setOption({
xAxis: { type: 'category', data: data.map(d => d.label) },
yAxis: { type: 'value' },
series: [{ data: data.map(d => d.value), type: 'bar' }],
});
}, [data]);
return <div ref={containerRef} style={{ width: '100%', height: 400 }} />;
}
4. 存储可变值(不触发渲染)
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
if (intervalRef.current !== null) return;
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
const reset = () => {
stop();
setSeconds(0);
};
useEffect(() => {
return () => stop();
}, []);
return (
<div>
<p>计时:{seconds} 秒</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
<button onClick={reset}>重置</button>
</div>
);
}
5. 保存前一次渲染的值
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = 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>
);
}
6. 无限滚动的 Intersection Observer
function InfiniteList({ loadMore }: { loadMore: () => Promise<void> }) {
const sentinelRef = useRef<HTMLDivElement>(null);
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 1.0 }
);
if (sentinelRef.current) {
observer.observe(sentinelRef.current);
}
return () => observer.disconnect();
}, [loadMore]);
return (
<div className="list">
{items.map(item => (
<div key={item.id} className="list-item">{item.title}</div>
))}
<div ref={sentinelRef} className="sentinel" />
</div>
);
}
六、使用注意事项
- 避免过度使用 ref:能用 state 和 props 声明式解决的问题,不要用 ref 命令式操作
- 不要在渲染期间读写 ref.current(除了初始化):渲染应该是纯函数,ref 操作应在 effect 或事件处理中进行
- ref 的设置时机:React 在 commit 阶段设置 ref,DOM 更新完成后才可用
- 函数组件没有实例:不能对函数组件使用 ref 获取实例,只能配合
forwardRef + useImperativeHandle 暴露特定方法
// ❌ 渲染期间读取 ref
function Bad() {
const ref = useRef(0);
ref.current += 1; // 不要在渲染期间修改 ref
return <div>{ref.current}</div>; // 不要在渲染期间读取 ref
}
// ✅ 在事件处理或 effect 中使用 ref
function Good() {
const ref = useRef(0);
const handleClick = () => {
ref.current += 1;
console.log(`点击了 ${ref.current} 次`);
};
return <button onClick={handleClick}>点击</button>;
}
七、总结
Refs 是 React 声明式模型中的命令式"逃生舱"。useRef 用于创建引用,forwardRef 用于转发引用,useImperativeHandle 用于自定义暴露的接口。主要应用场景包括 DOM 操作(焦点、测量、第三方库集成)和存储不触发渲染的可变值。使用时应遵循最小必要原则,优先使用声明式方案。