Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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],
    );