使用引用操作 DOM

React 会自动更新 DOM 以匹配你的渲染输出,因此你的组件不需要经常操作它。然而,有时你可能需要访问由 React 管理的 DOM 元素 - 例如,聚焦一个节点、滚动到它或测量它的大小和位置。在 React 中没有内置的方法来做这些事情,所以你需要一个 DOM 节点的引用。

¥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.

你将学习到

  • 如何使用 ref 属性访问由 React 管理的 DOM 节点

    ¥How to access a DOM node managed by React with the ref attribute

  • ref JSX 属性与 useRef 钩子的关系

    ¥How the ref JSX attribute relates to the useRef Hook

  • 如何访问另一个组件的 DOM 节点

    ¥How to access another component’s DOM node

  • 在哪些情况下修改 React 管理的 DOM 是安全的

    ¥In which cases it’s safe to modify the DOM managed by React

获取节点的引用

¥Getting a ref to the node

要访问由 React 管理的 DOM 节点,首先,导入 useRef 钩子:

¥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 属性传递给你想要获取 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 钩子返回一个对象,该对象只有一个名为 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:

  1. 使用 useRef 钩子声明 inputRef

    ¥Declare inputRef with the useRef Hook.

  2. 将其作为 <input ref={inputRef}> 传递。这告诉 React 将 <input> 的 DOM 节点放入 inputRef.current 中。

    ¥Pass it as <input ref={inputRef}>. This tells React to put this <input>’s DOM node into inputRef.current.

  3. handleClick 函数中,从 inputRef.current 读取输入 DOM 节点,并使用 inputRef.current.focus() 在其上调用 focus()

    ¥In the handleClick function, read the input DOM node from inputRef.current and call focus() on it with inputRef.current.focus().

  4. 使用 onClickhandleClick 事件处理程序传递给 <button>

    ¥Pass the handleClick event handler to <button> with onClick.

虽然 DOM 操作是引用最常见的用例,但 useRef 钩子可用于存储 React 之外的其他内容,例如计时器 ID。与状态类似,引用保留在渲染之间。引用就像状态变量,当你设置它们时不会触发重新渲染。阅读 使用引用引用值 中的引用。

¥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}>
          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>
    </>
  );
}

深入研究

如何使用引用回调管理引用列表

¥How to manage a list of refs using a ref callback

在上面的示例中,有预定义数量的引用。然而,有时你可能需要对列表中的每个条目进行引用,而你不知道你将拥有多少。像这样的事情是行不通的:

¥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>

这是因为钩子只能在组件的顶层调用。你不能在循环、条件或 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.

解决此问题的一种可能方法是获取对其父元素的单个引用,然后使用 DOM 操作方法,如 querySelectorAll 去 “寻找” 来自它的各个子节点。但是,这很脆弱,如果你的 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 回调。 React 将在设置引用时使用 DOM 节点调用你的引用回调,并在需要清除它时使用 null 调用你的引用回调。这使你可以维护自己的数组或 映射,并通过其索引或某种 ID 访问任何引用。

¥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 with null 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])}>Tom</button>
        <button onClick={() => scrollToCat(catList[5])}>Maru</button>
        <button onClick={() => scrollToCat(catList[9])}>Jellylorum</button>
      </nav>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat, node);
                } else {
                  map.delete(cat);
                }
              }}
            >
              <img src={cat} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

function setupCatList() {
  const catList = [];
  for (let i = 0; i < 10; i++) {
    catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
  }

  return catList;
}

在此示例中,itemsRef 不包含单个 DOM 节点。而是,它持有从条目 ID 到 DOM 节点的 映射。(引用可以包含任何值!)每个列表项上的 ref 回调 都会注意更新地图:

¥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();
if (node) {
// Add to the Map
map.set(cat, node);
} else {
// Remove from the Map
map.delete(cat);
}
}}
>

这使你可以稍后从映射中读取单个 DOM 节点。

¥This lets you read individual DOM nodes from the Map later.

Canary

此示例展示了使用 ref 回调清理函数管理 Map 的另一种方法。

¥This example shows another approach for managing the Map with a ref callback cleanup function.

<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 节点

¥Accessing another component’s DOM nodes

当你把一个引用放在输出浏览器元素的内置组件上时,比如 <input />,React 会将该引用的 current 属性设置为相应的 DOM 节点(例如浏览器中实际的 <input />)。

¥When you put a ref on a built-in component that outputs a browser element like <input />, React will set that ref’s current property to the corresponding DOM node (such as the actual <input /> in the browser).

但是,如果你尝试在自己的组件上添加引用,例如 <MyInput />,默认情况下你将得到 null。这是一个演示它的例子。请注意单击按钮不会使输入聚焦:

¥However, if you try to put a ref on your own component, like <MyInput />, by default you will get null. Here is an example demonstrating it. Notice how clicking the button does not focus the input:

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 还会向控制台打印错误:

¥To help you notice the issue, React also prints an error to the console:

Console

发生这种情况是因为默认情况下 React 不允许组件访问其他组件的 DOM 节点。连自己的子级都不放过!这是故意的。引用是一个应急方案,应该谨慎使用。手动操作另一个组件的 DOM 节点会使你的代码更加脆弱。

¥This happens because by default React does not let a component access the DOM nodes of other components. Not even for its own children! This is intentional. Refs are an escape hatch that should be used sparingly. Manually manipulating another component’s DOM nodes makes your code even more fragile.

相反,想要公开其 DOM 节点的组件必须选择该行为。一个组件可以指定它 “向前移动” 它的引用到它的一个子级。下面是 MyInput 如何使用 forwardRef API:

¥Instead, components that want to expose their DOM nodes have to opt in to that behavior. A component can specify that it “forwards” its ref to one of its children. Here’s how MyInput can use the forwardRef API:

const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

它是这样工作的:

¥This is how it works:

  1. <MyInput ref={inputRef} /> 告诉 React 将相应的 DOM 节点放入 inputRef.current 中。但是,由 MyInput 组件选择是否加入该选项 - 默认情况下,它不会。

    ¥<MyInput ref={inputRef} /> tells React to put the corresponding DOM node into inputRef.current. However, it’s up to the MyInput component to opt into that—by default, it doesn’t.

  2. MyInput 组件使用 forwardRef 声明。这选择它从上面接收 inputRef 作为在 props 之后声明的第二个 ref 参数。

    ¥The MyInput component is declared using forwardRef. This opts it into receiving the inputRef from above as the second ref argument which is declared after props.

  3. MyInput 本身将它收到的 ref 传递给它内部的 <input>

    ¥MyInput itself passes the ref it received to the <input> inside of it.

现在单击按钮以聚焦输入工作:

¥Now clicking the button to focus the input works:

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 节点是一种常见的模式,用于底层组件(如按钮、输入等)。另一方面,表单、列表或页面部分等高级组件通常不会公开其 DOM 节点,以避免对 DOM 结构的意外依赖。

¥In design systems, it is a common pattern for low-level components like buttons, inputs, and so on, to forward their refs to their DOM nodes. On the other hand, high-level components like forms, lists, or page sections usually won’t expose their DOM nodes to avoid accidental dependencies on the DOM structure.

深入研究

使用命令句柄公开 API 的子集

¥Exposing a subset of the API with an imperative handle

在上面的示例中,MyInput 公开了原始 DOM 输入元素。这让父组件在其上调用 focus()。然而,这也让父组件可以做其他事情 - 例如,改变它的 CSS 样式。在不常见的情况下,你可能希望限制公开的功能。你可以用 useImperativeHandle 做到这一点:

¥In the above example, MyInput exposes 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 {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} 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>
    </>
  );
}

这里,MyInput 中的 realInputRef 保存着实际的输入 DOM 节点。但是,useImperativeHandle 指示 React 将你自己的特殊对象作为引用的值提供给父组件。所以 Form 组件内部的 inputRef.current 将只有 focus 方法。在这种情况下,引用 “句柄” 不是 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 调用你的组件来确定屏幕上应该显示什么。

    ¥During render, React calls your components to figure out what should be on the screen.

  • 在提交期间,React 将更改应用于 DOM。

    ¥During commit, React applies changes to the DOM.

通常,你 不希望 在渲染期间访问引用。这也适用于持有 DOM 节点的引用。在第一次渲染期间,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.

通常,你将从事件处理程序访问引用。如果你想用引用做一些事情,但没有特定的事件可以做,你可能需要一个副作用。我们将在下一页讨论效果。

¥Usually, you will access refs from event handlers. If you want to do something with a ref, but there is no particular event to do it in, you might need an Effect. We will discuss Effects on the next pages.

深入研究

使用 flushSync 同步地刷新状态更新

¥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

引用是一个应急方案。你应该只在必须 “走出 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 管理的元素中修改、添加或删除子元素可能会导致不一致的视觉结果或像上面那样的崩溃。

¥Avoid changing DOM nodes managed by React. Modifying, adding children to, or removing children from elements that are managed by React can lead to inconsistent visual results or crashes like above.

但是,这并不意味着你根本无法做到。这需要谨慎。你可以安全地修改 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 元素。

    ¥Refs are a generic concept, but most often you’ll use them to hold DOM elements.

  • 你通过传递 <div ref={myRef}> 指示 React 将 DOM 节点放入 myRef.current

    ¥You instruct React to put a DOM node into myRef.current by passing <div ref={myRef}>.

  • 通常,你将使用引用进行非破坏性操作,例如聚焦、滚动或测量 DOM 元素。

    ¥Usually, you will use refs for non-destructive actions like focusing, scrolling, or measuring DOM elements.

  • 默认情况下,组件不会公开其 DOM 节点。你可以选择通过使用 forwardRef 并将第二个 ref 参数向下传递到特定节点来公开 DOM 节点。

    ¥A component doesn’t expose its DOM nodes by default. You can opt into exposing a DOM node by using forwardRef and passing the second ref argument down to a specific node.

  • 避免更改由 React 管理的 DOM 节点。

    ¥Avoid changing DOM nodes managed by React.

  • 如果你确实修改了由 React 管理的 DOM 节点,请修改 React 没有理由更新的部分。

    ¥If you do modify DOM nodes managed by React, modify parts that React has no reason to update.

挑战 1 / 4:
播放和暂停视频

¥Play and pause the video

在此示例中,按钮切换状态变量以在播放和暂停状态之间切换。然而,为了真正播放或暂停视频,切换状态是不够的。你还需要在 <video> 的 DOM 元素上调用 play()pause()。向其添加引用,并使按钮起作用。

¥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>
    </>
  )
}

对于额外的挑战,即使用户右键单击视频并使用内置的浏览器媒体控件播放它,也要使 “播放” 按钮与视频是否正在播放同步。为此,你可能需要收听视频中的 onPlayonPause

¥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.


React 中文网 - 粤ICP备13048890号