应急方案

高级

你的一些组件可能需要控制并与 React 之外的系统进行同步。例如,你可能需要使用浏览器 API 来聚焦输入框,播放或暂停一个非 React 实现的视频播放器,或者连接并监听来自远程服务器的消息。在本章中,你将学习一些“应急方案”,让你能够“走出”React,连接到外部系统。你大部分的应用逻辑和数据流不应依赖于这些功能。

🌐 Some of your components may need to control and synchronize with systems outside of React. For example, you might need to focus an input using the browser API, play and pause a video player implemented without React, or connect and listen to messages from a remote server. In this chapter, you’ll learn the escape hatches that let you “step outside” React and connect to external systems. Most of your application logic and data flow should not rely on these features.

使用引用引用值

🌐 Referencing values with refs

当你希望一个组件“记住”某些信息,但又不希望这些信息触发新的渲染时,你可以使用ref

🌐 When you want a component to “remember” some information, but you don’t want that information to trigger new renders, you can use a ref:

const ref = useRef(0);

像 state 一样,refs 会在 React 的重新渲染之间保留。然而,设置 state 会重新渲染组件。改变 ref 不会!你可以通过 current 属性访问该 ref 的当前值。

🌐 Like state, refs are retained by React between re-renders. However, setting state re-renders a component. Changing a ref does not! You can access the current value of that ref through the ref.current property.

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

ref 就像是你的组件中的一个秘密口袋,React 不会跟踪它。例如,你可以使用 refs 来存储 超时 IDDOM 元素 以及其他不影响组件渲染输出的对象。

🌐 A ref is like a secret pocket of your component that React doesn’t track. For example, you can use refs to store timeout IDs, DOM elements, and other objects that don’t impact the component’s rendering output.

准备好学习这个主题了吗?

阅读 使用 Refs 引用值 来学习如何使用 refs 记住信息。

🌐 Read Referencing Values with Refs to learn how to use refs to remember information.

阅读更多

使用引用操作 DOM

🌐 Manipulating the DOM with refs

React 会自动更新 DOM 以匹配你的渲染输出,因此你的组件通常不需要操作它。然而,有时你可能需要访问由 React 管理的 DOM 元素——例如,聚焦某个节点、滚动到它,或测量其大小和位置。在 React 中没有内置的方法来完成这些操作,因此你需要一个指向 DOM 节点的 ref。例如,点击按钮将使用 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. For example, clicking the button will focus the input using a ref:

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

准备好学习这个主题了吗?

阅读 使用 Refs 操作 DOM 以了解如何访问由 React 管理的 DOM 元素。

🌐 Read Manipulating the DOM with Refs to learn how to access DOM elements managed by React.

阅读更多

使用副作用进行同步

🌐 Synchronizing with Effects

有些组件需要与外部系统同步。例如,你可能希望根据 React 状态控制一个非 React 组件,建立服务器连接,或者在组件出现在屏幕上时发送分析日志。与允许你处理特定事件的事件处理程序不同,Effects 允许你在渲染后运行一些代码。使用它们可以将你的组件与 React 之外的系统同步。

🌐 Some components need to synchronize with external systems. For example, you might want to control a non-React component based on the React state, set up a server connection, or send an analytics log when a component appears on the screen. Unlike event handlers, which let you handle particular events, Effects let you run some code after rendering. Use them to synchronize your component with a system outside of React.

按几次播放/暂停,看看视频播放器如何与 isPlaying 属性值保持同步:

🌐 Press Play/Pause a few times and see how the video player stays synchronized to the isPlaying prop value:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

许多 Effect 也会在自身执行后进行“清理”。例如,一个用于建立与聊天服务器连接的 Effect 应该返回一个 清理函数,告诉 React 如何将你的组件从该服务器断开连接:

🌐 Many Effects also “clean up” after themselves. For example, an Effect that sets up a connection to a chat server should return a cleanup function that tells React how to disconnect your component from that server:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}

在开发过程中,React 会立即额外执行一次你的 Effect 并进行清理。这就是为什么你会看到 "✅ Connecting..." 被打印两次。这确保你不会忘记实现清理函数。

🌐 In development, React will immediately run and clean up your Effect one extra time. This is why you see "✅ Connecting..." printed twice. This ensures that you don’t forget to implement the cleanup function.

准备好学习这个主题了吗?

请阅读 与效果同步 以了解如何将组件与外部系统同步。

🌐 Read Synchronizing with Effects to learn how to synchronize components with external systems.

阅读更多

你可能不需要副作用

🌐 You Might Not Need An Effect

Effect 是从 React 范式中逃生的出口。它们让你可以“走出”React,并将你的组件与某些外部系统同步。如果没有涉及外部系统(例如,如果你想在某些 props 或 state 变化时更新组件的状态),你就不需要 Effect。移除不必要的 Effect 会使你的代码更易于理解,运行更快,并且更不容易出错。

🌐 Effects are an escape hatch from the React paradigm. They let you “step outside” of React and synchronize your components with some external system. If there is no external system involved (for example, if you want to update a component’s state when some props or state change), you shouldn’t need an Effect. Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.

有两种常见情况不需要副作用:

🌐 There are two common cases in which you don’t need Effects:

  • 你不需要 Effects 来转换用于渲染的数据。
  • 你不需要 Effects 来处理用户事件。

例如,你不需要副作用来根据其他状态调整某些状态:

🌐 For example, you don’t need an Effect to adjust some state based on other state:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');

// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}

而是,在渲染时尽可能多地计算:

🌐 Instead, calculate as much as you can while rendering:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}

然而,你确实需要 Effects 与外部系统同步。

🌐 However, you do need Effects to synchronize with external systems.

准备好学习这个主题了吗?

阅读 你可能不需要效果 来学习如何移除不必要的效果。

🌐 Read You Might Not Need an Effect to learn how to remove unnecessary Effects.

阅读更多

反应式副作用的生命周期

🌐 Lifecycle of reactive effects

Effect 有不同于组件的生命周期。组件可能会挂载、更新或卸载。Effect 只能做两件事:开始同步某些内容,以及之后停止同步它。如果你的 Effect 依赖随时间变化的 props 和 state,这个循环可以发生多次。

🌐 Effects have a different lifecycle from components. Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time.

此效果取决于 roomId 属性的值。属性是响应式值,这意味着它们可以在重新渲染时发生变化。请注意,如果 roomId 发生变化,效果会重新同步(并重新连接到服务器):

🌐 This Effect depends on the value of the roomId prop. Props are reactive values, which means they can change on a re-render. Notice that the Effect re-synchronizes (and re-connects to the server) if roomId changes:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

React 提供了一条 linter 规则,用于检查你是否正确指定了 Effect 的依赖。如果你在上面的示例中忘记在依赖列表中指定 roomId,linter 会自动发现这个错误。

🌐 React provides a linter rule to check that you’ve specified your Effect’s dependencies correctly. If you forget to specify roomId in the list of dependencies in the above example, the linter will find that bug automatically.

准备好学习这个主题了吗?

阅读 Reactive 事件的生命周期 以了解 Effect 的生命周期与组件的不同之处。

🌐 Read Lifecycle of Reactive Events to learn how an Effect’s lifecycle is different from a component’s.

阅读更多

将事件与副作用分开

🌐 Separating events from Effects

事件处理程序只有在你再次执行相同的交互时才会重新运行。与事件处理程序不同,如果它们读取的任何值(如 props 或状态)与上次渲染时不同,Effects 会重新同步。有时,你希望两种行为混合出现:一种会根据某些值重新运行但对其他值不重新运行的 Effect。

🌐 Event handlers only re-run when you perform the same interaction again. Unlike event handlers, Effects re-synchronize if any of the values they read, like props or state, are different than during last render. Sometimes, you want a mix of both behaviors: an Effect that re-runs in response to some values but not others.

Effects 中的所有代码都是响应式的。如果它读取的某个响应式值由于重新渲染而发生了改变,它将会再次运行。例如,如果 roomIdtheme 其中任何一个发生变化,该 Effect 将重新连接到聊天:

🌐 All code inside Effects is reactive. It will run again if some reactive value it reads has changed due to a re-render. For example, this Effect will re-connect to the chat if either roomId or theme have changed:

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}

这并不理想。你只想在 roomId 发生变化时重新连接聊天。切换 theme 不应该重新连接聊天!将读取 theme 的代码从你的 Effect 中移到 Effect 事件 中:

🌐 This is not ideal. You want to re-connect to the chat only if the roomId has changed. Switching the theme shouldn’t re-connect to the chat! Move the code reading theme out of your Effect into an Effect Event:

import { useState, useEffect } from 'react';
import { useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'} 
      />
    </>
  );
}

Effect 事件中的代码不是响应式的,因此改变 theme 不会再使你的 Effect 重新连接。

🌐 Code inside Effect Events isn’t reactive, so changing the theme no longer makes your Effect re-connect.

准备好学习这个主题了吗?

阅读 将事件与效果分开 以了解如何防止某些值重新触发效果。

🌐 Read Separating Events from Effects to learn how to prevent some values from re-triggering Effects.

阅读更多

移除副作用依赖

🌐 Removing Effect dependencies

当你编写一个 Effect 时,linter 会验证你是否已在 Effect 的依赖列表中包含了该 Effect 所读取的每一个响应式值(如 props 和 state)。这确保了你的 Effect 能与组件的最新 props 和 state 保持同步。不必要的依赖可能会导致你的 Effect 过于频繁地运行,甚至可能造成无限循环。删除它们的方式取决于具体情况。

🌐 When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component. Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop. The way you remove them depends on the case.

例如,这个 Effect 依赖于每次编辑输入时都会重新创建的 options 对象:

🌐 For example, this Effect depends on the options object which gets re-created every time you edit the input:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

你不希望每次在该聊天中开始输入消息时聊天都重新连接。要解决这个问题,将 options 对象的创建移动到 Effect 内,这样 Effect 只依赖于 roomId 字符串:

🌐 You don’t want the chat to re-connect every time you start typing a message in that chat. To fix this problem, move creation of the options object inside the Effect so that the Effect only depends on the roomId string:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

注意,你没有一开始就通过编辑依赖列表来移除 options 依赖。那样是错误的。相反,你改变了周围的代码,使得该依赖变得不必要。把依赖列表看作是你的 Effect 代码使用的所有响应式值的列表。你不会故意选择将什么放到列表中。这个列表描述的是你的代码。要改变依赖列表,就改变代码。

🌐 Notice that you didn’t start by editing the dependency list to remove the options dependency. That would be wrong. Instead, you changed the surrounding code so that the dependency became unnecessary. Think of the dependency list as a list of all the reactive values used by your Effect’s code. You don’t intentionally choose what to put on that list. The list describes your code. To change the dependency list, change the code.

准备好学习这个主题了吗?

阅读 移除 Effect 依赖 以了解如何让你的 Effect 更少重新运行。

🌐 Read Removing Effect Dependencies to learn how to make your Effect re-run less often.

阅读更多

使用自定义钩子重用逻辑

🌐 Reusing logic with custom Hooks

React 自带了内置的 Hooks,比如 useStateuseContextuseEffect。有时,你可能希望有一个用于更具体用途的 Hook:例如,用于获取数据、跟踪用户是否在线,或者连接到聊天室。为此,你可以根据你的应用需求创建自己的 Hooks。

🌐 React comes with built-in Hooks like useState, useContext, and useEffect. Sometimes, you’ll wish that there was a Hook for some more specific purpose: for example, to fetch data, to keep track of whether the user is online, or to connect to a chat room. To do this, you can create your own Hooks for your application’s needs.

在这个例子中,usePointerPosition 自定义 Hook 用于跟踪光标位置,而 useDelayedValue 自定义 Hook 返回一个比你传入的值“落后”了若干毫秒的值。将光标移动到沙箱预览区域上,可以看到一串跟随光标移动的点:

🌐 In this example, the usePointerPosition custom Hook tracks the cursor position, while useDelayedValue custom Hook returns a value that’s “lagging behind” the value you passed by a certain number of milliseconds. Move the cursor over the sandbox preview area to see a moving trail of dots following the cursor:

import { usePointerPosition } from './usePointerPosition.js';
import { useDelayedValue } from './useDelayedValue.js';

export default function Canvas() {
  const pos1 = usePointerPosition();
  const pos2 = useDelayedValue(pos1, 100);
  const pos3 = useDelayedValue(pos2, 200);
  const pos4 = useDelayedValue(pos3, 100);
  const pos5 = useDelayedValue(pos4, 50);
  return (
    <>
      <Dot position={pos1} opacity={1} />
      <Dot position={pos2} opacity={0.8} />
      <Dot position={pos3} opacity={0.6} />
      <Dot position={pos4} opacity={0.4} />
      <Dot position={pos5} opacity={0.2} />
    </>
  );
}

function Dot({ position, opacity }) {
  return (
    <div style={{
      position: 'absolute',
      backgroundColor: 'pink',
      borderRadius: '50%',
      opacity,
      transform: `translate(${position.x}px, ${position.y}px)`,
      pointerEvents: 'none',
      left: -20,
      top: -20,
      width: 40,
      height: 40,
    }} />
  );
}

你可以创建自定义 Hooks,将它们组合在一起,在它们之间传递数据,并在组件之间重复使用它们。随着你的应用的增长,你手动编写的 Effect 会更少,因为你可以重用已经编写的自定义 Hooks。React 社区也维护了许多优秀的自定义 Hooks。

🌐 You can create custom Hooks, compose them together, pass data between them, and reuse them between components. As your app grows, you will write fewer Effects by hand because you’ll be able to reuse custom Hooks you already wrote. There are also many excellent custom Hooks maintained by the React community.

准备好学习这个主题了吗?

阅读 使用自定义 Hook 重用逻辑 以了解如何在组件之间共享逻辑。

🌐 Read Reusing Logic with Custom Hooks to learn how to share logic between components.

阅读更多

下一步是什么?

🌐 What’s next?

前往 使用 Refs 引用值 开始逐页阅读本章!

🌐 Head over to Referencing Values with Refs to start reading this chapter page by page!