
ref 存的是「不用来渲染的值」
const countRef = useRef(0)
countRef.current += 1 // 界面不会变useRef(0) 返回一个对象,形状固定是 { current: 0 }。这个对象在组件生命周期里是同一个引用,可变的只有 .current。
它和 useState 的分工很清楚:值要出现在界面上就用 state,改它会重渲染;值只是被逻辑用一下,改它不该引起重渲染,就用 ref。计时器 ID、上一次的值、DOM 节点、第三方库实例,都属于后者。
拿到 DOM 节点
function InputFocus() { const inputRef = useRef(null)
useEffect(() => { inputRef.current.focus() }, [])
return <input ref={inputRef} />}要点有三个。挂载前 inputRef.current 是 null,所以读之前要判空。TypeScript 里写 useRef<HTMLInputElement>(null),让它知道将来装的是什么。要操作 DOM 就放在 Effect 里,不在渲染期间做。
如果不想为此起一个 Effect,用 ref 回调:
<div ref={node => { if (node) node.scrollIntoView() }}/>React 在提交阶段调用它,参数是真实节点。
ref 回调可以返回清理函数
React 19 起,ref 回调允许返回一个清理函数,卸载时 React 调用它:
<div ref={node => { const observer = new ResizeObserver(entries => { console.log(entries[0].contentRect) }) observer.observe(node) return () => observer.disconnect() }}/>旧写法是在回调里判断 node === null 来做清理。官方说将来会弃用「卸载时以 null 调用 ref 回调」这种做法(React 19 发布说明),新代码用返回清理函数的形式。
注意返回值的类型:ref={c => (instance = c)} 会被 TypeScript 拒绝,因为它返回了赋值结果。写成块体 ref={c => { instance = c }}。
把 ref 交给自己写的组件
React 18 里必须先包一层 forwardRef:
// 18 写法const MyInput = forwardRef(function MyInput(props, ref) { return <input {...props} ref={ref} />})React 19 起 ref 就是一个普通 prop,直接收:
// 19 写法function MyInput({ value, onChange, ref }) { return <input value={value} onChange={onChange} ref={ref} />}forwardRef 还没有删除,但官方已标为弃用(forwardRef)。读旧代码时认识它,新建组件不用它。
一个例外:传给类组件的 ref 不是 prop,它指向组件实例。
要向父组件暴露自定义方法时,用 useImperativeHandle,见下一篇。
两件别做的事
别在渲染期间读写 ref.current。渲染要纯:同样的 props 与 state 应该得到同样的 JSX。渲染里写 ref,在并发渲染下会读到中间值。要在渲染里计算东西,用 state 或在渲染中直接算。
别用 ref 驱动界面。ref 变了不触发重渲染,界面和数据就脱节了。
React 19 改了 useRef 的类型
useRef() 不传参现在过不了类型检查,写 useRef(null) 或 useRef(undefined)。这是类型要求的收紧,不是运行时错误,旧代码里那行 useRef() 在运行时不报错(React 19 升级指南)。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 190 天
部分内容可能已过时