掌握 React Ref:简洁而实用的组件和 DOM 元素操作技巧

简介

本文从以下几个方面介绍React组件的refs属性

  1. 什么是refs
  2. 类组件中使用refs的几种方式
  3. 函数式组件中使用refs的方式
  4. 总结refs

什么是refs

在React中,ref 是一种用于访问组件的底层DOM节点或React元素的方式。它允许你直接访问和操作组件内部的DOM元素,或者引用React组件的实例。ref 的主要用途包括:

  1. 访问DOM元素 :你可以使用 ref 来获取DOM元素的引用,以便在需要时直接操作它们,例如改变样式、聚焦、滚动等。
  2. 访问React组件的实例 :你可以使用 ref 来获取对React组件的实例的引用,以便在需要时直接调用组件的方法或访问其属性。
  3. 触发组件生命周期方法:通过获取组件的实例,你可以手动调用组件的生命周期方法,以模拟组件的生命周期事件。

类组件中使用ref

在类组件中有字符串形式的 refReact.createRef()回调函数形式的 ref 三种方式创建refs,我们推荐使用后两种方式。

字符串形式的 ref

这是较旧的一种方式,已经被官方淘汰了。不想了解的小伙伴可以直接跳到下一个。

它允许你为组件的 ref 属性分配一个字符串,然后你可以通过 this.refs 来访问它。

jsx 复制代码
class MyComponent extends React.Component {
  componentDidMount() {
    console.log(this.refs.myRef1)
    console.log(this.refs.myRef2)
  }
  render() {
    return (
      <div>
        <input ref="myRef1" />
        <input ref="myRef2" />
      </div>
    );
  }
}

React.createRef()

React.createRef()是 React 推荐的用于创建一个 ref 对象的方法。React.createRef() 返回的是一个 ref 对象,该对象可以被赋值给 React 元素的 ref 属性,从而允许你访问该元素或组件实例。使用方法如下:

1. 创建 Ref 对象

使用 React.createRef() 创建一个 ref 对象,通常在类组件的构造函数中进行初始化。例如:

jsx 复制代码
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }

  // ...
}

2. 将 Ref 与 React 元素关联

在 JSX 中,将创建的 ref 对象赋值给 React 元素的 ref 属性,以建立关联。这通常在组件的 render 方法中完成。例如:

jsx 复制代码
render() {
  return <div ref={this.myRef}>Hello, World!</div>;
}

在上面的例子中,this.myRef绑定了 <div> 元素。

3. 访问 Ref 中的当前值

通过 this.myRef.current 属性,我们可以访问与 ref 关联的元素或组件实例。例如:

jsx 复制代码
componentDidMount() {
  console.log(this.myRef.current); // 访问关联的元素或组件实例
}

这是在组件挂载后访问 ref 中的当前值的常见方式。

用途

React.createRef() 主要用于访问React元素的底层DOM节点或类组件的实例。你可以在需要时操作这些元素或实例,例如修改样式、获取或修改输入值、执行原生DOM操作,或调用组件的方法。

注意事项

  • Refs是可变的,因此你需要小心在访问它们之前检查它们是否存在。
  • 不要在函数组件中使用 React.createRef(),因为它通常与类组件的生命周期方法结合使用。在函数组件中,通常使用 useRef 钩子来创建 ref

创建创建多个ref,多次调用React.createRef(),会返回不同的引用,如下例子

jsx 复制代码
 constructor(props) {
    super(props);

    // 创建多个 ref 对象
    this.inputRef = React.createRef();
    this.divRef = React.createRef();
    this.customComponentRef = React.createRef();
  }

原因: 多次调用 React.createRef() 会生成不同的 ref 对象,每个对象都有其自己的 current 属性,用于引用与其关联的元素或实例。这样可以轻松管理多个不同的引用于不同的DOM元素或React组件实例。

总之,React.createRef() 是一个用于在React类组件中创建和管理引用的工具,允许你访问和操作React元素或组件实例。

回调函数形式的 ref

回调函数形式的 ref是将 ref 属性设置为一个回调函数 。这个回调函数接收一个参数 node,代表渲染的 input 元素。

jsx 复制代码
class MyComponent extends React.Component {
  componentDidMount() {
    // 在组件挂载后,将按钮元素的文本内容修改为"点击我"。
    this.myButton.textContent = "点击我";
  }
  render() {
    return (
      <div>
        <button
          ref={(node) => {
            this.myButton = node;
          }}
        >
          初始文本
        </button>
      </div>
    );
  }
}

函数式组件的ref

在 React 16.3 之前的版本中,函数式组件确实无法直接接收 ref,因为它们没有实例。然而,自 React 16.3 版本开始,React 引入了 forwardRefuseRef等函数,使得函数式组件也可以接收 ref

useRef

在 React 的函数式组件中,主要使用 useRef 这个 Hook 来创建一个可持久化的refuseRef 返回一个可变的 ref 对象,并且在组件的多次渲染之间保持不变。它主要用于获取或修改 DOM 节点、保存组件内部变量等场景。

useRef 的用法和特点如下:

  1. 创建和访问 ref:通过调用 useRef(initialValue) 来创建一个 ref 对象,并传入初始值。例如:const myRef = useRef(initialValue);

    • 创建的 ref 对象可以在组件的整个生命周期中持久存在,可以在组件的多个渲染中获取到相同的 ref 对象。
    • 可以通过 myRef.current 来访问或修改 ref 的当前值。
  2. 获取和操作 DOM 节点:将 useRef 与 JSX 中的 ref 属性结合使用,可以获取组件或 DOM 元素的引用。

    • 在函数组件中,通过将 ref 属性赋值为 useRef 创建的 ref 对象,就可以在 current 属性中获取到对应的 DOM 节点。
    • 例如:<div ref={myRef}>...</div>,然后可以通过 myRef.current 来访问该 <div> 元素。
  3. 保存组件内部变量:由于 ref 对象在组件的多次渲染之间保持不变,可以将一些需要在组件生命周期中保持持久化的变量保存在 ref 中。

    • 对于函数组件而言,由于每次渲染都会重新执行函数体,无法直接使用普通变量来保存值。而使用 useRef 创建的 ref 对象就可以用来保存这些值。
    • 通过修改 ref.current 的值,可以在组件的多次渲染之间保持变量的持久性。

注意事项:

  • useRef 返回的 ref 对象在组件重新渲染时不会触发更新,因此对其修改并不会引发组件的重新渲染。如果需要触发组件重新渲染,可以结合其他 Hook(如 useState)来实现。

为了便于理解,这里给出一个完整例子

jsx 复制代码
import React, { useRef } from 'react';

function ExampleComponent() {
  const inputRef = useRef(null);
  const countRef = useRef(0);

  const handleClick = () => {
    // 获取输入框的值
    console.log(inputRef.current.value);

    // 保存和修改组件内部变量
    countRef.current += 1;
    console.log(countRef.current);
  };

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleClick}>点击</button>
    </div>
  );
}

export default ExampleComponent;

在上述示例中,我们创建了两个 useRef 对象 inputRefcountRef

  • inputRef 用于获取输入框的值。我们将其赋值给 <input> 元素的 ref 属性,这样就可以通过 inputRef.current.value 获取到输入框当前的值。
  • countRef 用于保存和修改组件内部变量。每次点击按钮时,我们通过 countRef.current 将计数器加一,并打印出来。

这样,在组件重新渲染时,inputRefcountRef 的引用保持不变,可以在多次渲染之间共享和保留数据。

简而言之,useRef 是 React 中一个非常有用的 Hook,它能够创建一个持久存在的引用,并可用于获取和修改 DOM 节点、保存组件内部变量等场景。

forwardRef

通过使用 forwardRef 函数包装函数式组件,可以将 ref 作为参数传递给函数式组件,并在函数式组件内部通过 ref 参数来获取 ref 对象。

以下是一个示例,演示如何在 React 函数式组件中使用 forwardRef 来传递 ref

jsx 复制代码
import React, { forwardRef } from 'react';

const TextInput = forwardRef((props, ref) => {
  const handleChange = (event) => {
    if (props.onChange) {
      props.onChange(event.target.value);
    }
  };

  return (
    <input type="text" ref={ref} onChange={handleChange} />
  );
});

export default TextInput;

下面是在父组件中使用 TextInput 组件的例子:

jsx 复制代码
import React, { useRef } from 'react';
import TextInput from './TextInput';

function ExampleComponent() {
  const inputRef = useRef(null);

  const handleClick = () => {
    console.log(inputRef.current.value);
  };

  return (
    <div>
      <TextInput ref={inputRef} onChange={(value) => console.log(value)} />
      <button onClick={handleClick}>点击</button>
    </div>
  );
}

export default ExampleComponent;
  • 注意事项:

    • forwardRef 只能转发单个 ref。也就是说,如果子组件内部有多个需要访问的元素或组件,只能选择其中一个进行转发。
    • forwardRef 必须接收第二个参数 ref,并将其传递给实际的子组件。这样才能确保父组件通过 ref 属性传递的引用能够正确传递给子组件内部。
    • 子组件接收到的 ref 参数不是常规的 props 参数。在处理 ref 参数时,应该遵循 forwardRef 函数提供的格式。
    • 当使用 forwardRef 转发 ref 时,父组件传递给子组件的 ref 属性将不再生效。也就是说,在父组件中通过 ref 访问子组件的属性和方法时,应该直接使用传递给子组件的 ref
    • 在函数组件中使用 forwardRef 时,需要使用 forwardRef 函数包装整个函数组件。如果直接导出的是函数组件本身,则无法使用 ref
  • 缺点

    通常情况下,我们可以通过 forwardRef API 将子组件内部的 DOM 元素或组件实例转发给父组件,从而实现在父组件中操作子组件。但是这种方式有一些缺点

    • 转发的 ref 只能是单个元素或组件,而不能是多个;
    • 转发后父组件需要了解转发的元素或组件的具体实现细节等问题。
    • ......

使用useImperativeHandle可以克服这些缺点。

useImperativeHandle

useImperativeHandle 是一个 React Hook,用于调整由子组件向父组件暴露的 ref 对象。可以使用该 Hook 来自定义子组件实例或 DOM 元素暴露给父组件的属性和方法。它可以在子组件中控制哪些属性和方法可以被父组件访问。

通过 useImperativeHandle Hook 返回一个对象,该对象将作为子组件向外暴露的属性和方法。这样就可以控制子组件暴露给父组件的属性和方法,并隐藏一些不必要的实现细节。

下面是一个 useImperativeHandle 的例子:

jsx 复制代码
import React, { useRef, forwardRef, useImperativeHandle } from 'react';

const childInput = forwardRef((props, ref) => {
  const inputRef1 = useRef(null);
  const inputRef2 = useRef(null);

  useImperativeHandle(ref, () => {
    return {
      focusInput1: () => {
        console.log('Focusing Input1...');
        inputRef1.current.focus();
      },
      focusInput2: () => {
        console.log('Focusing Input2...');
        inputRef2.current.focus();
      },
      getValue1: () => {
        return inputRef1.current.value;
      },
      getValue2: () => {
        return inputRef2.current.value;
      }
    };
  });

  return (
    <div>
      <input type="text" ref={inputRef1} />
      <input type="text" ref={inputRef2} />
    </div>
  );
});

export default childInput;

在上述代码中,我们创建了两个分别对应两个 <input> 元素的 ref,即 inputRef1inputRef2。然后在 useImperativeHandle 中通过返回一个对象,可以将这两个 ref 都转发给父组件。

在父组件中使用转发后的 ref 来访问子组件的属性和方法,可以按照如下方式进行:

jsx 复制代码
import React, { useRef } from 'react';
import childInput from './childInput';

function ParentComponent() {
  const fancyInputRef = useRef(null);

  const handleClick = () => {
    console.log(fancyInputRef.current.getValue1());
    console.log(fancyInputRef.current.getValue2());
    fancyInputRef.current.focusInput1();
  };

  return (
    <div>
      <childInput ref={fancyInputRef} />
      <button onClick={handleClick}>点击</button>
    </div>
  );
}

export default ParentComponent;
  • 注意事项

    • 尽量避免使用 ref 来访问组件内部的状态或属性,因为这样会破坏封装性。如果需要暴露一些方法或属性可以通过 useImperativeHandle 实现。
    • useImperativeHandle 并不是必需的,只有当需要向外部暴露某些特定的方法和属性时才需要使用。
    • useImperativeHandle 中返回的对象应该只包含那些必须暴露的方法和属性,尽量不要暴露不必要的方法和属性,这样可以保持接口的简单性和清晰性
    • 在使用 useImperativeHandle 时,父组件必须使用 ref 属性引用子组件,否则无法访问子组件暴露的属性和方法。
    • 子组件暴露的属性和方法应该遵循常规函数和组件操作习惯,如:命名规范、参数传递等。 不要滥用 useImperativeHandle,因为 useImperativeHandle 可能会产生一些性能问题,它会使得父组件的渲染函数在子组件变化时被调用。
    • useImperativeHandle 中返回的对象应该是不可变的,避免在返回对象中添加或删除属性或方法。
    • useImperativeHandle 需要放在组件内部使用,而不能在顶层作用域调用。

总结

在 React 中,ref 是一个强大的特性,提供了三种常用的方式来获取组件实例或 DOM 元素。通过字符串 ref(已经被淘汰,不推荐使用)、回调函数 ref 和创建 ref 对象,我们可以轻松地操作组件中的数据和 DOM 元素,实现更灵活的交互和操作。无论是获取组件实例、控制焦点、执行动画还是其他操作,ref 都是不可或缺的利器。因此,在开发 React 应用时,充分利用 ref 这一特性,提升代码的可读性和可维护性,为用户带来更好的体验。

相关推荐
高山我梦口香糖5 分钟前
[electron]预脚本不显示内联script
前端·javascript·electron
神探小白牙6 分钟前
vue-video-player视频保活成功确无法推送问题
前端·vue.js·音视频
Angel_girl3191 小时前
vue项目使用svg图标
前端·vue.js
難釋懷1 小时前
vue 项目中常用的 2 个 Ajax 库
前端·vue.js·ajax
Qian Xiaoo1 小时前
Ajax入门
前端·ajax·okhttp
爱生活的苏苏1 小时前
vue生成二维码图片+文字说明
前端·vue.js
拉不动的猪1 小时前
安卓和ios小程序开发中的兼容性问题举例
前端·javascript·面试
炫彩@之星1 小时前
Chrome书签的导出与导入:步骤图
前端·chrome
贩卖纯净水.2 小时前
浏览器兼容-polyfill-本地服务-优化
开发语言·前端·javascript
前端百草阁2 小时前
从npm库 Vue 组件到独立SDK:打包与 CDN 引入的最佳实践
前端·vue.js·npm