React ref使用指北

93 阅读5分钟

前言

对于初学react的同学,不知道ref会不会让你感觉头大,下面我分别从ref的概念、class组件、函数式组件,以及ref在项目中使用的场景来对ref进行介绍;希望对大家有帮助!

ref是什么?

react早期版本中ref是这样定义的(react.yubolun.com/docs/refs-a…

refs提供了一种方式,用于访问在render方法中创建的DOM节点或React元素

ref的创建与使用

class组件中提供了三种创建ref的方式

旧版API: String Refs (已过时)

class App extends React.Component {
    constructor(props) {
        super(props);
    }

    componentDidMount() {
        setTimeout(() => {
            // 2. 通过 this.refs.xxx 获取 DOM 节点
            this.refs.textInput.value = "new value";
        }, 2000);
            console.log(this.refs.textInput);
    }

    render() {
        // 1. ref 直接传入一个字符串
        return (<input ref="textInput" value="value" />);
    }
}

打印结果:获取的是input元素 image.png String类型的refs存在问题,它已过时并可能会在未来的版本被移除。如果你目前还在使用this.refs.xxx这种方式访问refs,建议用其他两种方式替代。

回调refs

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    
    componentDidMount() {
        setTimeout(() => {
        // 2. 通过实例属性获取 DOM 节点
        this.textInput.value = "new value";
        }, 2000);
        console.log(" this.textInput", this.textInput);
    }

    render() {
        // 1. ref 传入一个回调函数
        // 该函数中接受 React 组件实例或 DOM 元素作为参数
        // 我们通常会将其存储到具体的实例属性(this.textInput)
        return (
             <input
                ref={(element) => {
                    this.textInput = element;
                }}
                value="value"
              />
        );
     }
}

createRef

class App extends React.Component {

    constructor(props) {
        super(props);
        // 1. 使用 createRef 创建 Refs
        // 并将 Refs 分配给实例属性 textInputRef,以便在整个组件中引用
        this.textInputRef = React.createRef();
    }

    componentDidMount() {
        setTimeout(() => {
            // 3. 通过 Refs 的 current 属性进行引用
            this.textInputRef.current.value = "new value";
        }, 2000);
        console.log("this.textInputRef", this.textInputRef);
    }
    
    render() {
        // 2. 通过 ref 属性附加到 React 元素
        return (<input ref={this.textInputRef} value="value" />);
    }
}

打印结果:

image.png

ref的值取决于节点的类型:

  • 当 ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为它的 current 属性以创建 ref 。
  • 当 ref 属性被用于一个自定义类组件时,ref 对象将接收该组件已挂载的实例作为它的 current 。
  • 你不能在函数式组件上使用 ref 属性,因为它们没有实例。

⭐️⭐️ useRef

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        开始
      </button>
      <button onClick={handleStop}>
        停止
      </button>
    </>
  );
}

该例子中ref存储着interval ID,用来记录你需要取消现有的 interval

我们可以看出ref不仅可以存储DOM元素( DOM接收ref)、实例(类组件接收ref)还可以存储数据,其实我们可以用ref存储任何数据更多使用技巧可以查看 zh-hans.react.dev/learn/refer…

forwardRef的作用

ref使用在函数组件上

以上四种创建ref的方式,都不可以直接作用到函数组件上,如果想获取到函数组件的ref我们可以使用forwardRef。 例如:

import { useRef } from 'react';

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

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

此时控制台输出

image.png 发生这种情况是因为默认情况下,React不允许组件访问其他组件的DOM节点。想要暴露其DOM节点组件必须指定将它的ref“转发”给一个子组件。

import { forwardRef, useRef } from 'react';

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

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

如果你使用 React 16.3 或更高, 这种情况下我们推荐使用 ref 转发。 Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档 中有一个详细的例子。

如果你使用 React 16.2 或更低,或者你需要比 ref 转发更高的灵活性,你可以使用 这个替代方案 将 ref 作为特殊名字的 prop 直接传递。

gist.github.com/gaearon/1a0…

useImperativeHandle

使用命令句柄暴露一部分API 在上面的例子中,MyInput暴露了原始的DOM元素input。这让父组件可以对其调用focus()方法。然而,这也让其父组件能够做其他事情---例如,改变css样式。在一些不常见的情况下,你可能希望限制暴露的功能。你可以使useImperativeHandle

import {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    // 只暴露 focus,没有别的
    focus() {
      realInputRef.current.focus();
    },
  }));
  
  return <input {...props} ref={realInputRef} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

ref在高阶组件中的使用

import React from "react";

function HocWithRef(WrappedComponent) {
    return React.forwardRef((props, ref) => {
        // 方式一:可以将ref当作普通的props传递
        return <WrappedComponent {...props} ref={props.testRef} />;
        // 方式二:用forwardRef第二个参数正常接收ref
        // return <WrappedComponent {...props} ref={ref} />;
    });
}

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
         age: 18
       };
    }
    
    render() {
        return <div>这是一个组件</div>;
   }
}

const MyComponentWithRef = HocWithRef(MyComponent);

class App extends React.Component {
    constructor(props) {
      super(props);
      this.myRef = React.createRef();
    }
    
    componentDidMount() {
       // 通过ref访问或操作MyComponent组件
       console.log(this.myRef.current)
    }

    render() {
        // 方式一:和上面获取方式一的对应 将ref当作props传递
        return <MyComponentWithRef testRef={this.myRef} />;
        
        // 方式二:和上面获取方式二的对应 直接传递ref通过forwardRef接收ref
        //return <MyComponentWithRef ref={this.myRef} />;
    }
}
export default App;

image.png

补充

wrappedComponentRef

wrappedComponentRef估计只会在维护react老版本项目中才会碰到,使用场景在antd v3 Form使用中3x.ant.design/components/… 详细内容可查看文档

image.png

总结

react全面拥抱hook的时代,大家尽量用函数组件useRef的方式来创建ref,其他几种方式也大致了解,毕竟在维护项目的时候,可能会遇到;我理解ref就像一个存储库一样,我们可以用它存储我们想要的数据,并且它还可以在组件中穿梭,让我们可以轻松的在父组件中使用子组件中的方法(实例化)等数据,可以让我们获取到DOM,操作DOM(react并不推荐我们直接操作DOM,但一些时候它又是需要的)想要更加透彻的了解ref再次推荐zh-hans.react.dev/reference/r… 相关章节。 (有问题欢迎指正,一切以官方文档为准哈~)