661 字
3 分钟
useImperativeHandle

默认会把整个 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 里。

第 16 篇,共 39 篇本文属于系列

useImperativeHandle
https://www.choria.top/posts/hook-use-imperative-handle/
作者
乔瑞雅·克林威尔
发布于
2026-05-02
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 190 天

部分内容可能已过时