转发 Refs

Ref 转发是一种自动将 ref 通过组件传递给子组件的技术。对于应用程序中的大多数组件,这通常不是必需的。但是,它对某些组件很有用,特别是在可重用的组件库中。最常见的情况如下所述。

转发 refs 给 DOM 组件

考虑一个渲染原生 button DOM 元素的 FancyButton 组件:

function FancyButton(props) {
  return (
    <button className="FancyButton">
      {props.children}
    </button>
  );
}

React 组件隐藏他们的实现细节,包括它们的渲染输出。使用 FancyButton 的其他组件 通常不需要 获得 ref 到内部 button DOM 元素。这很好,因为它可以防止组件过多依赖彼此的 DOM 结构。

虽然这样的封装对于像 FeedStoryComment 这样的应用程序级组件是可取的, 对于像 FancyButtonMyTextInput 这样的可高度重用的 “leaf” 组件来说可能会很不方便。 这些组件倾向于以与常规 DOM buttoninput 类似的方式在整个应用程序中使用, 并且访问他们的 DOM 节点可能不可避免地用于管理 焦点(focus),选择(selection)或动画(animations)。

Ref 转发是一种选择性加入的功能,可让某些组件接收他们收到的 ref,并将其向下传递(换句话说,“转发”)给孩子。

在下面的例子中, FancyButton 使用 React.forwardRef 来获取传递给它的 ref , 然后将其转发给它渲染的的 DOM button

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

通过这种方式,使用 FancyButton 的组件可以获得底层 button DOM 节点的引用并在必要时访问它 - 就像他们直接使用 DOM button 一样。

以下是对上述示例中发生情况逐步的说明:

  1. 我们通过调用 React.createRef 创建一个 React ref 并将其分配给 ref 变量。
  2. 通过将 ref 变量传递给指定ref为 JSX 属性的 <FancyButton ref={ref}>
  3. Reac t将ref传递给 forwardRef 中的 (props, ref) => ... 函数作为第二个参数。
  4. 我们将这个ref参数转发到指定ref为 JSX 属性的 <button ref = {ref}>
  5. 当附加 ref 时,ref.current 将指向 <button> DOM节点。

注意

第二个 ref 参数仅在使用 React.forwardRef 调用定义组件时才存在。常规函数或类组件不接收 ref 参数,而且 props 也不提供 ref 。

Ref 转发不限于 DOM 组件。您也可以将 refs 转发给类组件实例。

组件库维护者的注意事项

当你开始在组件库中使用 forwardRef 时,你应该将它视为一个重大变化,并发布库的一个新的主版本。这是因为你的库可能有明显不同的行为(例如什么 refs 被赋值,以及哪些类型被导出),这可能会破坏依赖于旧行为的应用和其他库。

出于同样的原因,也不建议有条件地应用 React.forwardRef :它会改变您的图书馆的行为方式,并在升级 React 本身时破坏用户的应用程序。

在高阶组件中转发 refs

该技术对于 高阶组件(也称为HOC)也特别有用。 让我们从一个 HOC示例开始,将组件 props 记录到控制台:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}

“logProps” HOC 将所有 props 传递给它包裹的组件, 所以渲染输出将是相同的。 例如,我们可以使用此 HOC 记录所有传递给 “fancy button” 组件的 props(属性):

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);

上面的例子有一个警告: refs 不会通过。那是因为 ref 不是 props(属性)。 像 key 一样,它的处理方式与 React 不同。 如果你添加一个 ref 到 HOC , 这个 ref 将引用最外面的容器组件,而不是包裹的组件。

这意味着打算给 FancyButton组件的 refs 实际上将被附加到 LogProps 组件:

import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

幸运的是,我们可以使用 React.forwardRef API 明确地将 ref 转发到内部的 FancyButton 组件。 React.forwardRef 接受一个渲染函数,该函数接收 propsref 参数并返回一个 React 节点。例如:

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // Assign the custom prop "forwardedRef" as a ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // Note the second param "ref" provided by React.forwardRef.
  // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
  // And it can then be attached to the Component.
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

在DevTools中显示一个自定义名称

React.forwardRef 接受渲染函数。 React DevTools 使用此函数来确定要为 ref 转发组件显示什么内容。

例如,以下组件将在 DevTools 中显示为 ”ForwardRef” :

const WrappedComponent = React.forwardRef((props, ref) => {
  return <LogProps {...props} forwardedRef={ref} />;
});

如果您命名渲染函数,DevTools 也将包含其名称(例如”ForwardRef(myFunction)”):

const WrappedComponent = React.forwardRef(
  function myFunction(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }
);

你甚至可以设置函数的 displayName 属性来包含你包裹的组件:

function logProps(Component) {
  class LogProps extends React.Component {
    // ...
  }

  function forwardRef(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }

  // Give this component a more helpful display name in DevTools.
  // e.g. "ForwardRef(logProps(MyComponent))"
  const name = Component.displayName || Component.name;
  forwardRef.displayName = `logProps(${name})`;

  return React.forwardRef(forwardRef);
}