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 . 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 (
{count}
);
}
React offers us the React useRef Hook which is the status quo API when using refs in . The useRef Hook returns us a mutable object which stays intact over the lifetime of a React component. Specifically, the returned object has a property which can hold any modifiable value for us:
React 提供了一个 React useRef Hook,在React 函数组件中使用refs时,它是被用于锁定state的API。这个useRef Hook在组件的整个生命周期过程中始终返回一个动态的对象。具体来说,这个返回的对象有一个 属性,它包含了所有变化的值
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 (
{count}
);
}
The ref's current property gets initialized with the argument we provide for the useRef hook (here ). 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而言,我们提供了参数对它的 属性进行初始化(这是)。只要我们想,我们可以随时给这个ref的 属性重新赋值。在上面的示例中,我们仅仅是确认这个按钮是否被点击了
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 ) 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更新函数(这里指)更新组件的state使得组件重新渲染时,对于ref的 属性而言,只是切换了开关并没有触发重新渲染
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 (
{count}
);
}
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 (
{count}
{/*
Only works because setCount triggers a re-render.
Just changing the ref's current value doesn't trigger a re-render.
*/}
{isFirstRender.current ? 'First render.' : 'Re-render.'}
);
}
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 -- 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的 属性值为 ,因为我们假设当组件首次进行初始化时,组件正确的开启它的首次渲染。接着,在组件完成第一次渲染之后,我们用React的useEffect Hook(运行过程中,没有依赖数组作为第二个参数)更新ref的 属性。虽然设置ref的 属性值为 ,但并没有触发重新渲染
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 (
{count}
);
}
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 (
);
}
function ComponentWithDomApi({ label, value, isFocus }) {
const ref = React.useRef(); // (1)
React.useEffect(() => {
if (isFocus) {
ref.current.focus(); // (3)
}
}, [isFocus]);
return (
);
}
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的 属性调用这个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 (
{text}
);
}
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中将它的 属性指向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 ) 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(这里指)什么时候变化,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 (
{text}
);
}
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 (
{text}
);
}
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 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 (
{text}
);
}
You could also be more specific here with the dependency array of the useCallback hook. For example, execute the of the callback ref only if state (here ) has changed and, of course, for the first render of the component:
你也可以用useCallback hook的依赖数组实现更多特殊的场景。比如,当state(这里指)变化之后,执行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 (
{text}
);
}
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
目前为止,我们仅仅将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 (
{text}
);
}
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 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 (
);
}
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, ? 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 top-level API too, which is the . There are also other refs called string refs which are deprecated in React.
这篇文章的内容向我们展示如何用ref和DOM节点建立关联,并且用useRef Hook 或者 callback ref 初始化实例变量。为了内容的完整性,我还想推荐一下 这个上层API,它和React 类组件中 是等价的。在React中也有其它的refs,比如 string refs 也是不推荐使用的。