12. useRef 除了获取 DOM 还能做什么?(存储可变值、避免重渲染)
答案:
useRef 的基本用法:
1. 获取 DOM 元素
function MyComponent() {
const inputRef = useRef(null)
const focusInput = () => {
inputRef.current.focus()
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
)
}
2. 存储可变值(不触发重渲染)
function MyComponent() {
const [count, setCount] = useState(0)
const renderCount = useRef(0)
const previousCount = useRef(0)
// 每次渲染时更新渲染次数
renderCount.current += 1
// 保存上一次的 count 值
useEffect(() => {
previousCount.current = count
})
return (
<div>
<p>Current count: {count}</p>
<p>Previous count: {previousCount.current}</p>
<p>Render count: {renderCount.current}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
3. 存储定时器 ID
function TimerComponent() {
const [seconds, setSeconds] = useState(0)
const intervalRef = useRef(null)
const startTimer = () => {
if (intervalRef.current) return // 防止重复启动
intervalRef.current = setInterval(() => {
setSeconds((prev) => prev + 1)
}, 1000)
}
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
useEffect(() => {
return () => {
// 组件卸载时清理定时器
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}, [])
return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
)
}
4. 存储前一次的值
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
function MyComponent() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
5. 存储组件实例
function ParentComponent() {
const childRef = useRef(null)
const handleChildAction = () => {
// 调用子组件的方法
childRef.current?.doSomething()
}
return (
<div>
<button onClick={handleChildAction}>Trigger Child Action</button>
<ChildComponent ref={childRef} />
</div>
)
}
const ChildComponent = forwardRef((props, ref) => {
const [data, setData] = useState(null)
useImperativeHandle(ref, () => ({
doSomething: () => {
console.log('Child component action triggered')
setData('Action performed')
},
}))
return <div>{data}</div>
})
6. 避免闭包陷阱
function MyComponent() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
// 更新 ref 的值
countRef.current = count
useEffect(() => {
const timer = setInterval(() => {
// 使用 ref 获取最新值,避免闭包陷阱
console.log('Current count:', countRef.current)
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖数组,只在挂载时执行
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
7. 存储表单数据
function FormComponent() {
const formDataRef = useRef({
name: '',
email: '',
message: '',
})
const handleSubmit = (e) => {
e.preventDefault()
console.log('Form data:', formDataRef.current)
// 提交表单数据
}
const updateField = (field, value) => {
formDataRef.current[field] = value
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
onChange={(e) => updateField('name', e.target.value)}
/>
<input
type="email"
placeholder="Email"
onChange={(e) => updateField('email', e.target.value)}
/>
<textarea
placeholder="Message"
onChange={(e) => updateField('message', e.target.value)}
/>
<button type="submit">Submit</button>
</form>
)
}
8. 存储动画状态
function AnimatedComponent() {
const [isVisible, setIsVisible] = useState(false)
const animationRef = useRef(null)
const startAnimation = () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
let startTime = null
const duration = 1000 // 1秒
const animate = (timestamp) => {
if (!startTime) startTime = timestamp
const progress = (timestamp - startTime) / duration
if (progress < 1) {
// 执行动画
console.log('Animation progress:', progress)
animationRef.current = requestAnimationFrame(animate)
} else {
console.log('Animation completed')
}
}
animationRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [])
return (
<div>
<button onClick={startAnimation}>Start Animation</button>
<div style={{ opacity: isVisible ? 1 : 0 }}>Animated Content</div>
</div>
)
}
9. 存储缓存数据
function useCache() {
const cacheRef = useRef(new Map())
const get = (key) => {
return cacheRef.current.get(key)
}
const set = (key, value) => {
cacheRef.current.set(key, value)
}
const clear = () => {
cacheRef.current.clear()
}
return { get, set, clear }
}
function DataComponent({ id }) {
const [data, setData] = useState(null)
const { get, set } = useCache()
useEffect(() => {
// 先检查缓存
const cachedData = get(id)
if (cachedData) {
setData(cachedData)
return
}
// 缓存中没有,从服务器获取
fetchData(id).then((result) => {
setData(result)
set(id, result) // 存入缓存
})
}, [id, get, set])
return <div>{data ? data.title : 'Loading...'}</div>
}
10. 存储 WebSocket 连接
function ChatComponent() {
const [messages, setMessages] = useState([])
const wsRef = useRef(null)
useEffect(() => {
// 建立 WebSocket 连接
wsRef.current = new WebSocket('ws://localhost:8080')
wsRef.current.onmessage = (event) => {
const message = JSON.parse(event.data)
setMessages((prev) => [...prev, message])
}
return () => {
// 组件卸载时关闭连接
if (wsRef.current) {
wsRef.current.close()
}
}
}, [])
const sendMessage = (text) => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ text }))
}
}
return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg.text}</div>
))}
<button onClick={() => sendMessage('Hello!')}>Send Message</button>
</div>
)
}
useRef vs useState 的区别:
| 特性 | useRef | useState |
|---|---|---|
| 触发重渲染 | 否 | 是 |
| 存储类型 | 任何值 | 任何值 |
| 更新方式 | ref.current = value | setValue(value) |
| 访问方式 | ref.current | 直接访问 |
| 用途 | 存储可变值、DOM 引用 | 存储状态 |
最佳实践:
- 明确用途:区分 DOM 引用和可变值存储
- 及时清理:在 useEffect 清理函数中清理定时器、事件监听器等
- 避免滥用:不要用 useRef 替代 useState,除非确实不需要触发重渲染
- 类型安全:使用 TypeScript 时,为 ref 指定正确的类型