useRef 有哪些用法?怎么把 ref 传给子组件?

进阶高频实践约 8 分钟读完

一句话回答

useRef(initial) 返回一个在组件整个生命周期内不变的对象 { current },修改 current 不会触发重新渲染。两个用途:一是访问 DOM,把 ref 交给元素的 ref 属性,commit 后 current 就是 DOM 节点;二是保存渲染用不到的可变值,比如定时器 id、最新的回调。把 ref 传给子组件:React 19 起函数组件可以直接从 props 里拿到 ref,之前要用 forwardRef 包一层;只想暴露几个方法时用 useImperativeHandle。ref 不要在渲染过程中读写,应该在 effect 和事件处理函数里用。

详细解析

两种用途

JSX
function Stopwatch() {
  const [elapsed, setElapsed] = useState(0)
  const timerRef = useRef(null) // 保存定时器 id:界面不需要显示它
  const inputRef = useRef(null) // 访问 DOM

  function start() {
    clearInterval(timerRef.current)
    timerRef.current = setInterval(() => setElapsed((t) => t + 1), 1000)
    inputRef.current.focus()
  }

  return (
    <>
      <input ref={inputRef} placeholder="备注" />
      <p>{elapsed} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={() => clearInterval(timerRef.current)}>停止</button>
    </>
  )
}
useState useRef
修改后重新渲染 会 不会
修改后何时能读到新值 下一次渲染 立即
适合 要显示在界面上的数据 定时器 id、DOM 节点、最新的回调(避免闭包陷阱)、上一次的值

不要在渲染期间读写 ref.current

渲染应该是纯的。React 不跟踪 ref 的变化,渲染时读 ref.current,界面可能和数据对不上;渲染时写它,在并发渲染或 StrictMode 下组件函数可能被调用多次,结果不可预测。例外是惰性初始化:

JSX
const playerRef = useRef(null)
if (playerRef.current === null) {
  playerRef.current = new VideoPlayer() // 只在第一次渲染时创建,结果每次都一样
}

另外,DOM ref 要等 commit 后才有值,首次渲染时 inputRef.current 还是 null。

ref 回调

ref 属性也可以是函数,节点挂载时以 DOM 节点为参数调用。React 19 起它可以返回清理函数,节点移除时执行,适合给一组动态的元素绑定观察器:

JSX
const observe = useCallback((node) => {
  const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width))
  observer.observe(node)
  return () => observer.disconnect() // 节点卸载时执行
}, [])

return <div ref={observe}>...</div>

ref 回调的函数引用变了,React 会先执行旧的清理(或者以 null 调用旧回调),再用新回调重新绑定。写成内联箭头函数,每次渲染都会解绑再绑定一次,开销大时用 useCallback 稳定引用。

把 ref 传给子组件

JSX
// React 19:ref 就是普通的 prop
function MyInput({ ref, ...props }) {
  return <input ref={ref} {...props} />
}

// React 18 及以前:用 forwardRef 接收第二个参数
const MyInput = forwardRef(function MyInput(props, ref) {
  return <input ref={ref} {...props} />
})

forwardRef 在 React 19 中仍然可用,官方计划以后废弃。类组件上的 ref 仍然指向组件实例,不会作为 prop 传入。

代码示例:只暴露 focus 和 clear

只暴露有限的方法,子组件可以自由修改内部实现,父组件也不能随意改动子组件的 DOM。

JSX
import { useRef, useImperativeHandle } from 'react'

function SearchInput({ ref, ...props }) {
  const inputRef = useRef(null)
  // 父组件拿到的是这个对象,而不是整个 input 元素
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => {
      inputRef.current.value = ''
    },
  }), [])
  return <input ref={inputRef} {...props} />
}

function Toolbar() {
  const searchRef = useRef(null)
  return (
    <>
      <SearchInput ref={searchRef} defaultValue="react" />
      <button onClick={() => searchRef.current.focus()}>聚焦</button>
      <button onClick={() => searchRef.current.clear()}>清空</button>
    </>
  )
}

面试官可能追问

为什么修改 ref 不会触发渲染?useRef 是怎么实现的?

useRef 在首次渲染时创建 { current: initial },保存在 Hook 链表上,之后每次渲染都返回这个对象本身。修改 current 只是改了一个普通对象的属性,没有经过 setState,React 不知道它变了,见 Hooks 的原理。可以把它理解成"不触发渲染的 useState":useState({ current: initial })[0]。

什么场景应该用 ref,什么场景应该用 props?

能用 props 描述的状态就用 props,比如弹窗是否打开传 isOpen,而不是暴露 open()、close()。ref 只适合命令式的操作:聚焦、滚动到某个位置、播放视频、选中文本、触发动画,以及集成不受 React 管理的第三方库。

列表中每一项都需要 ref,怎么办?

Hook 不能在循环里调用,不能给每一项调用 useRef。可以用一个 ref 保存 Map,在 ref 回调里按 id 存入节点、在清理函数里删除:ref={(node) => { map.set(item.id, node); return () => map.delete(item.id) }},需要时用 id 从 Map 里取。

易错点

  • 在渲染时读写 ref.current(惰性初始化除外),以及首次渲染时以为 DOM ref 已经有值
  • 用 ref 保存需要显示的数据,修改后界面不会更新
  • React 18 及以前,函数组件不用 forwardRef 就拿不到 ref;React 19 的 ref 回调如果用箭头函数隐式返回了值(如 ref={(n) => (el = n)}),返回值会被当作清理函数,要改成带花括号的函数体

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。