mCell
4 / 10

useRef

从“跨渲染保存值但不触发渲染”到 DOM 引用、避免闭包陷阱与常见用法的系统讲解

先用一句话理解 useRef

useRef 用来创建一个可变容器{ current: ... }),它的 current 值能在多次渲染之间保持不变,但修改它不会触发重新渲染


useRef 返回的到底是什么

jsx
const ref = useRef(initialValue)

返回值形状是:

js
{ current: initialValue }

特点:

  • 这个对象在组件整个生命周期内引用稳定(不会因为渲染变)
  • 你可以随时改 ref.current
  • 改完不会引起 UI 自动刷新

最常见用法:拿到 DOM 节点

jsx
import { useEffect, useRef } from 'react' function FocusInput() { const inputRef = useRef(null) useEffect(() => { inputRef.current?.focus() }, []) return <input ref={inputRef} placeholder="I will be focused" /> }

要点:

  • ref={inputRef} 会让 React 在提交 DOM 后把真实节点塞进 inputRef.current
  • 访问 DOM 一般放到 useEffect/useLayoutEffect 里做

useRef vs useState:核心区别

useState

  • 用于“影响渲染”的数据
  • 更新会触发重新渲染

useRef

  • 用于“不需要驱动渲染”的数据
  • 更新不会触发重新渲染

经验法则:

  • 改了要让 UI 变:useState
  • 改了只是给逻辑用:useRef

用 useRef 跨渲染保存值:计时器 id、订阅句柄等

jsx
function Timer() { const timerIdRef = useRef(null) const start = () => { if (timerIdRef.current) return timerIdRef.current = setInterval(() => { console.log('tick') }, 1000) } const stop = () => { clearInterval(timerIdRef.current) timerIdRef.current = null } return ( <> <button onClick={start}>start</button> <button onClick={stop}>stop</button> </> ) }

如果你用 useState 存 interval id,会带来无意义重渲染;用 useRef 更合适。


解决闭包陷阱:让回调拿到最新值

问题场景:事件监听/定时器里读到旧 state。

jsx
useEffect(() => { const id = setInterval(() => { console.log(count) // 可能永远是旧值 }, 1000) return () => clearInterval(id) }, [])

解决:用 ref 存最新值。

jsx
const countRef = useRef(count) useEffect(() => { countRef.current = count }, [count]) useEffect(() => { const id = setInterval(() => { console.log(countRef.current) // 永远最新 }, 1000) return () => clearInterval(id) }, [])

记住:ref 是“可变外置存储”,回调闭包引用的是 ref 这个稳定对象,所以能读到更新后的 current


ref 初始化值的时机

useState 类似:useRef(initialValue) 的 initialValue 仅在首次挂载时用于设置初始 current

但与 useState 不同的是:

  • useRef 没有 setter
  • 你之后直接改 ref.current

用 useRef 做“实例变量”:统计渲染次数

jsx
function RenderCount() { const renders = useRef(0) renders.current += 1 return <div>renders: {renders.current}</div> }

注意:这会在渲染过程中修改 ref(通常没问题),但不要在这里做副作用(例如读写 DOM、发请求)。


forwardRef 与 useRef:组件对外暴露 DOM/方法

父组件想拿到子组件里的 input:

jsx
const Input = forwardRef(function Input(props, ref) { return <input ref={ref} {...props} /> }) function Parent() { const ref = useRef(null) return ( <> <Input ref={ref} /> <button onClick={() => ref.current?.focus()}>focus</button> </> ) }

更进一步:子组件不想暴露 DOM,想暴露方法,用 useImperativeHandle

jsx
const FancyInput = forwardRef(function FancyInput(_, ref) { const inputRef = useRef(null) useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), clear: () => (inputRef.current.value = ''), })) return <input ref={inputRef} /> })

常见误区清单

  • 以为改 ref.current 会刷新 UI(不会)
  • 把“渲染相关数据”放 ref,导致 UI 不更新
  • 在 ref 里存对象并原地修改,然后又指望组件重渲染
  • 依赖 ref.current 写进 useEffect deps(通常没意义,因为 ref 引用不变;应依赖真正的 state/props)

记忆模型

  • useRef 给你一个“永远同一个盒子”
  • ref.current 是盒子里的内容,可以随意改
  • 改盒子里的内容不会触发渲染
  • DOM ref 是 useRef 的一个经典应用场景

最小练习题

实现一个输入框与按钮:点击按钮时输出“上一次输入值”和“当前输入值”。

提示:用 useRef 保存上一次值。

jsx
function LastValue() { const [value, setValue] = useState('') const lastRef = useRef('') const log = () => { console.log('last:', lastRef.current, 'current:', value) lastRef.current = value } return ( <> <input value={value} onChange={(e) => setValue(e.target.value)} /> <button onClick={log}>log</button> </> ) }

留言讨论

Discussion

欢迎交流与反馈