Effect 的生命周期与组件不同。组件可以挂载、更新或卸载。而 Effect 只能做两件事:开始同步某些内容,以及稍后停止同步。如果你的 Effect 依赖于随时间变化的 props 和 state,则此循环可能会发生多次。React 提供了一个 linter 规则来检查你是否正确指定了 Effect 的依赖项。这可以使你的 Effect 与最新的 props 和 state 保持同步。
你将学习
- Effect 的生命周期与组件的生命周期有何不同
- 如何独立思考每个 Effect
- 何时以及为何需要重新同步 Effect
- 如何确定 Effect 的依赖项
- 值的响应式意味着什么
- 空的依赖项数组意味着什么
- React 如何使用 linter 验证你的依赖项是否正确
- 当你不同意 linter 的提示时该怎么办
Effect 的生命周期
每个 React 组件都经历相同的生命周期
- 组件在添加到屏幕时挂载。
- 组件在接收到新的 props 或 state 时更新,通常是为了响应交互。
- 组件在从屏幕中移除时卸载。
用这种方式思考组件很好,但不适合思考 Effect。 相反,请尝试将每个 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 会在组件挂载时开始同步,在组件卸载时停止同步。然而,这并不是全部!有时,在组件保持挂载状态时,也可能需要多次启动和停止同步。
让我们来看看为什么需要这样做,何时会发生,以及如何控制这种行为。
为什么同步可能需要进行多次
假设这个 ChatRoom
组件接收一个用户在下拉菜单中选择的 roomId
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>;
}
想想接下来应该发生什么。用户看到 UI 中选中的聊天室是 "travel"
。但是,上次运行的 Effect 仍然连接到 "general"
房间。roomId
prop 已经更改,因此你的 Effect 之前所做的操作(连接到 "general"
房间)不再与 UI 匹配。
此时,你希望 React 做两件事
- 停止与旧的
roomId
同步(断开与"general"
房间的连接) - 开始与新的
roomId
同步(连接到"travel"
房间)
幸运的是,你已经教会了 React 如何做这两件事! 你的 Effect 的主体指定了如何开始同步,而清理函数指定了如何停止同步。React 现在要做的就是以正确的顺序调用它们,并传入正确的 props 和 state。让我们看看这是如何发生的。
React 如何重新同步 Effect
回想一下,你的 ChatRoom
组件接收到了一个新的 roomId
属性值。它之前是 "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 将再次通过调用其清理函数(断开你与 "travel"
房间的连接)来停止同步你的 Effect。然后,它将使用新的 roomId
属性(将你连接到 "music"
房间)运行其主体,再次开始同步。
最后,当用户转到另一个屏幕时,ChatRoom
卸载。现在,完全没有必要保持连接了。React 将最后一次停止同步你的 Effect,并断开你与 "music"
聊天室的连接。
从 Effect 的角度思考
让我们从 ChatRoom
组件的角度来回顾一下发生的一切。
ChatRoom
挂载,roomId
设置为"general"
。ChatRoom
更新,roomId
设置为"travel"
。ChatRoom
更新,roomId
设置为"music"
。ChatRoom
卸载。
在组件生命周期的每个时间点,你的 Effect 都做了不同的事情。
- 你的 Effect 连接到了
"general"
房间。 - 你的 Effect 断开了与
"general"
房间的连接,并连接到了"travel"
房间。 - 你的 Effect 断开了与
"travel"
房间的连接,并连接到了"music"
房间。 - 你的 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]);
这段代码的结构可能会启发你将发生的事情看作一系列不重叠的时间段。
- 你的 Effect 连接到了
"general"
房间(直到断开连接)。 - 你的 Effect 连接到了
"travel"
房间(直到断开连接)。 - 你的 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} />} </> ); }
请注意,当组件第一次挂载时,你会看到三条日志。
✅ 正在连接到 https://127.0.0.1:1234 的“general”房间...
(仅限开发环境)❌ 已从 https://127.0.0.1:1234 的“general”房间断开连接。
(仅限开发环境)✅ 正在连接到 https://127.0.0.1:1234 的“general”房间...
前两个日志仅在开发环境中出现。在开发环境中,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
// ...
以下是它的工作原理
- 你知道
roomId
是一个 prop,这意味着它会随着时间的推移而发生变化。 - 你知道你的 Effect 读取了
roomId
(因此它的逻辑依赖于一个以后可能会改变的值)。 - 这就是为什么你将它指定为 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 读取了两个变量(serverUrl
和 roomId
),但你只指定了 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、state 以及在组件内部声明的其他值是*响应式*的,因为它们是在渲染期间计算的,并参与 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} /> </> ); }
每当你更改像 roomId
或 serverUrl
这样的响应式值时,Effect 都会重新连接到聊天服务器。
具有空依赖项的 Effect 的含义
如果将 serverUrl
和 roomId
都移到组件外部,会发生什么?
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 如何启动和停止同步。目前,它没有响应式依赖项。但如果你希望用户能够随时更改 roomId
或 serverUrl
(并且它们会变成响应式的),则 Effect 的代码不会改变。你只需要将它们添加到依赖项中。
组件主体中声明的所有变量都是响应式的
属性和状态并非唯一的响应式值。你根据它们计算出的值也是响应式的。如果属性或状态发生变化,你的组件将重新渲染,并且根据它们计算出的值也将发生变化。这就是为什么 Effect 使用的组件主体中的所有变量都应该包含在 Effect 依赖项列表中的原因。
假设用户可以在下拉列表中选择聊天服务器,但他们也可以在设置中配置默认服务器。假设你已经将设置状态放入 上下文 中,因此你可以从该上下文中读取 settings
。现在,你可以根据从属性中选择的服务器和默认服务器来计算 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
不是属性或状态变量。它是一个在渲染期间计算的常规变量。但它是在渲染期间计算的,因此它可能会因重新渲染而发生变化。这就是为什么它是响应式的。
组件内的所有值(包括属性、状态和组件主体中的变量)都是响应式的。任何响应式值都可以在重新渲染时发生变化,因此你需要将响应式值作为 Effect 的依赖项。
换句话说,Effects 会“响应”组件主体中的所有值。
深入探讨
可变值(包括全局变量)不是响应式的。
像 location.pathname
这样的可变值不能作为依赖项。它是可变的,因此它可以随时在 React 渲染数据流之外完全更改。更改它不会触发组件的重新渲染。因此,即使你在依赖项中指定了它,当它发生变化时,React 也*不会知道*要重新同步 Effect。这也违反了 React 的规则,因为在渲染期间读取可变数据(即计算依赖项时)会破坏渲染的纯度。相反,你应该使用 useSyncExternalStore
读取和订阅外部可变值。
像 ref.current
这样的可变值或你从中读取的内容也不能作为依赖项。useRef
返回的 ref 对象本身可以作为依赖项,但它的 current
属性是故意设计为可变的。它让你可以跟踪某些内容而不会触发重新渲染。但由于更改它不会触发重新渲染,因此它不是响应式值,并且 React 在它发生变化时不会知道要重新运行你的 Effect。
正如你将在本页下方学到的那样,linter 会自动检查这些问题。
React 会验证你是否已将每个响应式值指定为依赖项
如果你的 linter 已针对 React 进行配置,它将检查 Effect 代码使用的每个响应式值是否都已声明为其依赖项。例如,这是一个 lint 错误,因为 roomId
和 serverUrl
都是响应式的
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 正在指出你的代码中的一个错误。roomId
和 serverUrl
都可能会随着时间的推移而发生变化,但是你忘记了在它们发生变化时重新同步你的 Effect。即使在用户在 UI 中选择了不同的值之后,你仍将保持连接到初始的 roomId
和 serverUrl
。
要修复此错误,请按照 linter 的建议将 roomId
和 serverUrl
指定为 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 错误是否已消失,并且聊天在需要时是否重新连接。
当您不想重新同步时该怎么办
在前面的示例中,您通过将 roomId
和 serverUrl
列为依赖项来修复 lint 错误。
但是,您可以改为“证明”给 linter,这些值不是响应式值,也就是说,它们*不能*因重新渲染而改变。例如,如果 serverUrl
和 roomId
不依赖于渲染并且始终具有相同的值,则可以将它们移到组件外部。现在,它们不需要作为依赖项
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 来解决这些问题! 您可以尝试以下方法
-
检查您的 Effect 是否代表一个独立的同步进程。 如果您的 Effect 没有同步任何内容,它可能是多余的。 如果它同步了几个独立的内容,请将其拆分。
-
如果您想在不“响应”props 或 state 的最新值且不重新同步 Effect 的情况下读取它,可以将 Effect 拆分为响应式部分(您将在 Effect 中保留)和非响应式部分(您将提取到称为*Effect 事件*的内容中)。 阅读有关将事件与 Effect 分离的信息。
-
避免依赖对象和函数作为依赖项。 如果您在渲染期间创建对象和函数,然后从 Effect 中读取它们,则它们在每次渲染时都会不同。 这将导致您的 Effect 每次都重新同步。 详细了解如何从 Effect 中删除不必要的依赖项。
回顾
- 组件可以挂载、更新和卸载。
- 每个 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} /> </> ); }