【React-7/Lesson90(2025-12-29)】React useRef 完全指南🎯

4 阅读5分钟

📚 什么是 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 的 ref
  • ref={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)

它们都可以:

  • 在组件中存储数据
  • 在多次渲染之间保持数据
  • 存储任何类型的值(对象、数组、函数等)

🔴 不同点

特性useStateuseRef
响应式✅ 是响应式的❌ 不是响应式的
更新方式通过 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

  1. DOM 引用: React 在渲染完成后自动更新
  2. 手动更新: 可以在任何地方直接修改 .current
const ref = useRef(0)

// 在事件处理中修改
function handleClick() {
  ref.current += 1
}

// 在定时器中修改
useEffect(() => {
  const timer = setInterval(() => {
    ref.current += 1
  }, 1000)
  
  return () => clearInterval(timer)
}, [])

⚠️ 注意事项

  1. 不要在渲染过程中读取/写入 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>
}
  1. 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 开发中更加得心应手,写出更高效、更优雅的代码!🎉