hook:useImperativeHandle
- 在父组件中定义 ref 并传入子组件时,自定义 子组件 能暴露给 父组件 的内容
- 核心作用: 不暴露子组件内的整个 DOM,只暴露特定的方法或属性
- 必须配合:
forwardRef一起使用
基本语法
useImperativeHandle(ref, createHandle, [deps]);
// ref: 父组件传入的 ref
// createHandle: 返回要暴露给父组件的对象
// deps: 依赖数组(可选)
场景代码示例
-
【场景 1】暴露特定方法(最常用):
-
子组件只暴露 focus 方法,而不是整个 input 元素
子组件
import { forwardRef, useRef, useImperativeHandle } from "react"; // 必须用 forwardRef 包裹 const TextInput = forwardRef((props, ref) => { const inputRef = useRef(); // 自定义暴露给父组件的内容 useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, })); return <input type="text" ref={inputRef} />; });父组件
export default function Parent() { const inputRef = useRef(); return ( <div> <button onClick={() => inputRef.current.focus()}>聚焦</button> <TextInput ref={inputRef} /> </div> ); } -
对比不使用 useImperativeHandle:
// ❌ 不使用 useImperativeHandle,父组件能访问整个 input DOM const TextInput = forwardRef((props, ref) => { return <input type="text" ref={ref} />; }); // 父组件可以调用所有 input 的原生方法 inputRef.current.focus(); inputRef.current.blur(); inputRef.current.select(); // ... 等等,暴露了太多内容
-
-
【场景 2】暴露多个方法(常用):
-
子组件暴露多个自定义方法
子组件
import { forwardRef, useRef, useImperativeHandle, useState } from "react"; const Modal = forwardRef((props, ref) => { const [isOpen, setIsOpen] = useState(false); const dialogRef = useRef(); useImperativeHandle(ref, () => ({ open: () => { setIsOpen(true); // 打开后聚焦到第一个输入框 setTimeout( () => dialogRef.current?.querySelector("input")?.focus(), 0 ); }, close: () => { setIsOpen(false); }, toggle: () => { setIsOpen((prev) => !prev); }, })); if (!isOpen) return null; return ( <div className="modal" ref={dialogRef}> <div className="modal-content"> {props.children} <button onClick={() => setIsOpen(false)}>关闭</button> </div> </div> ); });父组件
export default function Parent() { const modalRef = useRef(); return ( <div> <button onClick={() => modalRef.current.open()}>打开模态框</button> <button onClick={() => modalRef.current.close()}>关闭模态框</button> <button onClick={() => modalRef.current.toggle()}>切换模态框</button> <Modal ref={modalRef}> <h2>这是一个模态框</h2> <input type="text" placeholder="输入内容" /> </Modal> </div> ); } -
优势: 封装内部实现,只暴露必要的 API
-
-
【场景 3】暴露状态和方法(常用):
-
子组件同时暴露状态和方法
子组件
import { forwardRef, useRef, useImperativeHandle, useState } from "react"; const Video = forwardRef((props, ref) => { const videoRef = useRef(); const [isPlaying, setIsPlaying] = useState(false); useImperativeHandle(ref, () => ({ // 暴露方法 play: () => { videoRef.current.play(); setIsPlaying(true); }, pause: () => { videoRef.current.pause(); setIsPlaying(false); }, seek: (time) => { videoRef.current.currentTime = time; }, // 暴露状态 isPlaying, // 暴露计算属性 getCurrentTime: () => videoRef.current.currentTime, getDuration: () => videoRef.current.duration, })); return <video ref={videoRef} src={props.src} />; });父组件
export default function VideoPlayer() { const videoRef = useRef(); const handlePlay = () => { videoRef.current.play(); console.log("播放状态:", videoRef.current.isPlaying); }; const handleSeek = () => { videoRef.current.seek(30); // 跳转到 30 秒 console.log("当前时间:", videoRef.current.getCurrentTime()); }; return ( <div> <button onClick={handlePlay}>播放</button> <button onClick={() => videoRef.current.pause()}>暂停</button> <button onClick={handleSeek}>跳转到 30 秒</button> <Video ref={videoRef} src="video.mp4" /> </div> ); }
-
-
【场景 4】配合依赖数组优化(优化):
-
使用依赖数组避免不必要的重新创建
const Counter = forwardRef((props, ref) => { const [count, setCount] = useState(0); // ❌ 没有依赖数组,每次渲染都会重新创建 useImperativeHandle(ref, () => ({ reset: () => setCount(0), increment: () => setCount((c) => c + 1), })); // ✅ 使用依赖数组,只在必要时重新创建 useImperativeHandle( ref, () => ({ reset: () => setCount(0), increment: () => setCount((c) => c + 1), getCount: () => count, // 依赖 count }), [count] // 只有 count 变化时才重新创建 ); return <div>Count: {count}</div>; });
-
-
【场景 5】封装第三方库组件(进阶):
-
封装第三方库,暴露简化的 API
封装富文本编辑器
import { forwardRef, useRef, useImperativeHandle, useEffect } from "react"; import Quill from "quill"; // 假设使用 Quill 编辑器 const RichTextEditor = forwardRef((props, ref) => { const editorRef = useRef(); const quillRef = useRef(); useEffect(() => { // 初始化 Quill quillRef.current = new Quill(editorRef.current, { theme: "snow", }); }, []); useImperativeHandle(ref, () => ({ // 暴露简化的方法 getContent: () => quillRef.current.root.innerHTML, setContent: (html) => (quillRef.current.root.innerHTML = html), clear: () => quillRef.current.setText(""), focus: () => quillRef.current.focus(), insertText: (text) => { const selection = quillRef.current.getSelection(); quillRef.current.insertText(selection?.index || 0, text); }, })); return <div ref={editorRef} />; });父组件
export default function Editor() { const editorRef = useRef(); const handleSave = () => { const content = editorRef.current.getContent(); console.log("保存内容:", content); }; return ( <div> <button onClick={handleSave}>保存</button> <button onClick={() => editorRef.current.clear()}>清空</button> <button onClick={() => editorRef.current.insertText("Hello!")}> 插入文本 </button> <RichTextEditor ref={editorRef} /> </div> ); } -
优势: 隐藏第三方库的复杂 API,提供统一简洁的接口
-
-
【场景 6】TypeScript 类型定义(最佳实践):
-
为暴露的方法定义清晰的类型
import { forwardRef, useRef, useImperativeHandle, ForwardedRef, } from "react"; // 定义暴露的 API 类型 export interface TextInputRef { focus: () => void; blur: () => void; getValue: () => string; setValue: (value: string) => void; } // 定义 props 类型 interface TextInputProps { placeholder?: string; defaultValue?: string; } const TextInput = forwardRef<TextInputRef, TextInputProps>((props, ref) => { const inputRef = useRef<HTMLInputElement>(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current?.focus(); }, blur: () => { inputRef.current?.blur(); }, getValue: () => { return inputRef.current?.value || ""; }, setValue: (value: string) => { if (inputRef.current) { inputRef.current.value = value; } }, })); return ( <input ref={inputRef} type="text" placeholder={props.placeholder} defaultValue={props.defaultValue} /> ); });父组件(TypeScript)
import { useRef } from "react"; import TextInput, { TextInputRef } from "./TextInput"; export default function Parent() { const inputRef = useRef<TextInputRef>(null); const handleClick = () => { inputRef.current?.focus(); // 类型安全 const value = inputRef.current?.getValue(); // 类型提示 console.log(value); }; return ( <div> <button onClick={handleClick}>操作输入框</button> <TextInput ref={inputRef} placeholder="输入内容" /> </div> ); }
-
useImperativeHandle vs 直接传递 ref
| 方式 | 暴露的内容 | 封装性 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| 直接传递 ref | 整个 DOM 元素 | ❌ 差 | ✅ 高 | 简单场景,直接操作 DOM |
| useImperativeHandle | 自定义方法和属性 | ✅ 好 | ✅ 高 | 需要封装、限制暴露内容 |
| useImperativeHandle + 空对象 | 不暴露任何内容 | ✅ 最好 | ❌ 低 | 完全隐藏内部实现 |
常见应用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 表单控件封装 | 暴露 focus、blur、validate 等方法 | 输入框、下拉框 |
| 模态框/弹窗 | 暴露 open、close、toggle 方法 | Dialog、Modal、Drawer |
| 媒体控件 | 暴露 play、pause、seek 等方法 | 视频播放器、音频播放器 |
| 第三方库封装 | 暴露简化的 API | 富文本编辑器、地图组件 |
| 复杂组件的命令式操作 | 父组件需要主动触发子组件的某些行为 | 刷新列表、重置表单 |
| 获取子组件内部状态 | 父组件需要读取子组件的内部数据 | 获取输入框值、获取滚动位置 |
注意事项
-
⚠️ 必须配合 forwardRef 使用
// ❌ 错误:没有使用 forwardRef const TextInput = (props, ref) => { useImperativeHandle(ref, () => ({ focus: () => {} })); return <input />; }; // ✅ 正确:使用 forwardRef const TextInput = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus: () => {} })); return <input />; }); -
⚠️ 避免过度使用
// ❌ 不推荐:简单场景直接传 ref 即可 const TextInput = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), })); return <input ref={inputRef} />; }); // ✅ 推荐:简单场景直接传 ref const TextInput = forwardRef((props, ref) => { return <input ref={ref} />; }); -
⚠️ 不要暴露整个 DOM
// ❌ 错误:失去了封装的意义 useImperativeHandle(ref, () => inputRef.current); // ✅ 正确:只暴露必要的方法 useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), getValue: () => inputRef.current.value, })); -
✅ 使用 TypeScript 定义清晰的类型
// 为暴露的 API 定义接口 interface MyComponentRef { method1: () => void; method2: (arg: string) => void; } const MyComponent = forwardRef<MyComponentRef, Props>((props, ref) => { useImperativeHandle(ref, () => ({ method1: () => {}, method2: (arg) => {}, })); }); -
✅ 优先使用声明式编程
// ❌ 命令式:父组件控制子组件 const Parent = () => { const modalRef = useRef(); return ( <div> <button onClick={() => modalRef.current.open()}>打开</button> <Modal ref={modalRef} /> </div> ); }; // ✅ 声明式:通过 props 控制(更符合 React 思想) const Parent = () => { const [isOpen, setIsOpen] = useState(false); return ( <div> <button onClick={() => setIsOpen(true)}>打开</button> <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} /> </div> ); };但在以下场景,命令式更合适:
- 需要主动触发动画
- 需要主动聚焦元素
- 需要调用第三方库的方法
- 需要主动滚动到某个位置
-
✅ 使用依赖数组优化性能
// 只在依赖变化时重新创建方法对象 useImperativeHandle( ref, () => ({ getValue: () => value, }), [value], );