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 会在你的组件挂载时开始同步,并在你的组件卸载时停止同步。但是,这并不是故事的全部!有时,在组件保持挂载状态时,可能还需要多次开始和停止同步。
让我们来看看为什么需要这样做,何时会发生以及如何控制这种行为。
为什么同步可能需要多次发生
想象一下这个 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 执行两件事
- 停止与旧的
roomId
同步(断开与"general"
房间的连接) - 开始与新的
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
组件的角度发生的一切。
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} />} </> ); }
注意,当组件第一次挂载时,您会看到三条日志
✅ 连接到 "general" 房间 https://127.0.0.1:1234...
(仅限开发环境)❌ 断开与 "general" 房间的连接 https://127.0.0.1:1234.
(仅限开发环境)✅ 连接到 "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
// ...
以下是其工作原理
- 您知道
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、状态和其他值是响应式的,因为它们在渲染过程中计算,并参与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的代码不会更改。您只需要将它们添加到依赖项中。
在组件主体中声明的所有变量都是响应式的
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代码使用的每个响应式值是否都声明为其依赖项。例如,这是一个代码检查错误,因为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 指出的是你代码中的一个 bug。roomId
和 serverUrl
可能会随着时间变化,但你忘记在它们变化时重新同步你的 Effect。即使用户在 UI 中选择了不同的值,你仍然会连接到初始的 roomId
和 serverUrl
。
要修复此 bug,请按照 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} /> </> ); }