使用React Ref的详细教程(附代码)

514 阅读7分钟

使用React ref和真正理解它是两双不同的鞋。说实话,我至今也不确定自己是否能正确理解一切,因为它在React中并不像状态或副作用那样经常使用,而且它的API在React的过去确实经常变化。在这个React Ref教程中,我想给你一步一步地介绍React中的Refs。

React useRef Hook。Refs

React refs与DOM密切相关。这在过去是真的,但自从React引入React Hooks后就不再是了。Ref的意思只是引用,所以它可以是对任何东西的引用(DOM节点,JavaScript值,...)。因此,我们将退一步,在深入探讨React ref与HTML元素的使用之前,先探讨一下没有DOM的React ref。让我们以下面这个React组件为例。

function Counter() {
  const [count, setCount] = React.useState(0);

  function onClick() {
    const newCount = count + 1;

    setCount(newCount);
  }

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={onClick}>
        Increase
      </button>
    </div>
  );
}

React为我们提供了React useRef Hook,这是在React功能组件中使用ref的现状API。useRef Hook给我们返回一个可变的对象,在React组件的生命周期内保持不变。具体来说,返回的对象有一个current 属性,可以为我们保存任何可修改的值。

function Counter() {
  const hasClickedButton = React.useRef(false);

  const [count, setCount] = React.useState(0);

  function onClick() {
    const newCount = count + 1;

    setCount(newCount);

    hasClickedButton.current = true;
  }

  console.log('Has clicked button? ' + hasClickedButton.current);

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={onClick}>
        Increase
      </button>
    </div>
  );
}

ref的当前属性被初始化为我们为useRef钩子提供的参数(这里是false )。无论何时,我们都可以将Ref的当前属性重新赋值为一个新的值。在前面的例子中,我们只是在追踪按钮是否被点击了。

将React ref设置为一个新的值的问题是,它不会触发组件的重新渲染。虽然上一个例子中的状态更新函数(这里是setCount )更新了组件的状态,并使组件重新渲染,但仅仅是切换引用的当前属性的布尔值,根本不会触发重新渲染。

function Counter() {
  const hasClickedButton = React.useRef(false);

  const [count, setCount] = React.useState(0);

  function onClick() {
    // const newCount = count + 1;

    // setCount(newCount);

    hasClickedButton.current = true;
  }

  // Does only run for the first render.
  // Component does not render again, because no state is set anymore.
  // Only the ref's current property is set, which does not trigger a re-render.
  console.log('Has clicked button? ' + hasClickedButton.current);

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={onClick}>
        Increase
      </button>
    </div>
  );
}

好吧,我们可以使用React的useRef Hook来创建一个可变的对象,这个对象在组件存在的整个期间都会存在。但是每当我们改变它的时候,它不会触发重新渲染--因为这就是状态的作用--所以Ref在这里的用途是什么?

React Ref作为实例变量

当我们需要跟踪某种状态而不使用React的重新渲染机制时,Ref可以作为React中一个函数组件的实例变量。例如,我们可以跟踪一个组件是否被首次渲染或是否被重新渲染。

function ComponentWithRefInstanceVariable() {
  const [count, setCount] = React.useState(0);

  function onClick() {
    setCount(count + 1);
  }

  const isFirstRender = React.useRef(true);

  React.useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
    }
  });

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={onClick}>
        Increase
      </button>

      {/*
        Only works because setCount triggers a re-render.
        Just changing the ref's current value doesn't trigger a re-render.
      */}
      <p>{isFirstRender.current ? 'First render.' : 'Re-render.'}</p>
    </div>
  );
}

在这个例子中,我们将ref的current属性初始化为true,因为我们正确地假设,当组件第一次被初始化时,它就开始了第一次渲染。然而,然后我们利用React的useEffect Hook--它在第一次和每一次额外的渲染时都不需要依赖数组作为第二个参数--来在组件的第一次渲染后更新Ref的当前属性。将Ref的当前属性设置为false并不会触发重新渲染。

现在我们有能力创建一个useEffect Hook,它只在每次组件更新时运行其逻辑,而不是在初始渲染时。这当然是每个React开发者在某些时候都需要的功能,但React的useEffect Hook却没有提供。

function ComponentWithRefInstanceVariable() {
  const [count, setCount] = React.useState(0);

  function onClick() {
    setCount(count + 1);
  }

  const isFirstRender = React.useRef(true);

  React.useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
    } else {
      console.log(
        `
          I am a useEffect hook's logic
          which runs for a component's
          re-render.
        `
      );
    }
  });

  return (
    <div>
      <p>{count}</p>

      <button type="button" onClick={onClick}>
        Increase
      </button>
    </div>
  );
}

为React组件部署带有引用的实例变量并不广泛使用,也不经常需要。然而,在我的React研讨会上,我看到一些开发者在我的课程中了解到这个特性后,他们肯定知道他们需要一个带有useRef的实例变量来满足他们的特殊情况。

经验法则。当你需要跟踪你的React组件中的状态时,如果不应该触发组件的重新渲染,你可以使用React的useRef Hooks为它创建一个实例变量。

React useRef Hook,DOM Refs

让我们来看看React的Ref特长:DOM。大多数情况下,只要你要与你的HTML元素进行交互,你就会使用React的ref。React本质上是声明性的,但有时你需要从你的HTML元素中读取值,与你的HTML元素的API交互,甚至需要向你的HTML元素写值。对于这些罕见的情况,你必须使用React的参考文献,以命令式而非声明式的方式与DOM进行交互。

这个React组件展示了React ref和DOM API使用的相互作用的最流行的例子。

function App() {
  return (
    <ComponentWithDomApi
      label="Label"
      value="Value"
      isFocus
    />
  );
}

function ComponentWithDomApi({ label, value, isFocus }) {
  const ref = React.useRef(); // (1)

  React.useEffect(() => {
    if (isFocus) {
      ref.current.focus(); // (3)
    }
  }, [isFocus]);

  return (
    <label>
      {/* (2) */}
      {label}: <input type="text" value={value} ref={ref} />
    </label>
  );
}

像以前一样,我们使用React的useRef Hook来创建一个ref对象(1)。在这种情况下,我们不给它分配任何初始值,因为这将在下一步完成(2),我们将Ref对象作为ref HTML属性提供给HTML元素。React会自动将这个HTML元素的DOM节点分配给我们的ref对象。最后(3)我们可以使用DOM节点,也就是现在分配给ref的当前属性,来与它的API进行交互。

前面的例子已经向我们展示了如何与React中的DOM API进行交互。接下来,你将学习如何用ref从DOM节点中读取数值。下面的例子从我们的元素中读取大小,在浏览器中显示为标题。

function ComponentWithRefRead() {
  const [text, setText] = React.useState('Some text ...');

  function handleOnChange(event) {
    setText(event.target.value);
  }

  const ref = React.useRef();

  React.useEffect(() => {
    const { width } = ref.current.getBoundingClientRect();

    document.title = `Width:${width}`;
  }, []);

  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

像以前一样,我们用React的useRef Hook初始化ref对象,在React的JSX中使用它,将ref的当前属性分配给DOM节点,最后通过React的useEffect Hook为组件的第一次渲染读取元素的宽度。你应该能在浏览器的标签中看到你的元素的宽度,就像标题一样。

读取DOM节点的大小只发生在初始渲染时。如果你想在每次改变状态时读取它,因为这毕竟会改变我们HTML元素的大小,你可以把状态作为依赖变量提供给React的useEffect Hook。每当状态(这里是text )发生变化时,元素的新尺寸就会从HTML元素中读取,并写入文档的标题属性。

function ComponentWithRefRead() {
  const [text, setText] = React.useState('Some text ...');

  function handleOnChange(event) {
    setText(event.target.value);
  }

  const ref = React.useRef();

  React.useEffect(() => {
    const { width } = ref.current.getBoundingClientRect();

    document.title = `Width:${width}`;
  }, [text]);

  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

这两个例子都使用了React的useEffect Hook来处理ref对象。我们可以通过使用回调 refs 来避免这种情况。

React回调参考

与前面的例子相比,一个更好的方法是使用所谓的回调反射来代替。有了回调反射,你就不必再使用useEffect和useRef钩子了,因为回调反射让你在每次渲染时都能访问DOM节点。

function ComponentWithRefRead() {
  const [text, setText] = React.useState('Some text ...');

  function handleOnChange(event) {
    setText(event.target.value);
  }

  const ref = (node) => {
    if (!node) return;

    const { width } = node.getBoundingClientRect();

    document.title = `Width:${width}`;
  };

  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

回调 ref 无非是一个函数,可以用于JSX中HTML元素的ref属性。这个函数可以访问DOM节点,每当它被用在HTML元素的ref属性上时就会被触发。本质上,它的作用与我们之前的副作用相同,但这次回调的ref本身通知我们它已经被附加到了HTML元素上。

之前,当你使用useRef + useEffect的组合时,你能够在某些时候借助useEffect的钩子依赖数组来运行你的侧效果。你可以通过使用React的useCallback Hook加强回调Ref,使其只在组件的第一次渲染时运行,从而实现同样的效果。

function ComponentWithRefRead() {
  const [text, setText] = React.useState('Some text ...');

  function handleOnChange(event) {
    setText(event.target.value);
  }

  const ref = React.useCallback((node) => {
    if (!node) return;

    const { width } = node.getBoundingClientRect();

    document.title = `Width:${width}`;
  }, []);

  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

你也可以在这里对useCallback钩子的依赖数组进行更具体的处理。例如,只有当状态(这里是text )发生变化时,才执行回调函数,当然也包括组件的第一次渲染。

function ComponentWithRefRead() {
  const [text, setText] = React.useState('Some text ...');

  function handleOnChange(event) {
    setText(event.target.value);
  }

  const ref = React.useCallback((node) => {
    if (!node) return;

    const { width } = node.getBoundingClientRect();

    document.title = `Width:${width}`;
  }, [text]);

  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

然而,这样一来,我们又会像以前那样,不使用React的useCallback钩子,而只是在原地使用普通的回调函数--每次渲染都会被调用。

读/写操作的React Ref

到目前为止,我们只将DOM ref用于读取操作(例如,读取DOM节点的大小)。也可以修改引用的DOM节点*(写操作*)。下一个例子向我们展示了如何用React的ref应用样式,而不需要为它管理任何额外的React状态。

function ComponentWithRefReadWrite() {  const [text, setText] = React.useState('Some text ...');
  function handleOnChange(event) {    setText(event.target.value);  }
  const ref = (node) => {    if (!node) return;
    const { width } = node.getBoundingClientRect();
    if (width >= 150) {      node.style.color = 'red';    } else {      node.style.color = 'blue';    }  };
  return (    <div>      <input type="text" value={text} onChange={handleOnChange} />      <div>        <span ref={ref}>{text}</span>      </div>    </div>  );}

这可以对这个被引用的DOM节点上的任何属性进行操作。值得注意的是,通常React不应该这样使用,因为它具有声明性。相反,你会使用React的useState Hook来设置一个布尔值,以确定你是想把文本染成红色还是蓝色。然而,有时出于性能方面的考虑,在防止重新渲染的同时直接操作DOM会很有帮助。

只是为了学习它,我们也可以在React组件中这样管理状态:

function ComponentWithRefReadWrite() {
  const [text, setText] = React.useState('Some text ...');

  function handleOnChange(event) {
    setText(event.target.value);
  }

  const ref = (node) => {
    if (!node) return;

    const { width } = node.getBoundingClientRect();

    if (width >= 150) {
      node.style.color = 'red';
    } else {
      node.style.color = 'blue';
    }
  };

  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

不过,不建议你进入这个兔子洞......本质上,它应该只向你展示了如何在React中用React的ref属性来操作任何元素的写操作。然而,为什么我们会有React而不再使用vanilla JavaScript呢?因此,React的ref主要用于读操作。


这篇介绍应该已经向你展示了如何通过使用React的useRef Hooks或callback refs将React的ref用于对DOM节点和实例变量的引用。为了完整起见,我也想提到React的createRef() 顶层API,它相当于React类组件的useRef()。还有其他的Refs,叫做字符串Refs,在React中已经废弃了。