逃生舱口

高级

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

使用 refs 引用值

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

const ref = useRef(0);

与状态一样,refs 在重新渲染之间由 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 不会追踪它。例如,你可以使用 refs 来存储超时 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 prop 值保持同步

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 一次。这就是你看到"✅ Connecting..."打印两次的原因。这确保你不会忘记实现清理函数。

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

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

阅读更多

你可能不需要 useEffect

useEffect 是 React 范式的一种规避方法。它允许你“跳出”React 并将你的组件与某些外部系统同步。如果没有涉及外部系统(例如,如果只想在某些 props 或 state 更改时更新组件的状态),则不需要 useEffect。移除不必要的 useEffect 将使你的代码更易于理解、运行速度更快且更不容易出错。

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

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

例如,你不需要 useEffect 来根据其他状态调整某些状态

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 来与外部系统同步。

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

阅读 你可能不需要 useEffect 以了解如何移除不必要的 useEffect。

阅读更多

响应式 effect 的生命周期

Effects 的生命周期与组件不同。组件可以挂载、更新或卸载。一个 Effect 只做两件事:开始同步某些东西,然后停止同步它。如果你的 Effect 依赖于随时间变化的 props 和 state,则此周期可以发生多次。

此 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

事件处理程序仅在再次执行相同的交互操作时才会重新运行。与事件处理程序不同,如果 Effect 读取的任何值(例如 props 或 state)与上次渲染时不同,则 Effects 会重新同步。有时,你希望同时拥有两种行为:一个响应某些值但并非所有值的 Effect。

Effects 内部的所有代码都是响应式的。如果它读取的某些响应式值由于重新渲染而发生了更改,它将再次运行。例如,如果roomIdtheme 发生了更改,则此 Effect 将重新连接到聊天。

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 的代码从你的 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 依赖项列表中包含 Effect 读取的每个响应式值(例如 props 和 state)。这确保你的 Effect 与组件的最新 props 和 state 保持同步。不必要的依赖项可能会导致你的 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 运行频率降低。

阅读更多

使用自定义 Hook 重复使用逻辑

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

在这个例子中,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,
    }} />
  );
}

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

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

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

阅读更多

下一步是什么?

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