
默认会把整个 DOM 节点交出去
function MyInput({ ref }) { return <input ref={ref} />}父组件拿到的 ref.current 就是那个 <input> 节点,它可以改样式、删节点、拿它的父节点,子组件封装过的内部结构等于全敞开。
useImperativeHandle 让你换掉这份「交出的东西」,只给几个明确的方法。
React 19 的写法
子组件:
import { useRef, useImperativeHandle } from 'react'
function MyInput({ ref }) { const inputRef = useRef(null)
useImperativeHandle(ref, () => ({ focus() { inputRef.current.focus() }, clear() { inputRef.current.value = '' }, }), [])
return <input ref={inputRef} />}父组件:
function Form() { const ref = useRef(null)
return ( <form> <MyInput ref={ref} /> <button type="button" onClick={() => ref.current.focus()}> 聚焦 </button> </form> )}两处要留意。子组件里收到的 ref 没有再交给 <input>,而是交给了 useImperativeHandle;真实节点另用一个内部 ref(inputRef)持有。父组件只能调到 focus 与 clear,拿不到节点本身。
React 18 及更早必须先包一层 forwardRef 才拿得到 ref:
// 18 写法const MyInput = forwardRef((props, ref) => { // ...})React 19 起 ref 就是一个普通 prop(useImperativeHandle)。读旧代码会看到 forwardRef,新代码不用它。
三个参数
useImperativeHandle(ref, createHandle, dependencies?)ref 是从 props 收到的那个。createHandle 是无参函数,返回你要暴露的对象。dependencies 可选,createHandle 里用到的 props、state 等响应式值都要列进去;省略时每次渲染都会重新创建暴露对象。React 用 Object.is 逐个比较依赖。
方法名不必和 DOM 一致
官方的例子是评论区的 Post 组件,它暴露一个 scrollAndFocusAddComment():点一次按钮,先滚到列表底部,再聚焦输入框。这种「一次触发几件事」的操作,打包成一个方法比让父组件按顺序调三次更稳。
官方其实不建议常用
这条值得单独记住。官方在参考页上的原话是:能用 props 表达的东西,就不要用 ref。
它举的反例是弹窗:与其暴露 { open, close } 两个方法,不如收一个 isOpen prop,写成 <Modal isOpen={isOpen} />,内部用 Effect 处理开合。
真正该用 ref 的只有无法用 props 表达的命令式行为:滚动到某个节点、聚焦、触发动画、选中文本。
几条边界
只暴露父组件真正需要的方法,不要把内部 ref 交出去,否则封装就白做了。
TypeScript 里给暴露的对象定义接口,父组件调错方法名时能在编译期发现:
interface MyInputHandle { focus: () => void clear: () => void}父组件别在渲染期间调用 ref.current 上的方法。ref 在提交阶段才赋值,渲染期间它可能还是 null;调用放在事件处理函数或 Effect 里。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
最后更新于 ,距今已过 190 天
部分内容可能已过时