useRef 的 5 个冷门但救命的高级用法

问题场景

很多人对 useRef 的认知停留在"拿 DOM 元素":

tsx 复制代码
const btnRef = useRef<HTMLButtonElement>(null!)
return <button ref={btnRef}>点击</button>

但实际项目里,真正让我们头皮发麻的不是拿不到 DOM,而是:

❝ useEffect 里闭包捕获了旧值,怎么都拿不到最新 state ❞ ❝ 我想知道组件到底重渲染了多少次 ❞ ❝ 定时器清理时 ref 已经 null 了,清不掉 ❞ ❝ 不想用 useState 但又想记住一个值,还不想触发重渲染 ❞

这些问题,useRef 就是答案。


1. 🎯 解决 useEffect 闭包陷阱

场景:倒计时组件,用户点击暂停,定时器里拿到的 count 永远是旧值。

tsx 复制代码
function Countdown() {
  const [count, setCount] = useState(60)

  useEffect(() => {
    const timer = setInterval(() => {
      // ❌ count 被闭包捕获,永远是最初的 60
      setCount(count - 1)   // 永远是 60-1=59
    }, 1000)
    return () => clearInterval(timer)
  }, []) // 加依赖会频繁重建定时器
}

✅ useRef 解法

tsx 复制代码
function Countdown() {
  const [count, setCount] = useState(60)
  const countRef = useRef(count)

  // 同步 ref
  useEffect(() => { countRef.current = count }, [count])

  useEffect(() => {
    const timer = setInterval(() => {
      // ✅ ref 永远指向最新值,不参与闭包捕获
      if (countRef.current > 0) {
        setCount(c => c - 1)  // 也可以用函数式更新
      }
    }, 1000)
    return () => clearInterval(timer)
  }, [])
}

原理ref.current 是一个可变对象,修改它不会引发重渲染,且读取时总是最新值。


2. 📊 追踪组件渲染次数(调试神器)

tsx 复制代码
function ExpensiveList({ items }: { items: Item[] }) {
  const renderCount = useRef(0)
  renderCount.current++

  useEffect(() => {
    console.log(`🔁 已渲染 ${renderCount.current} 次`)
  })

  return <div>{/* ... */}</div>
}

开发时挂上这个,一眼看出哪些组件因 props/state 变化在无意义重渲染。配合 React DevTools Profiler 更香。

升级版------追踪哪个 prop 变化触发了重渲染

tsx 复制代码
function useWhyDidYouUpdate(name: string, props: Record<string, any>) {
  const prev = useRef(props)
  useEffect(() => {
    const changes: Record<string, { from: any; to: any }> = {}
    Object.keys({ ...prev.current, ...props }).forEach(key => {
      if (prev.current[key] !== props[key]) {
        changes[key] = { from: prev.current[key], to: props[key] }
      }
    })
    if (Object.keys(changes).length) {
      console.log(`[${name}] 变动的 props:`, changes)
    }
    prev.current = props
  })
}

3. ⏰ 定时器/事件的稳健清理

用 ref 保存定时器/事件句柄,避免 cleanup 时拿不到正确引用:

tsx 复制代码
function PollingComponent() {
  const timerRef = useRef<number | null>(null)

  // ✅ 任何时候清理都安全
  const stopPolling = useCallback(() => {
    if (timerRef.current !== null) {
      clearInterval(timerRef.current)
      timerRef.current = null
    }
  }, [])

  const startPolling = useCallback(() => {
    stopPolling() // 防止重复
    timerRef.current = window.setInterval(() => {
      console.log('polling...')
    }, 1000)
  }, [stopPolling])

  useEffect(() => {
    return stopPolling // 组件卸载时自动清理
  }, [stopPolling])

  return <button onClick={startPolling}>开始轮询</button>
}

React 18 StrictMode 下组件会 mount-unmount-mount,不用 ref 管理句柄,两次 mount 的定时器会打架。


4. 📋 追踪上一次的 props/state

不用额外 state,轻松拿到"上一次的值":

tsx 复制代码
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>()
  useEffect(() => {
    ref.current = value
  })
  return ref.current // 返回的是 useEffect 执行前的值
}

// 使用
function ScrollNotifier({ y }: { y: number }) {
  const prevY = usePrevious(y)

  useEffect(() => {
    if (prevY !== undefined && y > prevY) {
      console.log('⬇️ 向下滚动')
    } else if (prevY !== undefined && y < prevY) {
      console.log('⬆️ 向上滚动')
    }
  }, [y, prevY])

  return <div>滚动距离: {y}px</div>
}

为什么能拿到上一次值?

useRef 不参与渲染周期。第一次渲染时 ref 是 undefined;执行 useEffect 时 ref 被更新为 y。第二次渲染时,useEffect 还没跑,所以 ref 保存的仍然是上一次渲染周期的值。


5. 🔗 避免不必要的 useEffect

场景:表单编辑,只在"保存"时获取最新值,不需要每次输入都触发 state → 渲染 → 副作用。

tsx 复制代码
function EditForm({ initial }: { initial: { name: string } }) {
  const nameRef = useRef(initial.name)

  // ✅ 输入时不触发重渲染
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    nameRef.current = e.target.value
  }

  const handleSave = () => {
    alert(`保存: ${nameRef.current}`)
  }

  return (
    <div>
      <input defaultValue={initial.name} onChange={handleChange} />
      <button onClick={handleSave}>保存</button>
    </div>
  )
}

⚠️ 适用判断 :如果值需要在提交/特定时机使用,且不需要响应式渲染,就用 ref 代替 state。能避免大量不必要的渲染。


要点总结

场景 用 ref 还是 state
渲染到页面显示 🤯 state
不渲染但需要记住值 📝 ref
闭包里读最新值 🎯 ref
做调试计数/日志 🔍 ref
管理定时器/事件句柄 ⏰ ref
表单输入即时反馈 ✍️ state

一句话总结 :如果值的改变不需要 UI 刷新,就用 useRef 而不是 useState。它就是一个不会触发渲染的"记忆盒子",用好了能省掉一堆依赖数组的心智负担。

相关推荐
小小小小宇1 小时前
Harness Engineering 与 AI 联动
前端
鱼人1 小时前
HTML5 页面性能优化大全
前端
ping某1 小时前
专栏-null 和 undefined 到底是什么?
前端·javascript·后端
用户900463370401 小时前
5MB vs 4KB vs 无限大:浏览器存储谁更强?
前端
小小小小宇2 小时前
Harness Engineering 全解析与应用
前端
牧艺2 小时前
cos-design v3.0:从 15 个 Demo 到 49 个组件的视觉特效库
前端·视觉设计
lichenyang4532 小时前
ASCF 架构升级总览:WebRuntimePage 为什么要变薄
前端
道友可好2 小时前
从今天开始:你的第一个 Harness Engineering 实践
前端·人工智能·后端
Linsk2 小时前
组件 = 模板 + 业务逻辑
java·前端·vue.js