响应式副作用的生命周期

Effects 的生命周期与组件不同。组件可以挂载、更新或卸载。Effect 只能做两件事:开始同步某些内容,然后停止同步它。如果你的 Effect 依赖于随时间变化的 props 和 state,则此周期可能会发生多次。React 提供了一个 linter 规则来检查你是否正确指定了 Effect 的依赖项。这会使你的 Effect 与最新的 props 和 state 保持同步。

你将学习

  • Effect 的生命周期与组件的生命周期有何不同
  • 如何独立思考每个 Effect
  • 你的 Effect 何时需要重新同步,以及原因
  • 如何确定你的 Effect 的依赖项
  • 值的反应性意味着什么
  • 空依赖数组意味着什么
  • React 如何使用 linter 验证你的依赖项是否正确
  • 当你不同意 linter 的结果时该怎么做

Effect 的生命周期

每个 React 组件都经历相同生命周期

  • 组件在添加到屏幕时挂载
  • 组件在接收新的 props 或 state 时更新,通常是响应交互。
  • 组件在从屏幕中移除时卸载

这是一种思考组件的好方法,但不适用于 Effects。 相反,尝试独立于组件的生命周期来思考每个 Effect。Effect 描述了如何将外部系统同步到当前的 props 和 state。随着代码的变化,同步的频率也会发生变化。

为了说明这一点,考虑一下这个将你的组件连接到聊天服务器的 Effect

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

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

你的 Effect 的主体指定了如何开始同步:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

你的 Effect 返回的清理函数指定了如何停止同步:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
// ...

直观地,你可能会认为 React 会在你的组件挂载时开始同步,并在你的组件卸载时停止同步。但是,这并不是故事的全部!有时,在组件保持挂载状态时,可能还需要多次开始和停止同步

让我们来看看为什么需要这样做,何时会发生以及如何控制这种行为。

注意

有些 Effects 完全不返回清理函数。大多数情况下, 你会想要返回一个——但如果你不返回,React 会像你返回了一个空的清理函数一样工作。

为什么同步可能需要多次发生

想象一下这个 ChatRoom 组件接收一个 roomId prop,用户在下拉菜单中选择该 prop。假设用户最初选择 "general" 房间作为 roomId。你的应用显示 "general" 聊天室

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

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

UI 显示后,React 将运行你的 Effect 以开始同步。 它连接到 "general" 房间

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...

到目前为止,一切顺利。

稍后,用户在下拉菜单中选择不同的房间(例如,"travel")。首先,React 将更新 UI

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

想想接下来应该发生什么。用户看到 "travel" 是 UI 中选择的聊天室。但是,上次运行的 Effect 仍然连接到 "general" 房间。roomId prop 已更改,因此你的 Effect 之前所做的操作(连接到 "general" 房间)不再与 UI 匹配。

此时,你希望 React 执行两件事

  1. 停止与旧的 roomId 同步(断开与 "general" 房间的连接)
  2. 开始与新的 roomId 同步(连接到 "travel" 房间)

幸运的是,你已经教会 React 如何做这两件事了!你的 Effect 的主体部分指定了如何开始同步,而你的清理函数则指定了如何停止同步。React现在需要做的就是以正确的顺序和正确的 props 和 state 来调用它们。让我们看看这究竟是如何发生的。

React 如何重新同步你的 Effect

回想一下,你的 ChatRoom 组件收到了其 roomId prop 的新值。它曾经是 "general",现在它是 "travel"。React 需要重新同步你的 Effect 以重新连接你到不同的房间。

为了 停止同步,React 将调用你的 Effect 在连接到 "general" 房间后返回的清理函数。由于 roomId"general",因此清理函数断开了与 "general" 房间的连接。

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...

然后,React 将运行你在这次渲染过程中提供的 Effect。这次,roomId"travel",所以它将 开始同步"travel" 聊天室(直到它的清理函数最终也被调用)。

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...

由于这一点,你现在连接到了用户在 UI 中选择的同一个房间。灾难避免了!

每次你的组件使用不同的 roomId 重新渲染后,你的 Effect 都会重新同步。例如,假设用户将 roomId"travel" 更改为 "music"。React 将再次 停止同步你的 Effect,方法是调用其清理函数(断开与 "travel" 房间的连接)。然后它将 再次开始同步,方法是使用新的 roomId prop 运行其主体(连接到 "music" 房间)。

最后,当用户转到不同的屏幕时,ChatRoom 将卸载。现在根本不需要保持连接。React 将 最后一次停止同步你的 Effect,并断开与 "music" 聊天室的连接。

从 Effect 的角度思考

让我们回顾一下从 ChatRoom 组件的角度发生的一切。

  1. ChatRoom 使用 roomId 设置为 "general" 挂载。
  2. ChatRoom 使用 roomId 设置为 "travel" 更新。
  3. ChatRoom 使用 roomId 设置为 "music" 更新。
  4. ChatRoom 卸载。

在组件生命周期的每个这些点,你的 Effect 都做了不同的事情。

  1. 你的 Effect 连接到 "general" 房间。
  2. 你的 Effect 断开了与 "general" 房间的连接,并连接到 "travel" 房间。
  3. 你的 Effect 断开了与 "travel" 房间的连接,并连接到 "music" 房间。
  4. 你的 Effect 断开了与 "music" 房间的连接。

现在让我们从 Effect 本身角度思考一下发生了什么。

useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);

这段代码的结构可能会启发你将发生的事情视为一系列不重叠的时间段。

  1. 你的 Effect 连接到 "general" 房间(直到断开连接)。
  2. 你的 Effect 连接到 "travel" 房间(直到断开连接)。
  3. 你的 Effect 连接到 "music" 房间(直到断开连接)。

之前,你从组件的角度思考。当你从组件的角度来看时,很容易将 Effects 视为在特定时间(例如“渲染后”或“卸载前”)触发的“回调”或“生命周期事件”。这种思考方式很快就会变得复杂,因此最好避免。

相反,每次只关注一个开始/停止周期。组件是挂载、更新还是卸载并不重要。你只需要描述如何开始同步以及如何停止同步。如果你做得很好,你的 Effect 将能够承受多次启动和停止。

这可能会让你想起你在编写创建 JSX 的渲染逻辑时,不会考虑组件是挂载还是更新。你描述屏幕上应该显示什么,React 会处理其余部分。

React 如何验证你的 Effect 可以重新同步

这是一个您可以实际操作的示例。点击“打开聊天”以挂载ChatRoom组件

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');
  const [show, setShow] = 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>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

注意,当组件第一次挂载时,您会看到三条日志

  1. ✅ 连接到 "general" 房间 https://127.0.0.1:1234... (仅限开发环境)
  2. ❌ 断开与 "general" 房间的连接 https://127.0.0.1:1234. (仅限开发环境)
  3. ✅ 连接到 "general" 房间 https://127.0.0.1:1234...

前两条日志仅限开发环境。在开发环境中,React 总是会重新挂载每个组件一次。

React 通过强制其立即在开发环境中进行重新同步来验证您的 Effect 是否能够重新同步。这可能让您想起打开一扇门并额外关闭一次以检查门锁是否有效。React 在开发环境中额外启动和停止您的 Effect 一次,以检查您是否已正确实现其清理工作。

实际上,您的 Effect 重新同步的主要原因是它使用的某些数据已更改。在上面的沙盒中,更改所选聊天室。请注意,当roomId更改时,您的 Effect 会重新同步。

但是,还有一些更不寻常的情况需要重新同步。例如,尝试在聊天打开时编辑上面沙盒中的serverUrl。请注意,Effect 如何响应您对代码的编辑而重新同步。将来,React 可能会添加更多依赖于重新同步的功能。

React 如何知道需要重新同步 Effect

您可能想知道 React 如何知道在roomId更改后您的 Effect 需要重新同步。这是因为您告诉 React 其代码依赖于roomId,方法是将其包含在依赖项列表中:

function ChatRoom({ roomId }) { // The roomId prop may change over time
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// ...

以下是其工作原理

  1. 您知道roomId是一个 prop,这意味着它会随着时间的推移而发生变化。
  2. 您知道您的 Effect 读取roomId(因此其逻辑取决于以后可能更改的值)。
  3. 这就是您将其指定为 Effect 的依赖项的原因(以便在roomId更改时重新同步)。

每次组件重新渲染后,React 都会查看您传递的依赖项数组。如果数组中的任何值与您在上一次渲染期间传递的相同位置的值不同,React 将重新同步您的 Effect。

例如,如果您在初始渲染期间传递了["general"],之后您在下次渲染期间传递了["travel"],React 将比较"general""travel"。这些是不同的值(与Object.is相比),因此 React 将重新同步您的 Effect。另一方面,如果您的组件重新渲染,但roomId没有更改,则您的 Effect 将保持连接到同一房间。

每个 Effect 都代表一个单独的同步过程

不要仅仅因为此逻辑需要与您已编写的 Effect 同时运行,就将不相关的逻辑添加到您的 Effect 中。例如,假设您希望在用户访问房间时发送分析事件。您已经有了一个依赖于roomId的 Effect,因此您可能很想在那里添加分析调用

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

但想象一下,您稍后向此 Effect 添加另一个需要重新建立连接的依赖项。如果此 Effect 重新同步,它还会为同一个房间调用logVisit(roomId),而这并非您的本意。记录访问是一个与连接不同的过程。将它们编写为两个单独的 Effect

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}

代码中的每个 Effect 都应该代表一个单独且独立的同步过程。

在上面的示例中,删除一个 Effect 并不会破坏另一个 Effect 的逻辑。这是一个很好的迹象,表明它们同步不同的内容,因此将它们分开是有意义的。另一方面,如果您将一个内聚的逻辑块拆分成单独的 Effect,代码看起来可能“更干净”,但会更难以维护。这就是为什么您应该考虑这些过程是相同还是不同的,而不是代码看起来是否更干净的原因。

Effect 对响应式值做出“反应”

您的 Effect 读取两个变量(serverUrlroomId),但您只将roomId指定为依赖项

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

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

为什么serverUrl不需要作为依赖项?

这是因为serverUrl不会因重新渲染而更改。无论组件重新渲染多少次以及为什么,它始终是相同的。由于serverUrl从不更改,因此将其指定为依赖项没有意义。毕竟,依赖项只有在随着时间的推移而变化时才会发挥作用!

另一方面,roomId在重新渲染时可能不同。组件内部声明的Props、状态和其他值是响应式的,因为它们在渲染过程中计算,并参与React数据流。

如果serverUrl是一个状态变量,它将是响应式的。响应式值必须包含在依赖项中。

function ChatRoom({ roomId }) { // Props change over time
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); // State may change over time

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// ...
}

通过将serverUrl作为依赖项,可以确保Effect在它更改后重新同步。

尝试更改选定的聊天室或在此沙箱中编辑服务器URL。

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

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234');

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

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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} />
    </>
  );
}

每当更改响应式值(例如roomIdserverUrl)时,Effect都会重新连接到聊天服务器。

空依赖项的Effect意味着什么

如果将serverUrlroomId都移到组件外部会发生什么?

const serverUrl = 'https://127.0.0.1:1234';
const roomId = 'general';

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

现在,Effect的代码不使用任何响应式值,因此它的依赖项可以为空([])。

从组件的角度来看,空的[]依赖项数组意味着此Effect仅在组件挂载时连接到聊天室,并且仅在组件卸载时断开连接。(请记住,React仍然会在开发中额外重新同步一次以压力测试您的逻辑。)

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

const serverUrl = 'https://127.0.0.1:1234';
const roomId = 'general';

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

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

但是,如果您从Effect的角度考虑,则根本不需要考虑挂载和卸载。重要的是,您已经指定了Effect如何启动和停止同步。目前,它没有响应式依赖项。但是,如果您希望用户随着时间的推移更改roomIdserverUrl(它们将变成响应式的),则Effect的代码不会更改。您只需要将它们添加到依赖项中。

在组件主体中声明的所有变量都是响应式的

Props和状态并非唯一的响应式值。从它们计算出的值也是响应式的。如果props或状态发生变化,您的组件将重新渲染,并且从它们计算出的值也将发生变化。这就是为什么Effect使用组件主体中的所有变量都应该在Effect依赖项列表中的原因。

假设用户可以在下拉菜单中选择聊天服务器,但他们也可以在设置中配置默认服务器。假设您已将设置状态放在上下文中,因此您可以从该上下文中读取settings。现在,您可以根据props中的选定服务器和默认服务器计算serverUrl

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}

在此示例中,serverUrl不是prop也不是状态变量。它是一个在渲染过程中计算的常规变量。但它是在渲染过程中计算的,因此它可能会由于重新渲染而发生变化。这就是它具有响应性的原因。

组件内部的所有值(包括props、状态和组件主体中的变量)都是响应式的。任何响应式值都可能在重新渲染时发生更改,因此需要将响应式值作为Effect的依赖项。

换句话说,Effects“对”组件主体中的所有值“做出反应”。

深入探讨

全局变量或可变值可以作为依赖项吗?

可变值(包括全局变量)不是响应式的。

location.pathname这样的可变值不能作为依赖项。它是可变的,因此它可以随时在React渲染数据流之外发生变化。更改它不会触发组件的重新渲染。因此,即使您在依赖项中指定了它,React也不会知道在它更改时重新同步Effect。这也违反了React的规则,因为在渲染过程中读取可变数据(也就是计算依赖项时)会破坏渲染的纯度。相反,您应该使用useSyncExternalStore读取和订阅外部可变值。

ref.current或从中读取的内容也不能作为依赖项。useRef返回的ref对象本身可以作为依赖项,但它的current属性是故意可变的。它允许您跟踪某些内容而不会触发重新渲染。但是,由于更改它不会触发重新渲染,因此它不是响应式值,React不会知道在它更改时重新运行您的Effect。

正如您将在本页下面学到的那样,代码检查器会自动检查这些问题。

React会验证您是否已将每个响应式值指定为依赖项

如果您的代码检查器已配置为React,它将检查Effect代码使用的每个响应式值是否都声明为其依赖项。例如,这是一个代码检查错误,因为roomIdserverUrl都是响应式的。

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

function ChatRoom({ roomId }) { // roomId is reactive
  const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); // serverUrl is reactive

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Something's wrong here!

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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 错误,但实际上 React 指出的是你代码中的一个 bug。roomIdserverUrl 可能会随着时间变化,但你忘记在它们变化时重新同步你的 Effect。即使用户在 UI 中选择了不同的值,你仍然会连接到初始的 roomIdserverUrl

要修复此 bug,请按照 linter 的建议,将 roomIdserverUrl 指定为 Effect 的依赖项。

function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://127.0.0.1:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ All dependencies declared
// ...
}

在上面的沙箱中尝试此修复。验证 linter 错误是否消失,以及聊天是否在需要时重新连接。

注意

在某些情况下,即使 React 值是在组件内部声明的,React 知道该值永远不会改变。例如,useState 返回的 set 函数和 useRef 返回的 ref 对象是稳定的——它们保证不会在重新渲染时改变。稳定值不是响应式的,因此您可以将它们从列表中省略。包含它们也是允许的:它们不会改变,所以无关紧要。

如果你不想重新同步

在上一个例子中,你通过列出 roomIdserverUrl 作为依赖项来修复了 lint 错误。

但是,你也可以向 linter “证明”这些值不是响应式值,即它们不能因为重新渲染而改变。例如,如果 serverUrlroomId 不依赖于渲染并且始终具有相同的值,则可以将它们移到组件外部。现在它们不需要成为依赖项。

const serverUrl = 'https://127.0.0.1:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

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

你也可以将它们移到Effect 内部。它们不是在渲染过程中计算的,因此它们不是响应式的。

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://127.0.0.1:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Effect 是响应式代码块。当你在其中读取的值发生变化时,它们会重新同步。与仅对每次交互运行一次的事件处理程序不同,Effect 会在需要同步时运行。

你不能“选择”你的依赖项。你的依赖项必须包含你在 Effect 中读取的每个响应式值。linter 会强制执行此操作。有时这可能会导致无限循环等问题,并导致你的 Effect 过于频繁地重新同步。不要通过抑制 linter 来解决这些问题!以下是一些替代方法:

陷阱

linter 是你的朋友,但它的能力有限。linter 只知道依赖项何时错误。它不知道解决每个案例的最佳方法。如果 linter 建议使用依赖项,但添加它会导致循环,这并不意味着应该忽略 linter。你需要更改 Effect 内部(或外部)的代码,以便该值不是响应式的,并且不需要成为依赖项。

如果你有一个现有的代码库,你可能有一些像这样抑制 linter 的 Effect。

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

下一页,你将学习如何在不违反规则的情况下修复此代码。这总是值得修复的!

回顾

  • 组件可以挂载、更新和卸载。
  • 每个 Effect 都有一个与周围组件不同的生命周期。
  • 每个 Effect 都描述一个可以启动停止的独立同步过程。
  • 编写和读取 Effect 时,要从每个单独 Effect 的角度(如何启动和停止同步)而不是从组件的角度(如何挂载、更新或卸载)来思考。
  • 在组件主体内部声明的值是“响应式的”。
  • 响应式值应该重新同步 Effect,因为它们会随着时间而改变。
  • linter 验证 Effect 内部使用的所有响应式值是否都指定为依赖项。
  • linter 标记的所有错误都是合法的。总有一种方法可以修复代码而不违反规则。

挑战 1 5:
修复每次按键都重新连接的问题

在这个例子中,ChatRoom 组件在组件挂载时连接到聊天室,在卸载时断开连接,并在你选择不同的聊天室时重新连接。此行为是正确的,因此你需要保持其有效性。

但是,存在一个问题。每当你键入底部消息框输入时,ChatRoom 也会重新连接到聊天。(你可以通过清除控制台并在输入中键入来注意到这一点。)修复此问题,以便不会发生这种情况。

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 connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  });

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