pidan

pidan

写代码是因为爱,写到世界充满爱!
medium
twitter
telegram
follow

React 中 ref, forwardRef 以及 useImperativeHandle 的使用场景

前言#

React 中,refforwardRef 并不是在所有情况下都必须用到。 React 鼓励使用声明式的方式构建 UI,通过 propsstate 来驱动视图更新。 refforwardRef 属于更偏向命令式的操作,通常在需要进行 DOM 操作或者访问组件实例时才会被使用。

使用场景#

什么情况下 必须 (或者说非常需要 / 强烈建议) 使用 refforwardRef:

1. 直接操作 DOM 元素:

这是 ref 最主要也是最经典的应用场景。 在 React 的声明式世界里,我们通常避免直接操作 DOM。 但是,有些场景下,必须 或者 直接操作 DOM 会更方便高效

  • 焦点管理 (Focus Management):
    • 聚焦到特定元素: 比如,表单加载后自动聚焦到第一个输入框,或者模态框打开后聚焦到第一个可交互元素。
    • 失去焦点时执行操作: 例如,在输入框失去焦点时触发验证。
  • 文本选择 (Text Selection) 和 操作:
    • 程序化地选中输入框中的文本: 例如,点击一个按钮后,自动选中输入框中的所有文本。
    • 获取或设置文本框的光标位置。
  • 媒体元素控制 (Media Element Control):
    • 控制 <video><audio> 元素的播放、暂停、音量等: 例如,实现自定义的视频播放器控制。
    • 监听媒体元素的事件: 例如,监听视频播放结束事件。
  • Canvas 元素操作:
    • 获取 Canvas 元素的 Context (2D 或 WebGL) 进行绘图操作。
  • 集成第三方 DOM 操作库:
    • 当你需要集成一些直接操作 DOM 的第三方库 (虽然在现代 React 应用中这种情况越来越少见),例如某些旧的 jQuery 插件,或者一些需要直接访问 DOM 元素的图表库。
  • 触发 DOM 元素的原生方法:
    • 例如,触发 input 元素的 focus(), blur(), click(), select() 等方法。

示例:聚焦输入框

import React, { useRef, useEffect } from 'react';

function MyForm() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后,聚焦到 input 元素
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input type="text" ref={inputRef} placeholder="请输入..." />
      {/* ... 其他表单元素 ... */}
    </div>
  );
}

2. 访问子组件的 DOM 节点 (需要 forwardRef):

默认情况下,父组件无法直接通过 ref 访问子组件的 DOM 节点。 如果你想要在父组件中获取子组件的 DOM 元素 (例如,子组件是一个自定义的 Input 组件,父组件需要获取其内部的 <input> 元素), 就需要使用 forwardRef 将 ref 传递到子组件内部的 DOM 元素上。

为什么需要 forwardRef?

这是因为 ref 默认只能绑定到组件实例 (对于 class 组件) 或 DOM 元素。 对于函数式组件,默认情况下 ref 会绑定到组件本身,但函数式组件本身没有实例,所以 ref 通常为 nullforwardRef 允许你将父组件传递的 ref 转发到子组件的某个 DOM 元素或 class 组件实例上。

示例:父组件获取子组件 Input 的 DOM 元素

import React, { useRef, forwardRef, useImperativeHandle, useEffect } from 'react';

// 子组件 - 自定义 Input 组件
const CustomInput = forwardRef((props, ref) => {
  const inputElementRef = useRef(null);

  // 使用 useImperativeHandle 暴露子组件的方法 (可选,非 forwardRef 必须)
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputElementRef.current.focus();
    },
    getValue: () => {
      return inputElementRef.current.value;
    }
  }));

  return (
    <div>
      <label htmlFor={props.id}>{props.label}: </label>
      <input type="text" id={props.id} ref={inputElementRef} {...props} />
    </div>
  );
});
CustomInput.displayName = 'CustomInput'; // 方便 React DevTools 显示

// 父组件
function ParentComponent() {
  const customInputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后,聚焦到 CustomInput 组件内部的 input 元素
    customInputRef.current.focus();

    // 获取 CustomInput 组件内部 input 的值 (通过 useImperativeHandle 暴露的方法)
    // const inputValue = customInputRef.current.getValue();
    // console.log("Input Value:", inputValue);

  }, []);

  return (
    <div>
      <CustomInput label="姓名" id="nameInput" ref={customInputRef} />
      {/* ... 其他内容 ... */}
    </div>
  );
}

3. 在某些特定的组件库或场景中需要访问组件实例 (通过 useImperativeHandle 结合 forwardRef):

虽然 React 提倡数据驱动,但有些情况下,你可能需要在父组件中命令式地调用子组件的方法,例如:

  • 控制子组件的内部状态或行为: 例如,触发一个弹窗组件的 open()close() 方法,或者调用一个表格组件的 refreshData() 方法。
  • 集成某些第三方 UI 组件库: 有些组件库可能会提供命令式 API,需要通过 ref 获取组件实例来调用其方法。

使用 useImperativeHandle 控制暴露给父组件的 API:

当你需要让父组件通过 ref 访问子组件的方法时,通常会结合 forwardRefuseImperativeHandle Hook。 useImperativeHandle 允许你 精确地控制哪些方法和属性可以通过 ref 暴露给父组件,避免暴露所有内部细节。

什么时候应该避免使用 refforwardRef:

  • 一切都应该优先考虑声明式方式 (props 和 state)。 尽量通过 props 传递数据和回调函数来控制子组件的行为和状态。
  • 避免过度使用 ref 进行组件间的通信。 过多的 ref 使用会使组件关系变得复杂和难以维护,破坏 React 的数据流。
  • 不要为了访问子组件的 state 而使用 ref 应该通过 props 传递数据,让子组件将数据通过回调函数传递给父组件 (状态提升)。
  • 在没有明确必要性的情况下,不要滥用 forwardRef 如果父组件不需要直接访问子组件的 DOM 或实例,就不需要使用 forwardRef

总结:

refforwardRef 是 React 中强大的工具,但应该谨慎使用。 它们在以下情况是必要的或非常有用的:

  • 需要直接操作 DOM 元素时 (例如,焦点管理、媒体控制等)。
  • 需要在父组件中访问子组件的 DOM 节点时 (使用 forwardRef)。
  • 在某些特定场景下,需要命令式地调用子组件的方法时 (使用 forwardRefuseImperativeHandle)。

记住,优先考虑声明式的方式解决问题。 只有在声明式方式难以实现或者命令式操作更加高效简洁的情况下,才考虑使用 refforwardRef 避免过度使用它们,保持代码的清晰和可维护性。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。