How to use React Ref【译】

267 阅读6分钟

原文链接www.robinwieruch.de/react-ref

使用 React ref 并真正理解它可以使用在两种不同的场景。 老实说,我不确定到目前为止我是否正确理解了所有内容,因为它不像 React 中的状态或副作用那样经常使用,而且它的 API 在 React 过去确实经常变化。 在这个 React Ref 教程中,我想给你一步一步地介绍 React 中的 refs。

REACT USEREF HOOK: REFS

React refs 与 DOM 密切相关。 过去确实如此,但自从 React 引入 React Hooks 后就不再如此。 Ref 意味着只是引用,所以它可以是对任何东西的引用(DOM 节点、JavaScript 值,...)。 因此,在深入研究它与 HTML 元素的用法之前,我们将退后一步并首先探索没有 DOM 的 React ref。

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

我们以下面的 React 组件为例:

function Counter() {
  const hasClickedButton = React.useRef(0);
  let otherCount = 0;
  const [count, setCount] = React.useState(0);
 
  function onClick() {
    const newCount = count + 1;
    otherCount = otherCount + 1;
    setCount(newCount);
 
    hasClickedButton.current = hasClickedButton.current + 1;
  }
 
  console.log('clicked button? ' + hasClickedButton.current + 'tiems');
  console.log(otherCount);
  return (
    <div>
      <p>{count}</p>
 
      <button type="button" onClick={onClick}>
        Increase
      </button>
    </div>
  );
}

ref 的 current 属性使用我们为 useRef 钩子提供的参数进行初始化(此处为 0)。 无论何时,我们都可以将 ref 的当前属性重新分配给一个新值。 在前面的例子中,我们只是跟踪按钮被点击了几次。

将 React ref 设置为新值的事情是它不会触发组件的重新渲染(和其中声明的变量每次都是 0)。 虽然上一个示例中的状态更新器函数(此处为 setCount)更新组件的状态并使组件重新渲染,但仅切换 ref 的当前属性的布尔值根本不会触发重新渲染。

好的,我们可以使用 React 的 useRef Hook 来创建一个可变对象,该对象将在组件存在的整个时间内都存在。 但是每当我们更改它时,它都不会触发重新渲染——因为这就是状态的用途——那么这里的 ref 用法是什么?

REACT REF AS INSTANCE VARIABLE

当我们需要在不使用 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>
  );
}

使用 refs 为 React 组件部署实例变量并没有被广泛使用,也不是经常需要的。

经验法则:每当您需要跟踪不应触发组件重新渲染的 React 组件中的状态时,您可以使用 React 的 useRef Hooks 为其创建实例变量

REACT USEREF HOOK: DOM REFS

让我们来看看 React 的 ref 专长:DOM。 大多数情况下,当您必须与 HTML 元素进行交互时,您将使用 React 的 ref。 React 本质上是声明性的,但有时您需要从 HTML 元素读取值,与 HTML 元素的 API 交互,甚至必须将值写入 HTML 元素。 对于这些少数的情况,您必须使用 React 的 refs 以命令式而非声明式的方式与 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 对象。 在这种情况下,我们不为其分配任何初始值,因为这将在下一步中完成,我们将 ref 对象作为 ref HTML 属性提供给 HTML 元素。 React 会自动为我们将这个 HTML 元素的 DOM 节点分配给 ref 对象。 最后我们可以使用 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。 每当状态(此处为文本)更改时,元素的新大小将从 HTML 元素中读取并写入文档的 title 属性:

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 对象。 我们可以通过使用CALLBACK REF来避免这种情况。

REACT CALLBACK REF

对前面示例的更好方法是使用所谓的CALLBACK REF。 使用CALLBACK REF,您不必再使用 useEffect 和 useRef Hooks,因为CALLBACK REF使您可以在每次渲染时访问 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 元素(并没有const ref = React.useRef();)。

在使用 useRef + useEffect 组合之前,您可以在特定时间借助 useEffect 的钩子依赖数组运行您的副作用。 您可以通过使用 React 的 useCallback Hook 增强回调 ref 以使其仅在组件的第一次渲染时运行,从而实现与回调 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 挂钩的依赖项数组更具体。 例如,仅当状态(此处为文本)发生变化时才执行回调 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}`;
  }, [text]);
 
  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

REACT REF FOR READ/WRITE OPERATIONS

到目前为止,我们仅将 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 ComponentWithImperativeRefState() {
  const ref = React.useRef();
 
  React.useEffect(() => {
    ref.current.textContent = 0;
  }, []);
 
  function handleClick() {
    ref.current.textContent = Number(ref.current.textContent) + 1;
  }
 
  return (
    <div>
      <div>
        <span ref={ref} />
      </div>
 
      <button type="button" onClick={handleClick}>
        Increase
      </button>
    </div>
  );
}

虽然不建议深入这个兔子洞......本质上它应该只向您展示如何使用 React 的 ref 属性和写操作来操作 React 中的任何元素。 然而,为什么我们有 React 而不再使用 vanilla JavaScript 呢? 因为React 的 ref 多用于读操作。

这个介绍应该已经向你展示了如何使用 React 的 ref 来引用 DOM 节点和实例变量,通过使用 React 的 useRef Hooks 或回调 refs。