Refs 转发

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。最常见的案例如下所述。

转发 refs 到 DOM 组件

考虑这个渲染原生 DOM 元素 buttonFancyButton 组件:

  1. function FancyButton(props) {
  2. return (
  3. <button className="FancyButton">
  4. {props.children}
  5. </button>
  6. );
  7. }

React 组件隐藏其实现细节,包括其渲染结果。其他使用 FancyButton 的组件通常不需要获取内部的 DOM 元素 buttonref。这很好,因为这防止组件过度依赖其他组件的 DOM 结构。

虽然这种封装对类似 FeedStoryComment 这样的应用级组件是理想的,但其对 FancyButtonMyTextInput 这样的高可复用“叶”组件来说可能是不方便的。这些组件倾向于在整个应用中以一种类似常规 DOM buttoninput 的方式被使用,并且访问其 DOM 节点对管理焦点,选中或动画来说是不可避免的。

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

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

  1. const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children}
  2. </button>
  3. ));
  4. // 你可以直接获取 DOM button 的 ref:
  5. const ref = React.createRef();
  6. <FancyButton ref={ref}>Click me!</FancyButton>;

这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

以下是对上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>
  3. React 传递 refforwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

注意

第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref

Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

组件库维护者的注意事项

当你开始在组件库中使用 forwardRef 时,你应当将其视为一个破坏性更改,并发布库的一个新的主版本。 这是因为你的库可能会有明显不同的行为(例如 refs 被分配给了谁,以及导出了什么类型),并且这样可能会导致依赖旧行为的应用和其他库崩溃。

出于同样的原因,当 React.forwardRef 存在时有条件地使用它也是不推荐的:它改变了你的库的行为,并在升级 React 自身时破坏用户的应用。

在高阶组件中转发 refs

这个技巧对高阶组件(也被称为 HOC)特别有用。让我们从一个输出组件 props 到控制台的 HOC 示例开始:

  1. function logProps(WrappedComponent) { class LogProps extends React.Component {
  2. componentDidUpdate(prevProps) {
  3. console.log('old props:', prevProps);
  4. console.log('new props:', this.props);
  5. }
  6. render() {
  7. return <WrappedComponent {...this.props} />; }
  8. }
  9. return LogProps;
  10. }

“logProps” HOC 透传(pass through)所有 props 到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 “fancy button” 组件的 props:

  1. class FancyButton extends React.Component {
  2. focus() {
  3. // ...
  4. }
  5. // ...
  6. }
  7. // 我们导出 LogProps,而不是 FancyButton。
  8. // 虽然它也会渲染一个 FancyButton。
  9. export default logProps(FancyButton);

下面的示例有一点需要注意:refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

这意味着用于我们 FancyButton 组件的 refs 实际上将被挂载到 LogProps 组件:

  1. import FancyButton from './FancyButton';
  2. const ref = React.createRef();
  3. // 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
  4. // 尽管渲染结果将是一样的,
  5. // 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
  6. // 这意味着我们不能调用例如 ref.current.focus() 这样的方法
  7. <FancyButton
  8. label="Click Me"
  9. handleClick={handleClick}
  10. ref={ref}/>;

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

  1. function logProps(Component) {
  2. class LogProps extends React.Component {
  3. componentDidUpdate(prevProps) {
  4. console.log('old props:', prevProps);
  5. console.log('new props:', this.props);
  6. }
  7. render() {
  8. const {forwardedRef, ...rest} = this.props;
  9. // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
  10. return <Component ref={forwardedRef} {...rest} />; }
  11. }
  12. // 注意 React.forwardRef 回调的第二个参数 “ref”。
  13. // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  14. // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  15. return React.forwardRef((props, ref) => { return <LogProps {...props} forwardedRef={ref} />; });}

在 DevTools 中显示自定义名称

React.forwardRef 接受一个渲染函数。React DevTools 使用该函数来决定为 ref 转发组件显示的内容。

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

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

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

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

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

  1. function logProps(Component) {
  2. class LogProps extends React.Component {
  3. // ...
  4. }
  5. function forwardRef(props, ref) {
  6. return <LogProps {...props} forwardedRef={ref} />;
  7. }
  8. // 在 DevTools 中为该组件提供一个更有用的显示名。
  9. // 例如 “ForwardRef(logProps(MyComponent))”
  10. const name = Component.displayName || Component.name; forwardRef.displayName = `logProps(${name})`;
  11. return React.forwardRef(forwardRef);
  12. }