React 会自动更新 DOM 以匹配你的渲染输出,因此你的组件通常不需要去操作它。然而,有时你可能需要访问由 React 管理的 DOM 元素——例如,聚焦某个节点、滚动到它,或者测量它的大小和位置。在 React 中没有内置的方法来完成这些操作,所以你需要一个指向 DOM 节点的 ref。
🌐 React automatically updates the DOM to match your render output, so your components won’t often need to manipulate it. However, sometimes you might need access to the DOM elements managed by React—for example, to focus a node, scroll to it, or measure its size and position. There is no built-in way to do those things in React, so you will need a ref to the DOM node.
你将学习到
- 如何访问由 React 管理且具有
ref属性的 DOM 节点 refJSX 属性与useRefHook 的关系- 如何访问另一个组件的 DOM 节点
- 在哪些情况下修改 React 管理的 DOM 是安全的
获取节点的引用
🌐 Getting a ref to the node
要访问由 React 管理的 DOM 节点,首先导入 useRef Hook:
🌐 To access a DOM node managed by React, first, import the useRef Hook:
import { useRef } from 'react';然后,使用它在你的组件内声明一个引用:
🌐 Then, use it to declare a ref inside your component:
const myRef = useRef(null);最后,将你的 ref 作为 ref 属性传递给你想获取其 DOM 节点的 JSX 标签:
🌐 Finally, pass your ref as the ref attribute to the JSX tag for which you want to get the DOM node:
<div ref={myRef}>useRef Hook 返回一个包含单个属性 current 的对象。最初,myRef.current 将是 null。当 React 为这个 <div> 创建一个 DOM 节点时,React 会将该节点的引用放入 myRef.current。然后,你可以从你的 事件处理程序 访问这个 DOM 节点,并使用定义在其上的内置 浏览器 API。
🌐 The useRef Hook returns an object with a single property called current. Initially, myRef.current will be null. When React creates a DOM node for this <div>, React will put a reference to this node into myRef.current. You can then access this DOM node from your event handlers and use the built-in browser APIs defined on it.
// You can use any browser APIs, for example:
myRef.current.scrollIntoView();示例:聚焦文本输入
🌐 Example: Focusing a text input
在此示例中,单击按钮将聚焦输入:
🌐 In this example, clicking the button will focus the input:
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> </> ); }
要实现这一点:
🌐 To implement this:
- 使用
useRefHook 声明inputRef。 - 将其传递为
<input ref={inputRef}>。这告诉 React 将这个<input>的 DOM 节点放入inputRef.current中。 - 在
handleClick函数中,从inputRef.current读取输入 DOM 节点,并使用inputRef.current.focus()调用focus() - 将
handleClick事件处理程序与onClick一起传递给<button>。
虽然 DOM 操作是使用 refs 最常见的场景,但 useRef Hook 可以用于在 React 外部存储其他内容,例如定时器 ID。与 state 类似,refs 在渲染之间保持不变。refs 就像不会在设置时触发重新渲染的状态变量。请阅读关于 refs 的信息:使用 Refs 引用值。
🌐 While DOM manipulation is the most common use case for refs, the useRef Hook can be used for storing other things outside React, like timer IDs. Similarly to state, refs remain between renders. Refs are like state variables that don’t trigger re-renders when you set them. Read about refs in Referencing Values with Refs.
示例:滚动到某个元素
🌐 Example: Scrolling to an element
在一个组件中,你可以拥有多个引用。在这个例子中,有一个包含三张图片的轮播。每个按钮通过在对应的 DOM 节点上调用浏览器的 scrollIntoView() 方法来将图片居中:
🌐 You can have more than a single ref in a component. In this example, there is a carousel of three images. Each button centers an image by calling the browser scrollIntoView() method on the corresponding DOM node:
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}> Neo </button> <button onClick={handleScrollToSecondCat}> Millie </button> <button onClick={handleScrollToThirdCat}> Bella </button> </nav> <div> <ul> <li> <img src="https://placecats.com/neo/300/200" alt="Neo" ref={firstCatRef} /> </li> <li> <img src="https://placecats.com/millie/200/200" alt="Millie" ref={secondCatRef} /> </li> <li> <img src="https://placecats.com/bella/199/200" alt="Bella" ref={thirdCatRef} /> </li> </ul> </div> </> ); }
深入研究
🌐 How to manage a list of refs using a ref callback
在上述例子中,引用(refs)的数量是预定义的。然而,有时你可能需要为列表中的每一项创建一个引用,但你不知道会有多少项。像下面这样的做法是行不通的:
🌐 In the above examples, there is a predefined number of refs. However, sometimes you might need a ref to each item in the list, and you don’t know how many you will have. Something like this wouldn’t work:
<ul>
{items.map((item) => {
// Doesn't work!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>这是因为 Hooks 只能在组件的顶层调用。 你不能在循环中、条件中或 map() 调用内部调用 useRef。
🌐 This is because Hooks must only be called at the top-level of your component. You can’t call useRef in a loop, in a condition, or inside a map() call.
一种可能的解决方法是获取它们父元素的单个引用,然后使用像 querySelectorAll 这样的 DOM 操作方法从中“找到”各个子节点。然而,这种方法很脆弱,如果你的 DOM 结构发生变化可能会失效。
🌐 One possible way around this is to get a single ref to their parent element, and then use DOM manipulation methods like querySelectorAll to “find” the individual child nodes from it. However, this is brittle and can break if your DOM structure changes.
另一个解决方案是将函数传递给 ref 属性。 这被称为 ref 回调。当需要设置 ref 时,React 会使用 DOM 节点调用你的 ref 回调,当需要清除它时,会调用回调返回的清理函数。这允许你维护自己的数组或 Map,并通过索引或某种 ID 访问任何 ref。
🌐 Another solution is to pass a function to the ref attribute. This is called a ref callback. React will call your ref callback with the DOM node when it’s time to set the ref, and call the cleanup function returned from the callback when it’s time to clear it. This lets you maintain your own array or a Map, and access any ref by its index or some kind of ID.
此示例显示如何使用此方法滚动到长列表中的任意节点:
🌐 This example shows how you can use this approach to scroll to an arbitrary node in a long list:
import { useRef, useState } from "react"; export default function CatFriends() { const itemsRef = useRef(null); const [catList, setCatList] = useState(setupCatList); function scrollToCat(cat) { const map = getMap(); const node = map.get(cat); node.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } function getMap() { if (!itemsRef.current) { // Initialize the Map on first usage. itemsRef.current = new Map(); } return itemsRef.current; } return ( <> <nav> <button onClick={() => scrollToCat(catList[0])}>Neo</button> <button onClick={() => scrollToCat(catList[5])}>Millie</button> <button onClick={() => scrollToCat(catList[8])}>Bella</button> </nav> <div> <ul> {catList.map((cat) => ( <li key={cat.id} ref={(node) => { const map = getMap(); map.set(cat, node); return () => { map.delete(cat); }; }} > <img src={cat.imageUrl} /> </li> ))} </ul> </div> </> ); } function setupCatList() { const catCount = 10; const catList = new Array(catCount) for (let i = 0; i < catCount; i++) { let imageUrl = ''; if (i < 5) { imageUrl = "https://placecats.com/neo/320/240"; } else if (i < 8) { imageUrl = "https://placecats.com/millie/320/240"; } else { imageUrl = "https://placecats.com/bella/320/240"; } catList[i] = { id: i, imageUrl, }; } return catList; }
在这个示例中,itemsRef 并不保存单个 DOM 节点。相反,它保存了一个从项目 ID 到 DOM 节点的 Map。(Refs 可以保存任何值!) 每个列表项上的 ref 回调 负责更新该 Map:
🌐 In this example, itemsRef doesn’t hold a single DOM node. Instead, it holds a Map from item ID to a DOM node. (Refs can hold any values!) The ref callback on every list item takes care to update the Map:
<li
key={cat.id}
ref={node => {
const map = getMap();
// Add to the Map
map.set(cat, node);
return () => {
// Remove from the Map
map.delete(cat);
};
}}
>这使你可以稍后从映射中读取单个 DOM 节点。
🌐 This lets you read individual DOM nodes from the Map later.
访问另一个组件的 DOM 节点
🌐 Accessing another component’s DOM nodes
你可以像传递其他属性一样将 refs 从父组件传递给子组件。
🌐 You can pass refs from parent component to child components just like any other prop.
import { useRef } from 'react';
function MyInput({ ref }) {
return <input ref={ref} />;
}
function MyForm() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />
}在上面的示例中,ref 在父组件 MyForm 中创建,并传递给子组件 MyInput。然后 MyInput 将 ref 传递给 <input>。因为 <input> 是一个 内置组件,所以 React 会将 ref 的 .current 属性设置为 <input> DOM 元素。
🌐 In the above example, a ref is created in the parent component, MyForm, and is passed to the child component, MyInput. MyInput then passes the ref to <input>. Because <input> is a built-in component React sets the .current property of the ref to the <input> DOM element.
在 MyForm 中创建的 inputRef 现在指向由 MyInput 返回的 <input> DOM 元素。在 MyForm 中创建的点击处理程序可以访问 inputRef 并调用 focus() 来将焦点设置到 <input>。
🌐 The inputRef created in MyForm now points to the <input> DOM element returned by MyInput. A click handler created in MyForm can access inputRef and call focus() to set the focus on <input>.
import { useRef } from 'react'; function MyInput({ ref }) { return <input ref={ref} />; } export default function MyForm() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
深入研究
🌐 Exposing a subset of the API with an imperative handle
在上述示例中,传递给 MyInput 的 ref 会传递给原始 DOM 输入元素。这让父组件可以在其上调用 focus()。然而,这也让父组件可以做其他事情——例如,更改它的 CSS 样式。在不常见的情况下,你可能想限制暴露的功能。你可以使用 useImperativeHandle 来实现这一点:
🌐 In the above example, the ref passed to MyInput is passed on to the original DOM input element. This lets the parent component call focus() on it. However, this also lets the parent component do something else—for example, change its CSS styles. In uncommon cases, you may want to restrict the exposed functionality. You can do that with useImperativeHandle:
import { useRef, useImperativeHandle } from "react"; function MyInput({ ref }) { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ // Only expose focus and nothing else focus() { realInputRef.current.focus(); }, })); return <input ref={realInputRef} />; }; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}>Focus the input</button> </> ); }
在这里,realInputRef 位于 MyInput 内部,保存实际的输入 DOM 节点。然而,useImperativeHandle 指示 React 将你自己的特殊对象作为 ref 的值提供给父组件。因此,Form 组件内部的 inputRef.current 将只有 focus 方法。在这种情况下,ref “句柄” 不是 DOM 节点,而是你在 useImperativeHandle 调用中创建的自定义对象。
🌐 Here, realInputRef inside MyInput holds the actual input DOM node. However, useImperativeHandle instructs React to provide your own special object as the value of a ref to the parent component. So inputRef.current inside the Form component will only have the focus method. In this case, the ref “handle” is not the DOM node, but the custom object you create inside useImperativeHandle call.
当 React 附加引用时
🌐 When React attaches the refs
在 React 中,每次更新都分为 两个阶段:
🌐 In React, every update is split in two phases:
- 在渲染期间,React 会调用你的组件来确定屏幕上应该显示什么。
- 在 提交 阶段,React 会将更改应用到 DOM。
一般来说,你不想在渲染期间访问 refs。这同样适用于保存 DOM 节点的 refs。在第一次渲染时,DOM 节点还没有被创建,所以 ref.current 将会是 null。在更新渲染期间,DOM 节点还没有被更新。因此现在读取它们为时过早。
🌐 In general, you don’t want to access refs during rendering. That goes for refs holding DOM nodes as well. During the first render, the DOM nodes have not yet been created, so ref.current will be null. And during the rendering of updates, the DOM nodes haven’t been updated yet. So it’s too early to read them.
React 在提交期间设置 ref.current。在更新 DOM 之前,React 将受影响的 ref.current 值设置为 null。在更新 DOM 之后,React 会立即将它们设置为相应的 DOM 节点。
🌐 React sets ref.current during the commit. Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes.
通常,你会从事件处理程序中访问 refs。 如果你想对 ref 做一些操作,但没有特定的事件可以使用,你可能需要一个 Effect。我们将在接下来的页面中讨论 Effects。
深入研究
🌐 Flushing state updates synchronously with flushSync
考虑像这样的代码,它会添加一个新的待办事项并将屏幕滚动到列表的最后一个子项。注意,不知何故,它总是滚动到刚刚添加的待办事项的前一个:
🌐 Consider code like this, which adds a new todo and scrolls the screen down to the last child of the list. Notice how, for some reason, it always scrolls to the todo that was just before the last added one:
import { useState, useRef } from 'react'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; setText(''); setTodos([ ...todos, newTodo]); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Add </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
问题在于这两行:
🌐 The issue is with these two lines:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();在 React 中,状态更新是排队进行的。 通常,这正是你想要的。然而,在这里它引发了一个问题,因为 setTodos 并不会立即更新 DOM。所以当你滚动列表到最后一个元素时,待办事项尚未被添加。这就是为什么滚动总是“落后”一个项目的原因。
🌐 In React, state updates are queued. Usually, this is what you want. However, here it causes a problem because setTodos does not immediately update the DOM. So the time you scroll the list to its last element, the todo has not yet been added. This is why scrolling always “lags behind” by one item.
要解决此问题,你可以强制 React 同步更新(“刷新”)DOM。为此,请从 react-dom 导入 flushSync,并 将状态更新 封装到 flushSync 调用中:
🌐 To fix this issue, you can force React to update (“flush”) the DOM synchronously. To do this, import flushSync from react-dom and wrap the state update into a flushSync call:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();这将指示 React 在 flushSync 封装的代码执行后立即同步更新 DOM。因此,当你尝试滚动到最后一个待办事项时,它已经在 DOM 中了:
🌐 This will instruct React to update the DOM synchronously right after the code wrapped in flushSync executes. As a result, the last todo will already be in the DOM by the time you try to scroll to it:
import { useState, useRef } from 'react'; import { flushSync } from 'react-dom'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; flushSync(() => { setText(''); setTodos([ ...todos, newTodo]); }); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Add </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
使用引用操作 DOM 的最佳实践
🌐 Best practices for DOM manipulation with refs
Refs 是一种逃生通道。你应该只在必须“跳出 React”时使用它们。常见的例子包括管理焦点、滚动位置,或调用 React 未暴露的浏览器 API。
🌐 Refs are an escape hatch. You should only use them when you have to “step outside React”. Common examples of this include managing focus, scroll position, or calling browser APIs that React does not expose.
如果你坚持执行非破坏性的操作,比如聚焦和滚动,你不应该遇到任何问题。然而,如果你尝试手动修改DOM,你可能会与React正在进行的更改发生冲突。
🌐 If you stick to non-destructive actions like focusing and scrolling, you shouldn’t encounter any problems. However, if you try to modify the DOM manually, you can risk conflicting with the changes React is making.
为了说明这个问题,这个示例包含一条欢迎消息和两个按钮。第一个按钮使用条件渲染和状态来切换它的显示,就像你在 React 中通常做的一样。第二个按钮使用remove() DOM API在 React 控制之外强制将其从 DOM 中移除。
🌐 To illustrate this problem, this example includes a welcome message and two buttons. The first button toggles its presence using conditional rendering and state, as you would usually do in React. The second button uses the remove() DOM API to forcefully remove it from the DOM outside of React’s control.
尝试点击“使用 setState 切换”几次。消息应该会消失然后再次出现。然后点击“从 DOM 中移除”。这会强制将其移除。最后,点击“使用 setState 切换”:
🌐 Try pressing “Toggle with setState” a few times. The message should disappear and appear again. Then press “Remove from the DOM”. This will forcefully remove it. Finally, press “Toggle with setState”:
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 再次显示它会导致崩溃。这是因为你已经更改了 DOM,而 React 不知道如何正确继续管理它。
🌐 After you’ve manually removed the DOM element, trying to use setState to show it again will lead to a crash. This is because you’ve changed the DOM, and React doesn’t know how to continue managing it correctly.
避免更改由 React 管理的 DOM 节点。 修改、添加子元素或从由 React 管理的元素中移除子元素,可能导致视觉结果不一致或像上文一样发生崩溃。
然而,这并不意味着你完全不能这样做。这需要谨慎。**你可以安全地修改 React 没有理由更新的 DOM 部分。**例如,如果某个 <div> 在 JSX 中总是为空,React 就没有理由去修改它的子节点列表。因此,在那里手动添加或移除元素是安全的。
🌐 However, this doesn’t mean that you can’t do it at all. It requires caution. You can safely modify parts of the DOM that React has no reason to update. For example, if some <div> is always empty in the JSX, React won’t have a reason to touch its children list. Therefore, it is safe to manually add or remove elements there.
回顾
- 引用是一个通用概念,但大多数情况下你将使用它们来保存 DOM 元素。
- 你通过传递
<div ref={myRef}>来指示 React 将一个 DOM 节点放入myRef.current。 - 通常,你将使用引用进行非破坏性操作,例如聚焦、滚动或测量 DOM 元素。
- 组件默认不会公开其 DOM 节点。你可以通过使用
ref属性选择公开一个 DOM 节点。 - 避免更改由 React 管理的 DOM 节点。
- 如果你确实修改了由 React 管理的 DOM 节点,请修改 React 没有理由更新的部分。
挑战 1 of 4: 播放和暂停视频
🌐 Play and pause the video
在这个例子中,按钮切换一个状态变量以在播放和暂停状态之间切换。然而,仅切换状态并不足以真正播放或暂停视频。你还需要在 <video> 的 DOM 元素上调用 play() 和 pause()。给它添加一个 ref,并让按钮能够工作。
🌐 In this example, the button toggles a state variable to switch between a playing and a paused state. However, in order to actually play or pause the video, toggling state is not enough. You also need to call play() and pause() on the DOM element for the <video>. Add a ref to it, and make the button work.
import { useState, useRef } from 'react'; export default function VideoPlayer() { const [isPlaying, setIsPlaying] = useState(false); function handleClick() { const nextIsPlaying = !isPlaying; setIsPlaying(nextIsPlaying); } return ( <> <button onClick={handleClick}> {isPlaying ? 'Pause' : 'Play'} </button> <video width="250"> <source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4" /> </video> </> ) }
为了增加额外的挑战,即使用户右键点击视频并使用浏览器内置的媒体控件播放视频,也要保持“播放”按钮与视频的播放状态同步。你可能需要监听视频上的 onPlay 和 onPause 来实现这一点。
🌐 For an extra challenge, keep the “Play” button in sync with whether the video is playing even if the user right-clicks the video and plays it using the built-in browser media controls. You might want to listen to onPlay and onPause on the video to do that.