灏天阁

「React 技巧」:Refs

· Yin灏

摘要

Refs 是 React 提供的一种在典型数据流之外修改子组件的方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。Ref 可以直接操作 DOM 元素,这些操作往往脱离 React 的控制,容易引发 bug。我们需要了解 Ref 的正确使用规范,避免在项目中因为 Ref 造成意想不到的问题。

阅读本文,你可以了解到 Ref 工作原理,React 官方推荐的 Ref 使用方式:forwardRef 和 useImperativeHandle。

介绍

React 典型数据流是从上到下,props 是父子组件交互的唯一方式。要修改一个组件,需要使用新的 props 渲染它。但在某些情况下(如管理焦点,文本选择,媒体播放,触发强制动画等)需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。Refs 直接操作 DOM API,调用 element.focus(),element.remove() ,不受 React 的控制。React 官方将 Ref 称作 escape hatch(逃生窗口),提醒开发这勿过度使用 Refs,而 React 希望尽可能的控制 Refs,对 Refs 使用提出规范。React 推荐两种使用 Refs 的方法:

React.createRef

在 Class Compoent 中使用。我们在 constructor 创建了一个 refEle 的对象,在 render 时将 div#classRef 元素的 ref 属性设为 refEle,通过 refEle.current 访问这个 DOM 实例。

import React from "react";

class ClassRefCompt extends React.Component {
  constructor() {
    super();
    this.refEle = React.createRef();
  }
  componentDidMount() {
    console.log("refs:", this.refEle.current);
  }

  render() {
    return (
      <div>
        <div id="classRef" ref={this.refEle}>
          Hello Ref - createRef().
        </div>
      </div>
    );
  }
}

export default ClassRefCompt;

image.png

useRef()

在 Function Component 中使用,我们直接创建了一个 refEle 的对象,在 render 时将 div#functionRef 元素的 ref 属性设为 refEle,输出 refEle。

import React, { useRef } from "react";

const FunctionRefCompt = () => {
  const refEle = useRef();
  console.log("function ref:", refEle);

  return (
    <div>
      <div id="functionRef" ref={refEle}>
        Hello Ref - useRef().
      </div>
    </div>
  );
};

export default FunctionRefCompt;

image.png

控制台的输出告诉我们,第一次 ref.current 是 null,第二次拿到了 DOM 实例。调用 createRef/useRef ,会先创建创建一个对象,初始化对象的 current 为 null,在组件渲染后,React 会把 DOM 节点挂载到对象的 current。在 useEffect 中访问避免 ref.current 不存在。

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

const FunctionRefCompt = () => {
  const refEle = useRef();
  console.log("function ref:", refEle);

  useEffect(() => {
    console.log("in useEffect ref:", refEle);
  }, []);

  return (
    <div>
      <div id="functionRef" ref={refEle}>
        Hello Ref - useRef().
      </div>
    </div>
  );
};

export default FunctionRefCompt;

image.png

不正确使用 Refs 会引发 bug。正如 React 文档里提到的例子,单击按钮 1 将插入/删除 P 节点,单击按钮 2 将调用 DOM API 以删除 P 节点。

import React, { useState, useRef } from "react";

export default function ErrorRef() {
  const [show, setShow] = useState(true);
  const ref = useRef(null);

  return (
    <div>
      <button
        onClick={() => {
          setShow(!show);
        }}
      >
        Toggle with setState
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}
      >
        Remove from the DOM
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}

按钮 1 移除了 p 元素,按钮 2 通过 ref 来删除 p 元素。如果这两种删除 p 元素的方式混合使用,那么点击按钮 1 再点击按钮 2 会报错。

image.png

方法

在 React 中,组件可以分为低阶组件和高阶组件。低阶组件基于 DOM 元素,可以向 DOM 元素传递 Ref,如下面的 MyInput 组件。

const MyInput = (props) => {
  const inputRef = useRef(null);
  return <input ref={inputRef} {...props} />;
};

高阶组件基于低阶组件封装。高阶组件不能直接向 DOM 元素传递 Ref,例如下面的 MyForm 组件,基于 MyInput 组件包装器,我们无法做到单击 MyForm 组件中的按钮来操作输入焦点。

import React, { useRef } from "react";

const MyInput = (props) => {
  return <input {...props} />;
};

const MyForm = () => {
  const inputRef = useRef(null);
  function handleClick() {
    inputRef.current.focus();
  }
  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>input聚焦</button>
    </>
  );
};

export default MyForm;

image.png

React 为了尽可能控制 Refs,默认不支持跨组件传递 Refs。

forwardRef

我们可以使用 forwardRef API 显式的传递 Refs 来取消限制。在示例中,使用 forwardRef 包裹 MyInput,显式的将 Ref 传递到 DOM 元素中。对于 React 来说,开发者使用 forwardRef 说明他知道自己在做什么,并应该自己承担相应的风险。 同时,forwardRef 的存在更容易开发者定位 Ref 相关的错误。

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

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

const MyForm = () => {
  const inputRef = useRef(null);
  function handleClick() {
    inputRef.current.focus();
  }
  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>input聚焦</button>
    </>
  );
};

export default MyForm;

useImperativeHandle

除了限制跨组件 Refs,React 还提供了 useImperativeHandle API,通过限制 Refs 可使用的方法减少错误。useImperativeHandle 应与 forwardRef 一起使用:

const MyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));
  return <input {...props} ref={inputRef} />;
});

MyForm 组件通过 input.current 会以下数据结构,调用其他 DOM API 则会报错。

{
  focus: () => {
    realInputRef.current.focus();
  },
}

image.png

结论

Refs 是在 React 典型数据流之外,获取子组件实例的方式。Refs 是 React 的逃生窗口,不受其控制,随意使用 Refs 会造成意想不到的问题。React 提供了 forwardRef API 用于解除 Refs 跨组件传递的限制,useImperativeHandle 限制父组件可调用的 Refs 方法。正确使用 Ref 的方式:通过 forwardRef 关闭 React 限制,useImperativeHandle 限制调用方法,保证程序的健壮性。

代码地址:codesandbox.io/s/winter-cd…

参考

[1]  zh-hans.reactjs.org/docs/react-…

[2​]  zh-hans.reactjs.org/docs/hooks-…

[3]  github.com/facebook/re…

[4]  javascript.plainenglish.io/new-react-d…

[5]  zh-hans.reactjs.org/docs/react-…

[6]  zh-hans.reactjs.org/docs/hooks-…

- Book Lists -