逃生舱口

高级

一些组件可能需要控制和同步 React 外部的系统。例如,你可能需要使用浏览器 API 聚焦一个输入框,播放和暂停一个非 React 实现的视频播放器,或者连接到远程服务器并监听消息。在本章中,你将学习一些“逃生舱口”,这些舱口允许你“走出” React 并连接到外部系统。你的大多数应用程序逻辑和数据流不应该依赖于这些功能。

使用 Refs 引用值

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

const ref = useRef(0);

与状态类似,ref 在 React 的多次渲染之间都会保留。但是,设置状态会重新渲染组件。更改 ref 不会!你可以通过ref.current 属性访问该 ref 的当前值。

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 不会跟踪它。例如,你可以使用 ref 来存储超时 IDDOM 元素和其他不会影响组件渲染输出的对象。

准备学习这个主题吗?

阅读 使用 Refs 引用值 以了解如何使用 refs 来记住信息。

阅读更多

使用 Refs 操作 DOM

React 会自动更新 DOM 以匹配你的渲染输出,因此你的组件通常不需要操作它。但是,有时你可能需要访问 React 管理的 DOM 元素——例如,聚焦一个节点、滚动到它,或者测量它的尺寸和位置。在 React 中没有内置的方法来执行这些操作,因此你需要一个指向 DOM 节点的 ref。例如,点击按钮将使用 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 元素。

阅读更多

与 Effects 同步

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

按播放/暂停几次,看看视频播放器如何始终与isPlaying 属性值同步。

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

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

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 一次。这就是为什么你看到"✅ 连接中..." 打印两次。这确保了你不会忘记实现清理函数。

准备学习这个主题吗?

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

阅读更多

你可能不需要 Effect

Effects 是 React 范式的一种“逃生舱口”。它们允许你“走出” React 并将你的组件与某个外部系统同步。如果没有涉及外部系统(例如,如果你希望在某些属性或状态发生变化时更新组件的状态),你就不需要 Effect。删除不必要的 Effects 将使你的代码更容易理解、运行更快、错误更少。

有两种常见的情况,你不需要使用 Effects

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

例如,你不必使用 Effect 来根据其他状态调整某个状态

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

相反,在渲染时尽可能多地进行计算。

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

但是,您 *确实* 需要 Effects 与外部系统同步。

准备学习这个主题吗?

阅读 您可能不需要 Effect 以了解如何删除不必要的 Effects。

阅读更多

反应式 Effects 的生命周期

Effects 具有与组件不同的生命周期。组件可以挂载、更新或卸载。Effect 只能做两件事:开始同步某件事,然后停止同步它。如果您的 Effect 依赖于随时间变化的 props 和状态,此循环可能会发生多次。

此 Effect 依赖于 roomId prop 的值。Props 是 *反应式值*,这意味着它们可以在重新渲染时发生变化。请注意,如果 roomId 发生变化,Effect 会 *重新同步*(并重新连接到服务器)。

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

const serverUrl = 'https://127.0.0.1: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 会自动找到该错误。

准备学习这个主题吗?

阅读 反应式事件的生命周期 以了解 Effect 的生命周期如何与组件的生命周期不同。

阅读更多

将事件与 Effects 分开

正在建设中

本节描述了 尚未在 React 的稳定版本中发布的实验性 API

事件处理程序仅在您再次执行相同的交互时才重新运行。与事件处理程序不同,如果 Effects 读取的任何值(如 props 或状态)与上次渲染时不同,Effects 会重新同步。有时,您希望同时拥有两种行为:Effect 会响应某些值但不会响应其他值的重新运行。

Effects 中的所有代码都是 *反应式* 的。如果它读取的某个反应式值由于重新渲染而发生了变化,它将再次运行。例如,此 Effect 将重新连接到聊天,前提是 roomIdtheme 发生了变化

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

const serverUrl = 'https://127.0.0.1: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 事件* 中。

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

const serverUrl = 'https://127.0.0.1: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 重新连接。

准备学习这个主题吗?

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

阅读更多

删除 Effect 依赖项

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

例如,此 Effect 依赖于 options 对象,该对象在您每次编辑输入时都会重新创建。

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

const serverUrl = 'https://127.0.0.1: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 字符串。

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

const serverUrl = 'https://127.0.0.1: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 代码使用的所有反应式值的列表。您不会有意选择要放在该列表中的内容。该列表描述了您的代码。要更改依赖项列表,请更改代码。

准备学习这个主题吗?

阅读 删除 Effect 依赖项 以了解如何使您的 Effect 运行频率降低。

阅读更多

使用自定义 Hooks 重用逻辑

React 带有内置的 Hooks,如 useStateuseContextuseEffect。有时,您会希望有一个用于更特定目的的 Hook:例如,用于获取数据、跟踪用户是否在线或连接到聊天室。为此,您可以根据应用程序的需要创建自己的 Hooks。

在此示例中,usePointerPosition 自定义 Hook 跟踪光标位置,而 useDelayedValue 自定义 Hook 返回一个值,该值在您传递的值“落后”一定毫秒。将光标移到沙盒预览区域,以查看跟随光标的移动点迹。

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、将它们组合在一起、在它们之间传递数据并在组件之间重用它们。随着应用程序的增长,您将减少手动编写 Effects,因为您可以重用已编写的自定义 Hooks。React 社区还维护着许多出色的自定义 Hooks。

准备学习这个主题吗?

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

阅读更多

下一步?

前往 使用 Refs 引用值,逐页阅读本章节内容!