📚 什么是 useRef
useRef 是 React 提供的一个 Hook,用于创建一个可变的 ref 对象。这个对象在整个组件生命周期内保持不变,其 .current 属性可以被修改而不会触发组件重新渲染。
import { useRef } from 'react'
const myRef = useRef(initialValue)
// myRef.current 可以被读取和修改
🔍 useRef 与 useEffect 的关系
在 React 中,useEffect 类似于 Vue 的 onMounted 生命周期钩子。它们都在组件挂载后执行副作用操作。
useEffect(() => {
// 类似于 Vue 的 onMounted
console.log('组件已挂载')
}, [])
🎨 useRef 的应用场景
1️⃣ DOM 节点的引用
useRef 最常见的用途之一是直接访问 DOM 元素。通过将 ref 传递给 JSX 元素的 ref 属性,React 会在组件渲染后将 DOM 节点赋值给 ref 的 .current 属性。
import { useRef, useEffect } from 'react'
export default function InputDemo() {
const inputRef = useRef(null)
useEffect(() => {
console.log(inputRef.current)
inputRef.current.focus()
}, [])
return (
<>
<input ref={inputRef} placeholder="自动聚焦这里" />
</>
)
}
关键点:
useRef(null)创建初始值为 null 的 refref={inputRef}将 ref 绑定到 input 元素- 在
useEffect中可以访问inputRef.current获取真实的 DOM 节点 - 可以调用 DOM 方法如
focus()、scrollIntoView()等
2️⃣ 可变对象的存储
useRef 可以存储任何可变值,这些值在组件重新渲染时保持不变,非常适合存储不需要触发重新渲染的数据。
import { useRef, useState, useEffect } from 'react'
export default function TimerDemo() {
let intervalId = useRef(null)
const [count, setCount] = useState(0)
function start() {
intervalId.current = setInterval(() => {
console.log('tick~~~~')
}, 1000)
console.log(intervalId)
}
function stop() {
clearInterval(intervalId.current)
}
useEffect(() => {
console.log(intervalId.current)
}, [count])
return (
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
{count}
<button type="button" onClick={() => setCount(count + 1)}>count++</button>
</>
)
}
使用场景:
- 存储定时器 ID(如上面的例子)
- 存储动画帧 ID(requestAnimationFrame)
- 存储 WebSocket 连接
- 存储上一次的值用于比较
- 存储任何不需要触发重新渲染的数据
3️⃣ 创建持久化的引用对象
useRef 创建的 ref 对象在组件的整个生命周期内保持不变,即使组件重新渲染,ref 对象本身也不会改变,只有其 .current 属性的值可能会改变。
export default function PersistentRefDemo() {
const [count, setCount] = useState(0)
const inputRef = useRef(null)
console.log('组件重新渲染')
console.log(inputRef.current)
useEffect(() => {
console.log('组件挂载时执行')
inputRef.current?.focus()
}, [])
return (
<>
<input ref={inputRef} />
{count}
<button type="button" onClick={() => setCount(count + 1)}>count++</button>
</>
)
}
⚖️ useRef 与 useState 的比较
🟢 相同点
两者都是储存可变对象的容器
// useState
const [state, setState] = useState(initialValue)
// useRef
const ref = useRef(initialValue)
它们都可以:
- 在组件中存储数据
- 在多次渲染之间保持数据
- 存储任何类型的值(对象、数组、函数等)
🔴 不同点
| 特性 | useState | useRef |
|---|---|---|
| 响应式 | ✅ 是响应式的 | ❌ 不是响应式的 |
| 更新方式 | 通过 setState 函数 | 直接修改 .current |
| 重新渲染 | 状态改变会触发重新渲染 | 修改 .current 不会触发重新渲染 |
| 返回值 | 返回 [值, 更新函数] | 返回 { current: 值 } |
| 适用场景 | 需要响应式更新的数据 | 不需要触发重新渲染的数据 |
useState - 响应式状态:
const [count, setCount] = useState(0)
// 每次调用 setCount 都会触发组件重新渲染
setCount(count + 1)
useRef - 可变对象的存储:
const countRef = useRef(0)
// 直接修改 .current 不会触发重新渲染
countRef.current = countRef.current + 1
💡 深入理解 useRef 的工作原理
🔄 Ref 对象的结构
useRef 返回的是一个普通对象:
{
current: initialValue
}
这个对象在组件的整个生命周期内保持不变,React 会在渲染过程中更新其 .current 属性。
📝 何时更新 .current
- DOM 引用: React 在渲染完成后自动更新
- 手动更新: 可以在任何地方直接修改
.current
const ref = useRef(0)
// 在事件处理中修改
function handleClick() {
ref.current += 1
}
// 在定时器中修改
useEffect(() => {
const timer = setInterval(() => {
ref.current += 1
}, 1000)
return () => clearInterval(timer)
}, [])
⚠️ 注意事项
- 不要在渲染过程中读取/写入 ref
// ❌ 错误:在渲染过程中修改 ref
function BadComponent() {
const ref = useRef(0)
ref.current += 1 // 这会导致问题
return <div>{ref.current}</div>
}
// ✅ 正确:在事件处理或 useEffect 中修改
function GoodComponent()() {
const ref = useRef(0)
useEffect(() => {
ref.current = 0
}, [])
return <div onClick={() => ref.current++}>Click me</div>
}
- Ref 的初始值只在第一次渲染时使用
const ref = useRef(0)
// 组件重新渲染时,ref.current 不会被重置为 0
🎯 实际应用示例
📝 示例 1:自动聚焦输入框
import { useRef, useEffect } from 'react'
export default function AutoFocusInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="我会自动聚焦"
/>
</div>
)
}
⏱️ 示例 2:计时器管理
import { useRef, useState, useEffect } from 'react'
export default function Timer() {
const timerRef = useRef(null)
const [seconds, setSeconds] = useState(0)
const start = () => {
if (timerRef.current) return
timerRef.current = setInterval(() => {
setSeconds(prev => prev + 1)
}, 1000)
}
const stop = () => {
if (timerRef.current) {
clearInterval(timerRef.current)
timerRef.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>
)
}
📜 示例 3:滚动到元素
import { useRef } from 'react'
export default function ScrollToElement() {
const targetRef = useRef(null)
const scrollToTarget = () => {
targetRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
}
return (
<div>
<button onClick={scrollToTarget}>滚动到目标</button>
<div style={{ height: '500px' }}>内容区域</div>
<div
ref={targetRef}
style={{
padding: '20px',
backgroundColor: 'lightblue'
}}
>
目标元素
</div>
<div style={{ height: '500px' }}>更多内容</div>
</div>
)
}
🔄 示例 4:存储上一次的值
import { useRef, useState, useEffect } from 'react'
export default function PreviousValueDemo() {
const [count, setCount] = useState(0)
const prevCountRef = useRef(0)
useEffect(() => {
prevCountRef.current = count
}, [count])
return (
<div>
<p>当前值: {count}</p>
<p>上一次的值: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
</div>
)
}
🎨 示例 5:Canvas 绘图
import { useRef, useEffect } from 'react'
export default function CanvasDemo() {
const canvasRef = useRef(null)
useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'lightblue'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = 'red'
ctx.beginPath()
ctx.arc(100, 100, 50, 0, Math.PI * 2)
ctx.fill()
}, [])
return (
<canvas
ref={canvasRef}
width={200}
height={200}
/>
)
}
🚀 高级技巧
💾 在自定义 Hook 中使用 useRef
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
function Component() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>当前: {count}</p>
<p>之前: {prevCount}</p>
</div>
)
}
🔗 多个 Ref 的管理
function MultipleRefsDemo() {
const input1Ref = useRef(null)
const input2Ref = useRef(null)
const input3Ref = useRef(null)
const focusNext = (currentRef, nextRef) => {
if (currentRef.current?.value) {
nextRef.current?.focus()
}
}
return (
<form>
<input
ref={input1Ref}
onChange={() => focusNext(input1Ref, input2Ref)}
/>
<input
ref={input2Ref}
onChange={() => focusNext(input2Ref, input3Ref)}
/>
<input ref={input3Ref} />
</form>
)
}
📖 总结
useRef 是 React 中一个强大而灵活的 Hook,它的主要特点包括:
✅ 持久化引用 - 在组件生命周期内保持不变
✅ 不触发重新渲染 - 修改 .current 不会导致组件更新
✅ DOM 访问 - 可以直接访问和操作 DOM 元素
✅ 存储可变数据 - 适合存储不需要响应式的数据
✅ 性能优化 - 避免不必要的状态更新和重新渲染
选择 useRef 的时机:
- 需要直接访问 DOM 元素
- 需要存储定时器、动画帧等 ID
- 需要在渲染之间保持数据但不需要触发重新渲染
- 需要存储上一次的值用于比较
- 需要在回调函数中访问最新的值
选择 useState 的时机:
- 需要响应式更新 UI
- 状态改变需要触发组件重新渲染
- 需要在多个组件间共享状态
掌握 useRef 的使用,能够让你在 React 开发中更加得心应手,写出更高效、更优雅的代码!🎉