Appearance
useRef-DOM引用
学习目标
通过本章学习,你将全面掌握:
- useRef的概念和工作原理
- 使用useRef访问DOM元素
- useRef与createRef的区别
- DOM操作的各种场景
- 聚焦、滚动、测量等常见操作
- 第三方库集成
- 性能优化技巧
- React 19中的ref增强
第一部分:useRef基础
1.1 什么是useRef
useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数。返回的ref对象在组件的整个生命周期内保持不变。
jsx
import { useRef } from 'react';
function BasicUseRef() {
// 创建ref对象
const inputRef = useRef(null);
// ref对象的结构
console.log(inputRef); // { current: null }
const handleClick = () => {
// 通过ref访问DOM元素
console.log(inputRef.current); // <input> 元素
// 操作DOM
inputRef.current.focus();
inputRef.current.select();
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleClick}>聚焦输入框</button>
</div>
);
}1.2 useRef vs createRef
jsx
import { useRef, createRef } from 'react';
function UseRefVsCreateRef() {
// useRef:在整个组件生命周期中保持同一个引用
const useRefExample = useRef(null);
// createRef:每次渲染都创建新引用
const createRefExample = createRef();
const [count, setCount] = useState(0);
useEffect(() => {
console.log('useRef:', useRefExample.current); // 总是同一个DOM
console.log('createRef:', createRefExample.current); // 每次可能不同
});
return (
<div>
<input ref={useRefExample} placeholder="useRef" />
<input ref={createRefExample} placeholder="createRef" />
<button onClick={() => setCount(c => c + 1)}>
重新渲染 ({count})
</button>
</div>
);
}
// 结论:函数组件中应该使用useRef,不要使用createRef1.3 ref的赋值时机
jsx
function RefAssignmentTiming() {
const inputRef = useRef(null);
console.log('1. 渲染阶段,ref.current:', inputRef.current); // null
useEffect(() => {
console.log('3. Effect阶段,ref.current:', inputRef.current); // <input>
});
useLayoutEffect(() => {
console.log('2. LayoutEffect阶段,ref.current:', inputRef.current); // <input>
});
return <input ref={inputRef} />;
// 执行顺序:
// 1. 渲染阶段:ref.current = null
// 2. DOM更新
// 3. ref赋值:ref.current = <input>
// 4. useLayoutEffect执行:可以访问ref
// 5. 浏览器绘制
// 6. useEffect执行:可以访问ref
}第二部分:访问DOM元素
2.1 访问单个元素
jsx
function SingleElementAccess() {
const inputRef = useRef(null);
const textareaRef = useRef(null);
const selectRef = useRef(null);
const divRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
const getInputValue = () => {
console.log('input值:', inputRef.current.value);
};
const setInputValue = () => {
inputRef.current.value = '新值';
};
const clearTextarea = () => {
textareaRef.current.value = '';
};
const selectOption = () => {
selectRef.current.value = 'option2';
};
const scrollToDiv = () => {
divRef.current.scrollIntoView({ behavior: 'smooth' });
};
return (
<div>
<input ref={inputRef} defaultValue="初始值" />
<button onClick={focusInput}>聚焦</button>
<button onClick={getInputValue}>获取值</button>
<button onClick={setInputValue}>设置值</button>
<textarea ref={textareaRef} defaultValue="文本内容" />
<button onClick={clearTextarea}>清空</button>
<select ref={selectRef}>
<option value="option1">选项1</option>
<option value="option2">选项2</option>
</select>
<button onClick={selectOption}>选择选项2</button>
<div ref={divRef} style={{ marginTop: '1000px', height: '100px', background: '#f0f0f0' }}>
目标区域
</div>
<button onClick={scrollToDiv}>滚动到div</button>
</div>
);
}2.2 访问多个元素
jsx
function MultipleElementsAccess() {
// 方法1:多个useRef
const input1Ref = useRef(null);
const input2Ref = useRef(null);
const input3Ref = useRef(null);
// 方法2:使用数组
const inputRefs = useRef([]);
// 方法3:使用对象
const elementRefs = useRef({});
// 方法4:使用Map
const refMap = useRef(new Map());
const focusAll = () => {
// 多个ref
input1Ref.current?.focus();
input2Ref.current?.focus();
input3Ref.current?.focus();
// 数组refs
inputRefs.current.forEach(input => input?.focus());
// 对象refs
Object.values(elementRefs.current).forEach(el => el?.focus());
// Map refs
refMap.current.forEach(el => el?.focus());
};
return (
<div>
{/* 多个useRef */}
<input ref={input1Ref} />
<input ref={input2Ref} />
<input ref={input3Ref} />
{/* 数组refs */}
{[0, 1, 2].map(i => (
<input
key={i}
ref={el => inputRefs.current[i] = el}
/>
))}
{/* 对象refs */}
{['name', 'email', 'phone'].map(field => (
<input
key={field}
ref={el => elementRefs.current[field] = el}
placeholder={field}
/>
))}
{/* Map refs */}
{['a', 'b', 'c'].map(id => (
<input
key={id}
ref={el => refMap.current.set(id, el)}
/>
))}
<button onClick={focusAll}>全部聚焦</button>
</div>
);
}2.3 动态列表的ref
jsx
function DynamicListRefs() {
const [items, setItems] = useState([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
]);
const itemRefs = useRef(new Map());
const addItem = () => {
setItems([...items, {
id: Date.now(),
text: `Item ${items.length + 1}`
}]);
};
const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
itemRefs.current.delete(id);
};
const focusItem = (id) => {
const element = itemRefs.current.get(id);
element?.focus();
};
const scrollToItem = (id) => {
const element = itemRefs.current.get(id);
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
};
// 清理不存在的refs
useEffect(() => {
const currentIds = new Set(items.map(item => item.id));
itemRefs.current.forEach((_, id) => {
if (!currentIds.has(id)) {
itemRefs.current.delete(id);
}
});
}, [items]);
return (
<div>
<button onClick={addItem}>添加项目</button>
<ul>
{items.map(item => (
<li key={item.id}>
<input
ref={el => {
if (el) {
itemRefs.current.set(item.id, el);
} else {
itemRefs.current.delete(item.id);
}
}}
defaultValue={item.text}
/>
<button onClick={() => focusItem(item.id)}>聚焦</button>
<button onClick={() => scrollToItem(item.id)}>滚动到这里</button>
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}第三部分:常见DOM操作
3.1 聚焦控制
jsx
function FocusControl() {
const usernameRef = useRef(null);
const passwordRef = useRef(null);
const submitRef = useRef(null);
// 组件挂载时自动聚焦
useEffect(() => {
usernameRef.current.focus();
}, []);
// 按Enter键切换焦点
const handleUsernameKeyPress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
passwordRef.current.focus();
}
};
const handlePasswordKeyPress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submitRef.current.focus();
submitRef.current.click();
}
};
// 全选文本
const selectAll = () => {
usernameRef.current.select();
};
// 设置光标位置
const setCursorPosition = (position) => {
usernameRef.current.setSelectionRange(position, position);
usernameRef.current.focus();
};
return (
<form onSubmit={e => e.preventDefault()}>
<input
ref={usernameRef}
type="text"
placeholder="用户名"
onKeyPress={handleUsernameKeyPress}
/>
<input
ref={passwordRef}
type="password"
placeholder="密码"
onKeyPress={handlePasswordKeyPress}
/>
<button ref={submitRef} type="submit">
登录
</button>
<div>
<button onClick={selectAll}>全选用户名</button>
<button onClick={() => setCursorPosition(0)}>光标到开头</button>
<button onClick={() => setCursorPosition(5)}>光标到位置5</button>
</div>
</form>
);
}3.2 滚动控制
jsx
function ScrollControl() {
const containerRef = useRef(null);
const topRef = useRef(null);
const bottomRef = useRef(null);
// 滚动到顶部
const scrollToTop = () => {
containerRef.current.scrollTo({
top: 0,
behavior: 'smooth'
});
};
// 滚动到底部
const scrollToBottom = () => {
containerRef.current.scrollTo({
top: containerRef.current.scrollHeight,
behavior: 'smooth'
});
};
// 滚动到指定元素
const scrollToElement = (ref) => {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
};
// 监听滚动位置
const handleScroll = () => {
const container = containerRef.current;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
console.log({
scrollTop,
scrollHeight,
clientHeight,
atTop: scrollTop === 0,
atBottom: scrollTop + clientHeight >= scrollHeight - 1
});
};
return (
<div>
<div className="scroll-controls">
<button onClick={scrollToTop}>回到顶部</button>
<button onClick={scrollToBottom}>滚动到底部</button>
<button onClick={() => scrollToElement(topRef)}>滚动到顶部标记</button>
<button onClick={() => scrollToElement(bottomRef)}>滚动到底部标记</button>
</div>
<div
ref={containerRef}
onScroll={handleScroll}
style={{
height: '400px',
overflow: 'auto',
border: '1px solid #ccc'
}}
>
<div ref={topRef} style={{ background: '#f0f0f0', padding: '10px' }}>
顶部标记
</div>
<div style={{ height: '1500px', padding: '20px' }}>
<p>很长的内容...</p>
{Array.from({ length: 50 }, (_, i) => (
<p key={i}>段落 {i + 1}</p>
))}
</div>
<div ref={bottomRef} style={{ background: '#f0f0f0', padding: '10px' }}>
底部标记
</div>
</div>
</div>
);
}3.3 元素测量
jsx
function ElementMeasurement() {
const boxRef = useRef(null);
const [dimensions, setDimensions] = useState(null);
const [position, setPosition] = useState(null);
const measureElement = () => {
const element = boxRef.current;
// 获取尺寸
const rect = element.getBoundingClientRect();
setDimensions({
width: rect.width,
height: rect.height,
offsetWidth: element.offsetWidth,
offsetHeight: element.offsetHeight,
clientWidth: element.clientWidth,
clientHeight: element.clientHeight,
scrollWidth: element.scrollWidth,
scrollHeight: element.scrollHeight
});
// 获取位置
setPosition({
top: rect.top,
left: rect.left,
right: rect.right,
bottom: rect.bottom,
x: rect.x,
y: rect.y
});
};
// 监听窗口大小变化
useEffect(() => {
const handleResize = () => {
measureElement();
};
window.addEventListener('resize', handleResize);
measureElement();
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div>
<div
ref={boxRef}
style={{
width: '300px',
height: '200px',
padding: '20px',
border: '2px solid blue',
margin: '50px',
overflow: 'auto'
}}
>
<div style={{ height: '400px' }}>
测量这个盒子
</div>
</div>
<button onClick={measureElement}>测量</button>
{dimensions && (
<div>
<h3>尺寸信息:</h3>
<pre>{JSON.stringify(dimensions, null, 2)}</pre>
</div>
)}
{position && (
<div>
<h3>位置信息:</h3>
<pre>{JSON.stringify(position, null, 2)}</pre>
</div>
)}
</div>
);
}3.4 样式操作
jsx
function StyleManipulation() {
const boxRef = useRef(null);
const changeColor = (color) => {
boxRef.current.style.backgroundColor = color;
};
const changeSize = (width, height) => {
boxRef.current.style.width = width + 'px';
boxRef.current.style.height = height + 'px';
};
const addClass = (className) => {
boxRef.current.classList.add(className);
};
const removeClass = (className) => {
boxRef.current.classList.remove(className);
};
const toggleClass = (className) => {
boxRef.current.classList.toggle(className);
};
const setTransform = (transform) => {
boxRef.current.style.transform = transform;
};
return (
<div>
<div
ref={boxRef}
style={{
width: '200px',
height: '200px',
background: 'blue',
transition: 'all 0.3s'
}}
>
可操作的盒子
</div>
<div className="controls">
<button onClick={() => changeColor('red')}>变红色</button>
<button onClick={() => changeColor('green')}>变绿色</button>
<button onClick={() => changeSize(300, 300)}>变大</button>
<button onClick={() => changeSize(100, 100)}>变小</button>
<button onClick={() => addClass('rounded')}>添加圆角</button>
<button onClick={() => removeClass('rounded')}>移除圆角</button>
<button onClick={() => toggleClass('shadow')}>切换阴影</button>
<button onClick={() => setTransform('rotate(45deg)')}>旋转</button>
<button onClick={() => setTransform('scale(1.5)')}>缩放</button>
</div>
</div>
);
}第四部分:实战场景
4.1 自动聚焦搜索框
jsx
function SearchWithAutoFocus() {
const searchRef = useRef(null);
const [results, setResults] = useState([]);
// 组件挂载时自动聚焦
useEffect(() => {
searchRef.current.focus();
}, []);
// 按ESC清空并重新聚焦
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
searchRef.current.value = '';
searchRef.current.focus();
setResults([]);
}
};
// 执行搜索
const handleSearch = (e) => {
const term = e.target.value;
if (term) {
performSearch(term).then(setResults);
} else {
setResults([]);
}
};
return (
<div className="search-container">
<input
ref={searchRef}
type="search"
onChange={handleSearch}
onKeyDown={handleKeyDown}
placeholder="输入搜索词... (ESC清空)"
autoComplete="off"
/>
<div className="search-results">
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
</div>
);
}4.2 滚动到新消息
jsx
function ChatWindow({ messages }) {
const messagesEndRef = useRef(null);
const containerRef = useRef(null);
// 新消息时滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 检查是否在底部
const [isAtBottom, setIsAtBottom] = useState(true);
const handleScroll = () => {
const container = containerRef.current;
const isBottom = container.scrollHeight - container.scrollTop === container.clientHeight;
setIsAtBottom(isBottom);
};
// 只有在底部时才自动滚动
useEffect(() => {
if (isAtBottom) {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, isAtBottom]);
return (
<div className="chat-window">
<div
ref={containerRef}
onScroll={handleScroll}
className="messages-container"
>
{messages.map(message => (
<div key={message.id} className="message">
<span className="author">{message.author}:</span>
<span className="text">{message.text}</span>
</div>
))}
<div ref={messagesEndRef} />
</div>
{!isAtBottom && (
<button
className="scroll-to-bottom"
onClick={() => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })}
>
↓ 回到底部
</button>
)}
</div>
);
}4.3 图片懒加载
jsx
function LazyImageLoader() {
const [images] = useState(
Array.from({ length: 100 }, (_, i) => ({
id: i,
src: `https://picsum.photos/300/200?random=${i}`,
alt: `Image ${i}`
}))
);
const imageRefs = useRef(new Map());
const [loadedImages, setLoadedImages] = useState(new Set());
useEffect(() => {
// 使用Intersection Observer实现懒加载
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src && !img.src) {
img.src = src;
setLoadedImages(prev => new Set([...prev, img.dataset.id]));
observer.unobserve(img);
}
}
});
},
{
rootMargin: '50px' // 提前50px开始加载
}
);
imageRefs.current.forEach(img => {
if (img) observer.observe(img);
});
return () => {
observer.disconnect();
};
}, []);
return (
<div className="image-grid">
{images.map(image => (
<div key={image.id} className="image-container">
<img
ref={el => imageRefs.current.set(image.id, el)}
data-src={image.src}
data-id={image.id}
alt={image.alt}
className={loadedImages.has(String(image.id)) ? 'loaded' : 'loading'}
/>
{!loadedImages.has(String(image.id)) && (
<div className="placeholder">加载中...</div>
)}
</div>
))}
</div>
);
}4.4 Canvas绘图
jsx
function CanvasDrawing() {
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// 初始化canvas
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
}, []);
const startDrawing = (e) => {
setIsDrawing(true);
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(
e.clientX - rect.left,
e.clientY - rect.top
);
};
const draw = (e) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
ctx.lineTo(
e.clientX - rect.left,
e.clientY - rect.top
);
ctx.stroke();
};
const stopDrawing = () => {
setIsDrawing(false);
};
const clearCanvas = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
const saveImage = () => {
const canvas = canvasRef.current;
const dataUrl = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'drawing.png';
link.href = dataUrl;
link.click();
};
return (
<div>
<canvas
ref={canvasRef}
width={800}
height={600}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
style={{
border: '1px solid #000',
cursor: 'crosshair'
}}
/>
<div>
<button onClick={clearCanvas}>清空</button>
<button onClick={saveImage}>保存</button>
</div>
</div>
);
}4.5 视频播放器控制
jsx
function VideoPlayerControl() {
const videoRef = useRef(null);
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const play = () => {
videoRef.current.play();
setPlaying(true);
};
const pause = () => {
videoRef.current.pause();
setPlaying(false);
};
const togglePlay = () => {
playing ? pause() : play();
};
const seek = (time) => {
videoRef.current.currentTime = time;
setCurrentTime(time);
};
const changeVolume = (vol) => {
videoRef.current.volume = vol;
setVolume(vol);
};
const toggleMute = () => {
videoRef.current.muted = !videoRef.current.muted;
};
const toggleFullscreen = () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
videoRef.current.requestFullscreen();
}
};
const handleTimeUpdate = () => {
setCurrentTime(videoRef.current.currentTime);
};
const handleLoadedMetadata = () => {
setDuration(videoRef.current.duration);
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="video-player">
<video
ref={videoRef}
src="/video.mp4"
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
width="100%"
/>
<div className="controls">
<button onClick={togglePlay}>
{playing ? '⏸' : '▶'}
</button>
<span className="time">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
<input
type="range"
min="0"
max={duration}
value={currentTime}
onChange={e => seek(Number(e.target.value))}
className="seek-bar"
/>
<button onClick={toggleMute}>
{videoRef.current?.muted ? '🔇' : '🔊'}
</button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={e => changeVolume(Number(e.target.value))}
className="volume-bar"
/>
<button onClick={toggleFullscreen}>
⛶ 全屏
</button>
</div>
</div>
);
}第五部分:第三方库集成
5.1 集成富文本编辑器
jsx
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
function QuillEditor({ initialValue, onChange }) {
const editorRef = useRef(null);
const quillRef = useRef(null);
useEffect(() => {
// 初始化Quill
quillRef.current = new Quill(editorRef.current, {
theme: 'snow',
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image'],
['clean']
]
}
});
// 设置初始值
if (initialValue) {
quillRef.current.root.innerHTML = initialValue;
}
// 监听变化
quillRef.current.on('text-change', () => {
const html = quillRef.current.root.innerHTML;
onChange?.(html);
});
return () => {
quillRef.current = null;
};
}, []);
return <div ref={editorRef} />;
}5.2 集成图表库
jsx
import Chart from 'chart.js/auto';
function ChartComponent({ data, type = 'bar' }) {
const canvasRef = useRef(null);
const chartRef = useRef(null);
useEffect(() => {
const ctx = canvasRef.current.getContext('2d');
// 销毁旧图表
if (chartRef.current) {
chartRef.current.destroy();
}
// 创建新图表
chartRef.current = new Chart(ctx, {
type,
data: {
labels: data.labels,
datasets: data.datasets
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
return () => {
if (chartRef.current) {
chartRef.current.destroy();
}
};
}, [data, type]);
return (
<div style={{ height: '400px' }}>
<canvas ref={canvasRef} />
</div>
);
}5.3 集成地图库
jsx
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
function MapComponent({ center, zoom, markers }) {
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
const markersRef = useRef([]);
useEffect(() => {
// 初始化地图
mapInstanceRef.current = L.map(mapRef.current).setView(center, zoom);
// 添加瓦片层
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(mapInstanceRef.current);
return () => {
mapInstanceRef.current.remove();
};
}, []);
// 更新中心点
useEffect(() => {
mapInstanceRef.current?.setView(center, zoom);
}, [center, zoom]);
// 更新标记
useEffect(() => {
// 清除旧标记
markersRef.current.forEach(marker => marker.remove());
markersRef.current = [];
// 添加新标记
markers.forEach(markerData => {
const marker = L.marker([markerData.lat, markerData.lng])
.addTo(mapInstanceRef.current)
.bindPopup(markerData.popup);
markersRef.current.push(marker);
});
}, [markers]);
return <div ref={mapRef} style={{ height: '500px', width: '100%' }} />;
}第六部分:性能优化
6.1 避免过度使用ref
jsx
// ❌ 不好:过度使用ref
function OveruseRef() {
const textRef = useRef(null);
// 不需要ref,应该用state
const handleChange = () => {
const value = textRef.current.value;
// 每次都要读取DOM
};
return <input ref={textRef} onChange={handleChange} />;
}
// ✅ 好:合理使用state
function ProperState() {
const [text, setText] = useState('');
// 直接使用state,更符合React理念
return <input value={text} onChange={e => setText(e.target.value)} />;
}
// ✅ ref的正确使用场景:
// 1. 聚焦、选择、滚动等DOM操作
// 2. 测量DOM尺寸和位置
// 3. 集成第三方DOM库
// 4. 保存不触发渲染的可变值6.2 ref回调的优化
jsx
function OptimizedRefCallback() {
const [items, setItems] = useState([]);
const itemRefs = useRef(new Map());
// ❌ 不好:每次渲染创建新函数
return (
<ul>
{items.map(item => (
<li
key={item.id}
ref={el => {
if (el) {
itemRefs.current.set(item.id, el);
}
}}
>
{item.text}
</li>
))}
</ul>
);
// ✅ 好:使用useCallback
const setItemRef = useCallback((id) => {
return (el) => {
if (el) {
itemRefs.current.set(id, el);
} else {
itemRefs.current.delete(id);
}
};
}, []);
return (
<ul>
{items.map(item => (
<li key={item.id} ref={setItemRef(item.id)}>
{item.text}
</li>
))}
</ul>
);
}第七部分:React 19增强
7.1 ref作为prop
jsx
// React 19:ref可以作为普通prop传递
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// 使用
function Parent() {
const inputRef = useRef(null);
return (
<div>
<MyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>
聚焦
</button>
</div>
);
}
// React 18及之前需要forwardRef
const MyInputOld = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});7.2 ref清理函数
jsx
// React 19:ref回调支持返回清理函数
function RefCleanup() {
const [items, setItems] = useState([]);
return (
<ul>
{items.map(item => (
<li
key={item.id}
ref={el => {
if (el) {
console.log('元素挂载:', item.id);
// 返回清理函数
return () => {
console.log('元素卸载:', item.id);
};
}
}}
>
{item.text}
</li>
))}
</ul>
);
}练习题
基础练习
- 使用useRef实现输入框的自动聚焦
- 创建一个滚动到顶部/底部的功能
- 测量一个元素的尺寸和位置
- 实现一个简单的视频播放器控制
进阶练习
- 实现一个聊天窗口,新消息自动滚动到底部
- 创建一个图片懒加载组件
- 集成一个第三方富文本编辑器
- 实现一个Canvas绘图应用
高级练习
- 实现一个虚拟滚动列表,使用ref优化性能
- 创建一个复杂的表单,使用ref管理多个输入
- 集成多个第三方库,正确管理ref生命周期
- 使用React 19的新ref特性优化代码
通过本章学习,你已经全面掌握了useRef访问DOM的各种技巧。useRef是React中操作DOM的主要方式,掌握它对集成第三方库和实现复杂交互非常重要。继续学习,探索useRef的更多用法!