React 学习笔记(4)—— Refs

262 阅读3分钟

React 提供了 Refs 来访问 DOM 节点和 React 元素。Refs 适用场景有:

  1. 管理焦点,如:文本选择或媒体播放等
  2. 强制触发动画
  3. 继承第三方 DOM 库

Refs 是在自上而下数据流之外强制修改子组件,应该尽可能通过声明式实现需求而不是直接使用 Refs,更改 state 属性所在组件层可能会有所帮助。

使用 Refs

class 组件和函数组件中的使用方式如下:

import React, { useCallback, useEffect, useMemo, useRef } from "react";
import type { RefObject } from "react";

/* class 组件 */
class CustomTextInput extends React.Component {
  inputDOMRef: RefObject<HTMLInputElement>;

  constructor(props) {
    super(props);
    this.inputDOMRef = React.createRef<HTMLInputElement>();
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    this.inputDOMRef.current?.focus();
  }

  render() {
    return (
      <div>
        <input
          type="text"
          placeholder="please input text"
          ref={this.inputDOMRef}
        />
      </div>
    );
  }
}

class AutoFocusTextInput extends React.Component {
  inputCPNRef: RefObject<CustomTextInput>;

  constructor(props) {
    super(props);
    this.inputCPNRef = React.createRef<CustomTextInput>();
  }

  componentDidMount() {
    this.inputCPNRef.current?.focusTextInput();
  }

  render() {
    return <CustomTextInput ref={this.inputCPNRef} />;
  }
}

/* 函数组件 */
function FCTextInput() {
  const inputDOMRef = useRef<HTMLInputElement>(null);
  const inputCPNRef = useRef<CustomTextInput>(null);
  const onDOMChange = useCallback(() => {
    inputCPNRef.current?.focusTextInput();
  }, [inputCPNRef]);
  
  useEffect(() => {
    inputDOMRef.current?.focus();
  }, []);

  return (
    <>
      <input
        type="text"
        placeholder="please input text"
        ref={inputDOMRef}
        onChange={onDOMChange}
      />
      <CustomTextInput ref={inputCPNRef} />
    </>
  );
}

创建 Refs

class 组件:

this.textRef = React.createRef();

函数组件:

const textRef = useRef();

Refs 使用 React.createRef()(class 组件)或 useRef()(函数组件)创建,并通过 ref 属性附加到 React 元素上。Refs 通常保存为实例属性(class 组件)或是局部变量(函数组件)。

访问 Refs

通过 ref 的 current 属性,可以获取 ref 的值:

  1. ref 属性作用于 HTML 元素,current 属性返回 DOM 元素;
  2. ref 属性作用于 class 组件,current 属性返回组件实例;
  3. ref 属性不能作用到函数组件上。

回调 Refs

React 允许 ref 属性接收一个函数,函数运行时相应的 DOM 元素或 class 组件实例将作为实参传入,这种使用方式称为“回调 Refs”,可以方便地将 DOM 元素或 class 组件实例保存到其它地方。

function ChildTextInput(props: { inputRef: (el: HTMLInputElement) => void }) {
  return <input type="text" placeholder="please input!" ref={props.inputRef} />;
}

class ParentTextInput extends React.Component {
  inputElement: HTMLInputElement;

  render() {
    return (
      <ChildTextInput
        inputRef={(el: HTMLInputElement) => {
          this.inputElement = el;
        }}
      />
    );
  }
}

ref 更新时机

ref 的值会在 componentDidMountcomponentDidUpdate 生命周期触发前更新;组件卸载时,ref 的值会重置为 null;回调 Refs 则会在相应时间节点接收相应的值作为实参。

如果 ref 属性直接接收一个函数表达式,该函数会被执行两次。由于每次渲染都是一个新的函数实例,React 首先会传入 null 清空旧的 ref,然后再传入新的 DOM 元素或 class 组件实例设置新的 ref。先将回调函数保存为一个变量,再把该变量传给 ref 可以优化这一问题。

暴露内部 ref 给父组件

在极少数情况下,父组件需要引用子组件中内部的 DOM 元素或 class 组件实例。

对于 React 16.2 之前的版本,可以将 ref 作为特殊名字的 prop 传递给子组件。

React 16.3 版本之后提供了 Refs 转发,使得组件可以像暴露自己为 ref 一样,暴露内部的 DOM 或 class 组件实例为 ref。

Refs 转发

Refs 转发是一个可选特性,它允许某些组件接收 ref,并将其向下传递(转发)给子组件。使用方式如下:

import React, { useRef } from "react";
import type { ForwardedRef } from "react";

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

function CustomButton() {
  const buttonRef = useRef<HTMLButtonElement>(null);
  return <FancyButton ref={buttonRef}>Click</FancyButton>;
}

React.forwardRef 函数的第二个参数 ref 是特有的,常规函数组件和 class 组件不接收 ref 参数,props 中也不存在 ref

如果在组件库中开始使用 React.forwardRef,应该视为破坏性更改,并发布新的主版本;因为这可能会使组件库表现出明显不同的行为,因为 ref 被分配的对象以及导出的类型可能发生变化。

高阶组件(HOC)上的 ref 属性值直接指向包装后的组件上,要想获取被包装的组件,也可以通过 React.forwardRef 来实现。

Refs 转发组件在 React DevTools 中显示的名字由 React.forwardRef 所接收的渲染函数的名字及其 displayName 属性来决定,优先级为:displayName 属性 > name 属性。