React根据数据自动更新DOM,不需要经常手动操作DOM。但是,有时候你需要访问DOM元素,例如使node处于focus状态,滚动一个node,或是获取node的大小和位置。可以使用指向DOM的ref来访问DOM。
获取node的ref
访问一个DOM node,首先导入useRefHook:
import { useRef } from 'react';
然后,在组件内声明一个ref:
const myRef = useRef(null);
最后,把ref传递给JSX标签的ref属性,获得想访问的DOM node:
<div ref={myRef}>
useRef返回带有一个current属性的对象。起初,myRef.current的值是null。当React创建了这个<div>的DOM node后,React把myRef.current指向到这个DOM。既可以在事件处理函数里面访问这个DOM,也可以使用内嵌的一些浏览器APIs。
// You can use any browser APIs, for example:
myRef.current.scrollIntoView();
例子:使一个input处于focus状态
在这个例子中,点击button会使input处于focus状态:
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
实现过程:
- 使用
useRefHook声明一个inputDef。 - 把inputRef传递给
<input ref={inputRef}>。告诉React把inputRef.current指向<input>的DOM node。 - 在handleClick函数里,使用inputRef.current读取input的DOM node,然后使用inputREf.current.focus()调用focus()。
- 把handleClick事件函数绑定在的onClick事件上。
DOM操作是refs最常见的用例,uesRef也可以存储React本身渲染外的其他数据,比如timer IDs。和state类似,refs保留两次渲染间的数据值,不被重新初始化值。
例子:滚动元素
一个组件中可以有多个ref。在这个例子中,有3个图片,对应3个button,点击每个button使对应的图片居中。每个button的事件处理函数调用浏览器的DOM的scrollIntoView()方式使图片居中。
import { useRef } from 'react';
export default function CatFriends() {
const firstCatRef = useRef(null);
const secondCatRef = useRef(null);
const thirdCatRef = useRef(null);
function handleScrollToFirstCat() {
firstCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function handleScrollToSecondCat() {
secondCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function handleScrollToThirdCat() {
thirdCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
return (
<>
<nav>
<button onClick={handleScrollToFirstCat}>
Tom
</button>
<button onClick={handleScrollToSecondCat}>
Maru
</button>
<button onClick={handleScrollToThirdCat}>
Jellylorum
</button>
</nav>
<div>
<ul>
<li>
<img
src="https://placekitten.com/g/200/200"
alt="Tom"
ref={firstCatRef}
/>
</li>
<li>
<img
src="https://placekitten.com/g/300/200"
alt="Maru"
ref={secondCatRef}
/>
</li>
<li>
<img
src="https://placekitten.com/g/250/200"
alt="Jellylorum"
ref={thirdCatRef}
/>
</li>
</ul>
</div>
</>
);
}
访问其他组件的DOM nodes
如果你把ref指向一个自定义组件,比如<MyInput />,ref指向的值是null。我们看下面的例子。点击button按钮没有使input处于focus状态:
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}>
Focus the input
</button>
</>
);
}
为了帮助你注意到这个问题,React还会在控制台打印一个错误:
默认情况下React不允许组件访问其他组件的DOM nodes,即使自己的子组件也不行。手动操作另一个组件的DOM节点会使代码更加脆弱。
如果组件想要暴露他们的DOM,需要组件自己指定这个行为。组件可以指定它将其ref“转发”给它的一个子组件。下面是MyInput如何使用forwardRef API:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
<MyInput ref={inputRef} />告诉React将相应的DOM节点放入inputRef.current中。能不能这样做取决于MyInput组件来选择,默认情况下是不可以的。- 使用forwardRef声明MyInput组件。 myInput选择接收inputRef,通过第二个ref参数接收。
- MyInput本身将接收到的ref传递给它内部的
<input>。
当点击按钮的时候,input处于focus状态,看下面的代码:
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}>
Focus the input
</button>
</>
);
}
什么时候把DOM赋值给refs
在React中,每次更新都分为两个阶段:
- 在render阶段,React调用组件来确定屏幕上应该显示什么。
- 在commit阶段,React更改DOM。
在渲染期间不能访问refs指向的DOM。在第一次渲染期间,DOM nodes还没有被创建,所以ref.current值为null。在重新渲染期间,DOM nodes还没有被更新,所以读DOM还有些早。 React在commit阶段设置ref.current。在更新DOM前,React设置ref.current值为null。更新DOM之后,React立即把ref.current值设置为对应的DOM nodes。
在通常,在事件处理函数里面读refs。如果你想用ref做一些事情,但没有特定的事件来做,你可能需要Effect。
最佳实践
Refs是一个与外部交互的接口。必须只在“走出React”的时候使用它们。常见的例子是管理focus,滚动条位置或是调用React没有对外的浏览器接口。
如果坚持使用非破坏性的操作,比如focus和滚动,应该不会遇到任何问题。但是,如果尝试手动修改DOM,就有可能与React所做的更改发生冲突。
为了说明这个问题,这个示例包括一个欢迎信息和两个button。第一个button使用state改变来切换是否展示欢迎信息。第二个button脱离React的控制,使用remove() DOM API强制删除DOM。
尝试多次点击“Toggel with setSate”button。欢迎消息会切换展示和不展示。然后点击“Remove from the DOM”强制删除DOM。最后点击“Tooggle with setState” button:
import { useState, useRef } from 'react';
export default function Counter() {
const [show, setShow] = useState(true);
const ref = useRef(null);
return (
<div>
<button
onClick={() => {
setShow(!show);
}}>
Toggle with setState
</button>
<button
onClick={() => {
ref.current.remove();
}}>
Remove from the DOM
</button>
{show && <p ref={ref}>Hello world</p>}
</div>
);
}
手动强制删除DOM元素后,然后使用setState展示欢迎信息,会导致bug。因为你已经改变了DOM,但是React不知道如何继续正确的管理它。
避免更改由React管理的DOM节点。
修改、添加或删除由React管理的元素中的子元素可能会导致不一致的视觉结果或如上所述的崩溃。
这并不意味着不能使用ref改变DOM元素,就是要谨慎使用。可以安全地修改React不更新的DOM。例如,在JSX中一个<div>一直是空的,React将不会去碰它的孩子列表,给这个<div>手动添加或删除元素是安全的。
总结
- ref是一个通用概念,但大多数情况下,将使用它们来保存DOM元素。
- 指示React把一个DOM节点放到myRef中。通过
<div ref={myRef}>传递。 - 通常,使元素处于focus状态、滚动元素或读取DOM元素等非破坏性操作中使用refs。
- 默认情况下,组件不暴露其DOM节点。你可以选择使用forwardRef并传第二个ref参数暴露DOM节点。
- 避免更改由React管理的DOM节点。
- 如果要修改由React管理的DOM节点,修改React不会更新的DOM。