useImperativeHandle 从基础到实践

182 阅读5分钟

我正在参加Trae「超级体验官」创意实践征文,  本文所使用的 Trae 免费下载链接:  www.trae.ai/?utm_source…

本文中的代码均在Trae智能协作 AI IDE下编写,Trae提供了强大的Chat和Builder模式,帮助我快速生成和修改示例代码。并且内置了WebView,代码运行后无需切换浏览器,即可马上看到页面效果,并且可以交互验证功能,本文中的截图展示了在Trae WebView中验证代码运行效果。此外Trae的UI美观,无需复杂配置,简单易上手,推荐大家试用!!!

image.png


最近研究了一下 React 的 useImperativeHandle,虽然这个 Hook 用得不多,但一旦碰到合适的场景,它真的特别实用!所以,今天想跟大家聊聊这个 Hook 的用法,以及它到底能用来干嘛。


1. 先说说 ref

先来复习一下 ref,这个应该大家都用过吧?

  • ref 是干啥的?

    • 其实它就是让你直接操作 DOM 或者类组件实例的一个工具。
  • 啥时候用 ref

    • 比如要获取输入框的值,或者想给某个元素设置焦点的时候。

举个例子:

import React, { useRef } from "react";

export default function RefExample() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleFocus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} placeholder="点按钮聚焦输入框" />
      <button onClick={handleFocus}>聚焦输入框</button>
    </div>
  );
}

就这么简单,ref 就是直接让你操作到 DOM 元素。


2. forwardRef 又是个啥?

如果你用的是函数组件,ref 默认是传不进去的。这时候就得用 forwardRefref 转发到子组件里。

随便写个例子看看:

import React, { forwardRef, useRef } from "react";

const Input = forwardRef((props, ref) => {
  return <input ref={ref} placeholder="ForwardRef 示例" />;
});

export default function ForwardRefExample() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current?.focus();
  };

  return (
    <div>
      <Input ref={inputRef} />
      <button onClick={handleFocus}>聚焦输入框</button>
    </div>
  );
}

forwardRefref 就可以直接传给子组件了,是不是很方便?这个其实是 useImperativeHandle 的基础,先搞明白这个,后面就简单了。


3. useImperativeHandle 怎么用?

useImperativeHandle 的主要作用是让我们自定义暴露给父组件的内容,换句话说,它能让你精细控制父组件通过 ref 能干啥。

来,写个简单点的例子:

import React, { forwardRef, useRef, useImperativeHandle } from "react";

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => (inputRef.current!.value = ""),
  }));

  return <input ref={inputRef} placeholder="自定义输入框" />;
});

export default function UseImperativeHandleExample() {
  const inputRef = useRef<{ focus: () => void; clear: () => void }>(null);

  return (
    <div>
      <CustomInput ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>聚焦输入框</button>
      <button onClick={() => inputRef.current?.clear()}>清空输入框</button>
    </div>
  );
}

这里用 useImperativeHandle 自定义了两个方法:focusclear,父组件只能调用这些方法,而不是直接操作 ref


4. 这些场景用起来很爽

场景 1:表单操作

比如封装一个表单组件,可以直接通过 ref 提供表单的重置或校验功能,这样外层调用起来非常方便。

示例代码:

import React, { useRef, forwardRef, useImperativeHandle, useState } from "react";

const Form = forwardRef((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [message, setMessage] = useState<string>('');

  useImperativeHandle(ref, () => ({
    reset: () => {
      if (inputRef.current) {
        inputRef.current.value = '';
        setMessage("表单已重置");
      }
    },
    validate: () => {
      if (inputRef.current) {
        const value = inputRef.current.value;
        const specialCharsRegex = /[!@#$%^&*(),.?":{}|<>]/;

        if (specialCharsRegex.test(value)) {
          setMessage("验证失败:输入内容不能包含特殊字符");
          return false;
        }

        setMessage("验证通过");
        return true;
      }
    },
  }));

  return (
    <form>
      <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
        <input ref={inputRef} placeholder="输入点啥" />
        {message && <span style={{ color: message.includes('失败') ? 'red' : 'green' }}>{message}</span>}
      </div>
    </form>
  );
});

export default function FormExample() {
  const formRef = useRef<{ reset: () => void; validate: () => void }>(null);

  return (
    <div>
      <Form ref={formRef} />
      <button onClick={() => formRef.current?.reset()}>重置表单</button>
      <button onClick={() => formRef.current?.validate()}>校验表单</button>
    </div>
  );
}

image.png

场景 2:模态框控制

写一个模态框组件,暴露 openclose 方法给父组件。

示例代码:

import React, { useState, forwardRef, useRef, useImperativeHandle } from "react";

const Modal = forwardRef((props, ref) => {
  const [isVisible, setIsVisible] = useState(false);

  useImperativeHandle(ref, () => ({
    open: () => setIsVisible(true),
    close: () => setIsVisible(false),
  }));

  if (!isVisible) return null;

  return (
    <div style={{
      position: "fixed",
      top: 0,
      left: 0,
      width: "100%",
      height: "100%",
      background: "rgba(0, 0, 0, 0.5)",
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
    }}>
      <div style={{
        background: "white",
        padding: "20px",
        borderRadius: "8px",
        minWidth: "300px",
        textAlign: "center",
      }}>
        <p>模态框内容</p>
        <button onClick={() => setIsVisible(false)}>关闭</button>
      </div>
    </div>
  );
});

export default function ModalExample() {
  const modalRef = useRef<{ open: () => void; close: () => void }>(null);

  return (
    <div style={{ textAlign: "center", marginTop: "50px" }}>
      <button onClick={() => modalRef.current?.open()} style={{ padding: "10px 20px", fontSize: "16px" }}>
        打开模态框
      </button>
      <Modal ref={modalRef} />
    </div>
  );
}

image.png

场景 3:复杂交互组件

比如封装一个视频播放器,直接暴露 playpauseseek 方法,想想都很实用。

示例代码:

import React, { useRef, forwardRef, useImperativeHandle } from "react";

const VideoPlayer = forwardRef((props, ref) => {
  const videoRef = useRef<HTMLVideoElement>(null);

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time: number) => {
      if (videoRef.current) videoRef.current.currentTime = time;
    },
  }));

  return (
    <video ref={videoRef} controls style={{ width: "100%" }}>
      <source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4" />
    </video>
  );
});

export default function VideoPlayerExample() {
  const videoRef = useRef<{ play: () => void; pause: () => void; seek: (time: number) => void }>(null);

  return (
    <div>
      <VideoPlayer ref={videoRef} />
      <button onClick={() => videoRef.current?.play()}>播放</button>
      <button onClick={() => videoRef.current?.pause()}>暂停</button>
      <button onClick={() => videoRef.current?.seek(10)}>跳转到 10 秒</button>
    </div>
  );
}

image.png

5. 说点背后的逻辑

  1. 它到底改了啥?

    • 默认情况下,ref 会指向子组件的实例或 DOM 节点。
    • 用了 useImperativeHandle,你就可以自定义 ref 暴露的东西。
  2. 啥时候生效?

    • 它只有在组件挂载后才有效。
    • 如果 ref 是空的,那这个 Hook 也就啥都做不了。

6. 最后几点建议

  1. 别滥用

    • React 讲究单向数据流,useImperativeHandle 是个偏底层的工具,用它的时候要确定真的需要。
    • 如果能用 props 实现组件通信,就尽量用 props
  2. 一定要配合 forwardRef

    • 在函数组件中,useImperativeHandle 必须和 forwardRef 一起用,不然直接会报错。
  3. 封装组件的时候最实用

    • 特别是那种需要直接操作 DOM 或实现复杂交互的组件,比如模态框、播放器、表单等。

总结

useImperativeHandle 虽不是常用 Hook,但在特定场景下作用显著,能增强代码的灵活性与可维护性。无论是表单操作、模态框控制还是复杂交互组件,它都能发挥重要作用。

掌握 useImperativeHandle 并合理运用,能让我们在 React 开发中更得心应手。希望本文能助你理解它,在需要时能熟练运用。有什么想法,评论区见~