写点什么

React Ref 如何使用(译)

用户头像
西贝
关注
发布于: 2020 年 11 月 04 日
React Ref 如何使用(译)

原文

https://www.robinwieruch.de/react-ref


Using React ref and really understanding it are two different pair of shoes. To be honest, I am not sure if I understand everything correctly to this date, because it's not as often used as state or side-effects in React and because its API did change quite often in React's past. In this React Ref tutorial, I want to give you a step by step introduction to refs in React.


用 React ref 和真正理解它是两个概念。说实话,直到今天我也不确定我理解的每一项都是正确的,因为在 React 中,它并不像 state 或者 effect 那样被经常用到,而且它的 API 在 React 的迭代中经常被修改。在本文中,我会一点一点的向你介绍 React 中的 refs



REACT USEREF HOOK: REFS


React refs are strongly associated with the DOM. This has been true in the past, but not anymore since React introduced React Hooks. Ref means just reference, so it can be a reference to anything (DOM node, JavaScript value, ...). So we will take one step back and explore the React ref without the DOM first, before diving into its usages with HTML elements. Let's take the following React component as example:


refs 和 DOM 有十分紧密的关系,这点在过去是公认的,但是自从 React 出现了 React Hooks,这种说法不再完全准确。Ref 仅仅意味着引用,所以它可以是任何东西的引用(DOM 节点,JS 变量,……),所以在深入了解它在 HTML 元素上的应用之前,我们优先介绍不带 DOM 的 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 offers us the React useRef Hook which is the status quo API when using refs in React function components. The useRef Hook returns us a mutable object which stays intact over the lifetime of a React component. Specifically, the returned object has a current property which can hold any modifiable value for us:


React 提供了一个 React useRef Hook,在 React 函数组件中使用 refs 时,它是被用于锁定 state 的 API。这个 useRef Hook 在组件的整个生命周期过程中始终返回一个动态的对象。具体来说,这个返回的对象有一个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>  );}
复制代码


The ref's current property gets initialized with the argument we provide for the useRef hook (here false). Whenever we want, we can reassign the ref's current property to a new value. In the previous example, we are just tracking whether the button has been clicked.


对于这个 useRef Hook 而言,我们提供了参数对它的current 属性进行初始化(这是false)。只要我们想,我们可以随时给这个 ref 的current 属性重新赋值。在上面的示例中,我们仅仅是确认这个按钮是否被点击了


The thing about setting the React ref to a new value is that it doesn't trigger a re-render for the component. While the state updater function (here setCount) in the last example updates the state of the component and makes the component re-render, just toggling the boolean for the ref's current property wouldn't trigger a re-render at all:


对 ref 重新赋值并没有引发组件的重新渲染。在上面的示例中,当 state 更新函数(这里指setCount)更新组件的 state 使得组件重新渲染时,对于 ref 的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;  }   // 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>  );}
复制代码


Okay, we can use React's useRef Hook to create a mutable object which will be there for the entire time of the component's existence. But it doesn't trigger a re-render whenever we change it -- because that's what state is for --, so what's the ref's usage here?


OK,我们现在用 React 的 useRef Hook 创建了一个动态的对象,这个对象在这个组件整个生命周期中始终存在。但是无论我们如何更改它,它都不会触发重新渲染(这是 state 存在的意义),那 refs 在这可以做什么呢?



REACT REF AS INSTANCE VARIABLE

The ref can be used as instance variable for a function component in React whenever we need to track some kind of state without using React's re-render mechanism. For example, we can track whether a component has been rendered for the first time or whether it has been re-rendered:


在 React 的函数组件中,当我们需要追溯一些不需要触发 React 重新渲染机制的状态类型时,ref 可以作为一个实例变量来使用,比如我们可以追踪一个组件是第一次被渲染还是已经被重新渲染过了


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>  );}
复制代码


In this example, we initialize the ref's current property with true, because we assume rightfully that the component starts with its first render when it gets initialized for the first time. However, then we make use of React's useEffect Hook -- which runs without a dependency array as second argument for the first and every additional render -- to update the ref's current property after the first render of the component. Setting the ref's current property to false doesn't trigger a re-render though.


在这个例子中,我们初始化 ref 的current 属性值为 true,因为我们假设当组件首次进行初始化时,组件正确的开启它的首次渲染。接着,在组件完成第一次渲染之后,我们用 React 的 useEffect Hook(运行过程中,没有依赖数组作为第二个参数)更新 ref 的current 属性。虽然设置 ref 的current 属性值为 false,但并没有触发重新渲染


Now we gain the ability to create a useEffect Hook which only runs its logic for every component update, but not for the initial render. It's certainly a feature which every React developer needs at some point but which isn't provided by React's useEffect Hook:


现在,我们扩展功能,创建一个 useEffect Hook,当任意组件更新时,它会执行它的内部逻辑,但是初始化渲染时,它不需要执行。在某些情况下,这是每一个 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>  );}
复制代码


Deploying instance variables with refs for React components isn't widely used and not often needed. However, I have seen developers from my React workshops who surely knew that they needed an instance variable with useRef for their particular case after they have learned about this feature during my classes.


对于 React 组件,用 ref 作为实例变量并不常见,而且也并不经常会用到。但是我发现很多的开发者在我的教程中学习到这个功能之后,他们确信需要这样一个实例变量来实现他们的一些特殊场景


Rule of thumb: Whenever you need to track state in your React component which shouldn't trigger a re-render of your component, you can use React's useRef Hooks to create an instance variable for it.


提示:在 React 组件中,当你需要追踪一个 state,但不想重新渲染这个组件时,你可以选择使用 useRef Hook 来创建一个实例变量



REACT USEREF HOOK: DOM REFS

Let's get to React's ref speciality: the DOM. Most often you will use React's ref whenever you have to interact with your HTML elements. React by nature is declarative, but sometimes you need to read values from your HTML elements, interact with the API of your HTML elements, or even have to write values to your HTML elements. For these rare cases, you have to use React's refs for interacting with the DOM with an imperative and not declarative approach.


我们看一下 React 中 ref 的特长:DOM 操作。通常情况下,当你想要和 HTML 元素建立关联时,你会选择使用 ref。原则上,React 属于声明式,但是有时你需要从 HTML 元素中读取 value,获取 HTML 元素的 API,甚至不得不向 HTML 元素做写操作。面对这些少数情况,你不得不选择使用 ref 用命令式而非声明式的方式和 DOM 建立联系


This React component shows the most popular example for the interplay of a React ref and DOM API usage:


下面这个 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>  );}
复制代码


Like before, we are using React's useRef Hook to create a ref object (1). In this case, we don't assign any initial value to it, because that will be done in the next step (2) where we provide the ref object to the HTML element as ref HTML attribute. React automatically assigns the DOM node of this HTML element to the ref object for us. Finally (3) we can use the DOM node, which is now assigned to the ref's current property, to interact with its API.


像以前一样,我们用 useRef Hook 创建一个 ref 对象(1)。在这个示例中,我们没有对它指定任意初始值,因为之后我们会在下一步(2)的位置把这个 ref 对象作为 HTML 的属性值赋值给这个元素。React 会自动的将这个 HTML 元素的 DOM 节点指向 ref 对象。最后,在(3)的位置,我们可以直接通过 ref 的current 属性调用这个 DOM 节点,并且和它的 API 建立联系


The previous example has shown us how to interact with the DOM API in React. Next, you will learn how to read values from a DOM node with ref. The following example reads the size from our element to show it in our browser as title:


上个示例向我们展示了如何在 React 中和 DOM API 建立联系。接下来,我们会学习如何通过 ref 从 DOM 节点中读取 value。下个示例会从元素中读取 size,并在浏览器的头部进行展示


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>  );}
复制代码


As before, we are initializing the ref object with React's useRef Hook, use it in React's JSX for assigning the ref's current property to the DOM node, and finally read the element's width for the component's first render via React's useEffect Hook. You should be able to see the width of your element as title in your browser's tab.


同样的,我们用 useRef Hook 初始化一个 ref 对象,在 JSX 中将它的current 属性指向 DOM 节点,最后读取组件通过 useEffect Hook 首次渲染元素的宽度。你应该可以在浏览器的 tab 上看到元素的宽度作为标题展示


Reading the DOM node's size happens only for the initial render though. If you would want to read it for every change of the state, because that's after all what will change the size of our HTML element, you can provide the state as dependency variable to React's useEffect Hook. Whenever the state (here text) changes, the new size of the element will be read from the HTML element and written into the document's title property:


对 DOM 节点的 size 做读取操作仅仅在初始渲染时进行。如果你想要在任意状态发生变化时进行读操作,由于 HTML 元素的 size 是在所有操作完成之后变化的,所以你需要依赖 useEffect Hook 来更改 state。这样的话,无论 state(这里指text)什么时候变化,HTML 元素的新 size 都会被重新读取,并且写到 document 的 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>  );}
复制代码


Both examples have used React's useEffect Hook to do something with the ref object though. We can avoid this by using callback refs.


虽然上面两个例子都用到了 useEffect Hook 对 ref 对象做一些处理,但是我们同样可以通过回调 ref 来实现同样的功能,从而避免使用 useEffect Hook



REACT CALLBACK REF

A better approach to the previous examples is using a so called callback ref instead. With a callback ref, you don't have to use useEffect and useRef hooks anymore, because the callback ref gives you access to the DOM node on every render:


上面的示例我们可以通过被称为 callback ref 的方法来更好的实现。有了 callback ref,你不用再使用 useEffect 和 useRef hook,因为 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>  );}
复制代码


A callback ref is nothing else than a function which can be used for the HTML element's ref attribute in JSX. This function has access to the DOM node and is triggered whenever it is used on a HTML element's ref attribute. Essentially it's doing the same as our side-effect from before, but this time the callback ref itself notifies us that it has been attached to the HTML element.


callback ref 就是一个函数,在 JSX 中它可以作为 HTML 元素的 ref 属性。这个函数可以访问 DOM 节点,当它作为 HTML 元素的 ref 属性时,可以随时被触发。实际上它和以前的副作用是一样的,不同的是,callback ref 会自动的通知我们,它已经和 HTML 元素建立了联系


Before when you used the useRef + useEffect combination, you were able to run the your side-effect with the help of the useEffect's hook dependency array for certain times. You can achieve the same with the callback ref by enhancing it with React's useCallback Hook to make it only run for the first render of the component:


在以前,当你把 useRef 和 useEffect 组合使用时,你需要花费一些时间在 useEffect hook 依赖数组的帮助下触发你的副作用。你可以用 callback ref 实现同样的功能,并且进一步用 useCallback Hook 实现这个组件仅仅在第一次渲染时执行


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>  );}
复制代码


You could also be more specific here with the dependency array of the useCallback hook. For example, execute the callback function of the callback ref only if state (here text) has changed and, of course, for the first render of the component:


你也可以用 useCallback hook 的依赖数组实现更多特殊的场景。比如,当 state(这里指text)变化之后,执行 callback ref 的 callback 函数,当然,只限于组件的首次渲染


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>  );}
复制代码


However, then we would end up again with the same behavior like we had before without using React's useCallback Hook and just having the plain callback ref in place -- which gets called for every render.


但是,之后我们会像之前没有用 useCallback Hook 并且只在需要渲染的地方执行回调一样,以同样的方式结束这次执行



REACT REF FOR READ/WRITE OPERATIONS


So far, we have used the DOM ref only for read operations (e.g. reading the size of a DOM node). It's also possible to modify the referenced DOM nodes (write operations). The next example shows us how to apply style with React's ref without managing any extra React state for it:


目前为止,我们仅仅将 DOM ref 用于做读操作(比如,读 DOM 节点的 size)。其实,它也可以被用来修改引用的 DOM 节点(写操作)。下面的示例会展示如何在没有使用任何额外的 state 的情况下用 ref 来添加 style


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>  );}
复制代码


This can be done for any attribute on this referenced DOM node. It's important to note that usually React shouldn't be used this way, because of its declarative nature. Instead you would use React's useState Hook to set a boolean whether you want to color the text red or blue. However, sometimes it can be quite helpful for performance reasons to manipulate the DOM directly while preventing a re-render.


这个操作可以实现在引用的 DOM 节点上添加任何属性。请注意,通常在 React 中不推荐这么做,因为它是声明式的。当你想要更改文本颜色为红色或者蓝色时,你可以选择用 useState Hook 设置一个 boolean 实现。但是有时候,考虑到性能问题,为了避免在直接操作 DOM 时重新渲染,ref 是十分有效的。


Just for the sake of learning about it, we could also manage state this way in a React component:


只是出于学习的目的,在 React 组件中,我们也可以用它来管理 state


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>  );}
复制代码


It's not recommended to go down this rabbit hole though... Essentially it should only show you how it's possible to manipulate any elements in React with React's ref attribute with write operations. However, why do we have React then and don't use vanilla JavaScript anymore? Therefore, React's ref is mostly used for read operations.


虽然不推荐这些操作,但是实际上,这些事例只是为了说明,在 React 中用 ref 属性是可以操作元素进行写操作的。但是,为什么我们不在 React 中用原生 Javascript 呢?所以,React 的 ref 大多数情况下仅用于读操作


This introduction should have shown you how to use React's ref for references to DOM nodes and instance variables by using React's useRef Hooks or callback refs. For the sake of completeness, I want to mention React's createRef() top-level API too, which is the equivalent of useRef() for React class components. There are also other refs called string refs which are deprecated in React.


这篇文章的内容向我们展示如何用 ref 和 DOM 节点建立关联,并且用 useRef Hook 或者 callback ref 初始化实例变量。为了内容的完整性,我还想推荐一下 createRef()这个上层 API,它和 React 类组件中useRef() 是等价的。在 React 中也有其它的 refs,比如 string refs 也是不推荐使用的。


发布于: 2020 年 11 月 04 日阅读数: 115
用户头像

西贝

关注

还未添加个人签名 2019.02.15 加入

还未添加个人简介

评论

发布
暂无评论
React Ref 如何使用(译)