React-核心hook:forwardRef

64 阅读2分钟

前言

在 React 的世界里,Props 是数据传递的主旋律。然而,有两位特殊的客人——keyref,它们并不会被包含在 props 中。如果你尝试在普通函数组件中接收 ref,只会得到 undefined。为了解决这个问题,forwardRef 应运而生。

一、 产生背景:消失的 Ref

在 React 中,函数组件没有实例(Instance),因此父组件无法像操作 Class 组件那样通过 ref 直接获取函数组件的引用。此外,React 对 ref 做了特殊处理,它不会像普通属性那样通过 props 传递。

二、 核心概念:什么是 forwardRef?

forwardRef(引用转发)是一个高阶组件工具,它允许函数组件接收父组件传递的 ref,并将其进一步“转发”给内部的 DOM 元素或子组件。

语法定义

React.forwardRef((props,ref)=>{})​,它会包裹一个渲染函数,这个渲染函数接受两个参数,第一个参数为props,第二个参数为父组件传递的ref

// 子组件:使用 forwardRef
const ChildComponent = React.forwardRef<HTMLInputElement, any>((props, ref) => {
  // 将 ref 绑定到内部的 button 元素上
  return <button ref={ref} {...props}>Click Me</button>;
});

// 父组件
const ParentComponent: React.FC = () => {
  const buttonRef = useRef(null);

  const handleClick = () => {
    // 现在可以通过 ref 直接操作子组件内部的 button DOM 元素
    buttonRef.current.focus();
  };

  return (
    <>
      <ChildComponent ref={buttonRef} />
      <button onClick={handleClick}>Focus the Button</button>
    </>
  );
}

三、 实战:跨组件操控 DOM (TSX 实现)

这是 forwardRef 最经典的使用场景:父组件直接聚焦子组件内部的输入框。

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

// 1. 定义子组件 Props 类型
interface ChildProps {
  label: string;
}

// 2. 使用 forwardRef 包裹渲染函数
// 第一个泛型是 Ref 类型,第二个是 Props 类型
const MyInput = forwardRef<HTMLInputElement, ChildProps>((props, ref) => {
  return (
    <div className="input-group">
      <label>{props.label}</label>
      {/* 3. 将接收到的 ref 绑定到具体的 DOM 元素上 */}
      <input ref={ref} type="text" className="input-field" />
    </div>
  );
});

// 父组件
const Home: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleFocus = () => {
    // 4. 直接通过 current 操作子组件内部的 DOM
    inputRef.current?.focus();
  };

  return (
    <div style={{ padding: '20px' }}>
      <MyInput ref={inputRef} label="用户名:" />
      <button onClick={handleFocus} style={{ marginTop: '10px' }}>
        点击聚焦子组件输入框
      </button>
    </div>
  );
};

export default Home;

四、 进阶使用场景:暴露子组件方法

除了访问 DOM,我们还可以利用 forwardRef 配合 useImperativeHandle 来让父组件调用子组件内部定义的特定方法。

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

// 定义子组件向外暴露的接口
export interface ChildActions {
  scrollIntoView: () => void;
  showMessage: () => void;
}

const FancyChild = forwardRef<ChildActions, {}>((props, ref) => {
  const internalRef = useRef<HTMLDivElement>(null);

  // 使用 useImperativeHandle 自定义暴露给父组件的内容
  useImperativeHandle(ref, () => ({
    scrollIntoView: () => {
      internalRef.current?.scrollIntoView({ behavior: 'smooth' });
    },
    showMessage: () => {
      alert("这是子组件内部的方法!");
    }
  }));

  return <div ref={internalRef} style={{ height: '100px', background: '#eee' }}>我是子组件</div>;
});

const Parent: React.FC = () => {
  const actionsRef = useRef<ChildActions>(null);

  return (
    <>
      <FancyChild ref={actionsRef} />
      <button onClick={() => actionsRef.current?.showMessage()}>
        调用子组件自定义方法
      </button>
    </>
  );
};

export default Parent;

五、 总结与注意事项

  1. 封装性原则:虽然 forwardRef 很强大,但不要过度使用。只有在像 Input、Button 这种基础 UI 组件,或者必须手动控制 DOM(聚焦、滚动、测量)时才推荐使用。
  2. 高阶组件 (HOC) :如果你使用了高阶组件包裹了被 forwardRef 的组件,确保 HOC 也转发了 ref