React/Socket.io 不显示作为 prop 传递的最新消息
React/Socket.io not displaying latest message passed down as prop
我正在使用 React 和 socket.io 开发一个聊天应用程序。后端是 express/node。相关组件是:
Room.js --> Chat.js --> Messages.js --> Message.js
从服务器接收的消息数据存储在 Room.js 的状态中。然后它通过 Chat.js 向下传递到 Messages.js,在那里它映射到一系列 Message.js 组件。
当收到消息时,它们会出现,但只有在我再次开始在表单中键入并触发 messageChangeHandler() 之后才会出现。当接收到新消息并将其添加到 Room.js 中的状态时,消息不会重新呈现的任何想法?我已经确认状态和道具在它们应该在的任何地方都在更新——它们只是 appearing/re-rendering 直到 messageChangeHandler() 触发它自己的重新渲染。
这是组件。
Room.js
export default function Room(props) {
const [messagesData, setMessagesData] = useState([])
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
let previousData = messagesData
previousData.push(data)
// buildMessages(previousData)
setMessagesData(previousData)
})
}, [socket])
console.log('this is messagesData in queue.js', messagesData)
return(
// queue counter will go up here
// <QueueDisplay />
// chat goes here
<Chat
profile={props.profile}
messagesData={messagesData}
/>
)
}
Chat.js
export default function Chat(props) {
// state
const [newPayload, setNewPayload] = useState({
message: '',
sender: props.profile.name
})
// const [messagesData, setMessagesData] = useState([])
const [updateToggle, setUpdateToggle] = useState(true)
const messageChangeHandler = (e) => {
setNewPayload({... newPayload, [e.target.name]: e.target.value})
}
const messageSend = (e) => {
e.preventDefault()
if (newPayload.message) {
socket.emit('chat message', newPayload)
setNewPayload({
message: '',
sender: props.profile.name
})
}
}
return(
<div id='chatbox'>
<div id='messages'>
<Messages messagesData={props.messagesData} />
</div>
<form onSubmit={messageSend}>
<input
type="text"
name="message"
id="message"
placeholder="Start a new message"
onChange={messageChangeHandler}
value={newPayload.message}
autoComplete='off'
/>
<input type="submit" value="Send" />
</form>
</div>
)
}
Messages.js
export default function Messages(props) {
return(
<>
{props.messagesData.map((data, i) => {
return <Message key={i} sender={data.sender} message={data.message} />
})}
</>
)
}
Message.js
export default function Message(props) {
return(
<div key={props.key}>
<p>{props.sender}</p>
<p>{props.message}</p>
</div>
)
}
提前感谢您的帮助!
更改房间中的 useEffect 以包含以下内容修复了问题:
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
// let previousData = messagesData
// previousData.push(data)
// setMessagesData(previousData)
setMessagesData(prev => prev.concat([data]))
})
}, [socket])```
我不认为你的 useEffect()
函数会像你想象的那样。
红旗
如果您看到 useEffect()
函数使用在封闭范围(在闭包中)中声明的变量,但这些变量未在 useEffect()
中列出,您的大脑应该会立即发出警告信号s依赖(useEffect()
末尾的[]
)
实际发生了什么
在这种情况下,messagesData
在 useEffect()
内部使用但未声明为依赖项。发生的情况是,在接收到第一个 broadcast
并调用 setMessagesData
之后,messagesData
在 useEffect()
中不再有效。它指的是一个数组,从上次 运行 时的闭包开始,不再分配给 messageData
。当你调用 setMessagesData
时,React 知道该值已更新,并重新渲染。它 运行 是 useState()
行并得到一个新的 messagesData
。 useEffect()
,这是一个 memoized 函数,NOT 不会被重新创建,所以它仍在使用来自先前 运行.
如何修复
清理useEffect()
在我们开始之前,让我们消除函数中的一些噪音:
useEffect(() => {
socket.on('broadcast', data => {
setMessagesData([...messagesData, data])
})
}, [socket])
这在功能上等同于您的代码,减去 console.log()
消息和额外变量。
让我们更进一步,将处理程序变成单行程序:
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket])
添加缺少的依赖项
现在,让我们添加缺少的依赖项!
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket, messagesData])
从技术上讲,我们也依赖于 setMessagesData()
,但是 React has this to say about setState()
functions:
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
厨师太多
useEffect()
功能看起来不错,但我们仍然依赖 messagesData
。这是一个问题,因为每次socket
收到一个broadcast
,messagesData
变化,所以useEffect()
是重新运行。每次re-运行,都会为broadcast
个消息添加一个新的handler/listener,也就是说在收到下一条消息时,每handler/listener调用setMessagesData()
.代码可能仍然意外地工作,至少在逻辑上是这样,因为监听器通常是按照注册的顺序同步调用的,我相信如果在同一个渲染期间进行多次 setState()
调用,React仅使用最终 setState()
调用重新渲染一次。但这肯定是内存泄漏,因为我们无法注销所有这些侦听器。
这个小问题通常最终会成为一个巨大的难题,因为要解决这个问题,我们需要在每次注册新监听器时注销旧监听器。要取消注册一个监听器,我们调用 removeListener()
函数,使用我们注册的相同函数——但我们不再有那个函数了。这意味着我们需要将旧函数保存为状态或记忆它,但现在我们的 useEffect()
函数也有另一个依赖项。事实证明,避免无限重新渲染的连续循环并非易事。
诀窍
事实证明,我们不必跳过所有这些障碍。如果我们仔细观察我们的 useEffect()
函数,我们可以看到我们实际上并没有使用 messagesData
,除了设置新值。我们正在获取旧值并附加到它。
React 开发人员知道这是一个常见的场景,因此实际上有一个内置的帮助程序。 setState()
可以接受一个函数,该函数将立即以先前的值作为参数调用。此函数的结果将是新状态。听起来比实际更复杂,但它看起来像这样:
setState(previous => previous + 1);
或者在我们的具体情况下:
setMessagesData(oldMessagesData => [...oldMessagesData, data]);
现在我们不再依赖 messagesData
:
useEffect(() => {
socket.on('broadcast', data => setMessagesData(oldMessagesData => [...oldMessagesData, data]);
}, [socket])
有礼貌
还记得我们之前讨论过内存泄漏吗?事实证明,我们最新的代码仍然会发生这种情况。该组件可能会被多次挂载和卸载(例如,在单页应用程序中,当用户切换页面时)。每次发生这种情况时,都会注册一个新的侦听器。礼貌的做法是让 useEffect()
return 一个函数来清理。在我们的例子中,这意味着 unregistering/removing 监听器。
首先,在注册之前保存监听器,然后return一个函数来删除它
useEffect(() => {
const listener = data => setMessagesData(oldMessagesData => [...oldMessagesData, data];
socket.on('broadcast', listener);
return () => socket.removeListener('broadcast', listener);
}, [socket])
请注意,如果 socket
更改,我们的侦听器仍将悬空,并且由于在代码中不清楚 socket
的来源,无论更改还必须删除所有旧侦听器,例如socket.removeAllListeners()
或 socket.removeAllListeners('broadcast')
.
我正在使用 React 和 socket.io 开发一个聊天应用程序。后端是 express/node。相关组件是: Room.js --> Chat.js --> Messages.js --> Message.js
从服务器接收的消息数据存储在 Room.js 的状态中。然后它通过 Chat.js 向下传递到 Messages.js,在那里它映射到一系列 Message.js 组件。
当收到消息时,它们会出现,但只有在我再次开始在表单中键入并触发 messageChangeHandler() 之后才会出现。当接收到新消息并将其添加到 Room.js 中的状态时,消息不会重新呈现的任何想法?我已经确认状态和道具在它们应该在的任何地方都在更新——它们只是 appearing/re-rendering 直到 messageChangeHandler() 触发它自己的重新渲染。
这是组件。
Room.js
export default function Room(props) {
const [messagesData, setMessagesData] = useState([])
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
let previousData = messagesData
previousData.push(data)
// buildMessages(previousData)
setMessagesData(previousData)
})
}, [socket])
console.log('this is messagesData in queue.js', messagesData)
return(
// queue counter will go up here
// <QueueDisplay />
// chat goes here
<Chat
profile={props.profile}
messagesData={messagesData}
/>
)
}
Chat.js
export default function Chat(props) {
// state
const [newPayload, setNewPayload] = useState({
message: '',
sender: props.profile.name
})
// const [messagesData, setMessagesData] = useState([])
const [updateToggle, setUpdateToggle] = useState(true)
const messageChangeHandler = (e) => {
setNewPayload({... newPayload, [e.target.name]: e.target.value})
}
const messageSend = (e) => {
e.preventDefault()
if (newPayload.message) {
socket.emit('chat message', newPayload)
setNewPayload({
message: '',
sender: props.profile.name
})
}
}
return(
<div id='chatbox'>
<div id='messages'>
<Messages messagesData={props.messagesData} />
</div>
<form onSubmit={messageSend}>
<input
type="text"
name="message"
id="message"
placeholder="Start a new message"
onChange={messageChangeHandler}
value={newPayload.message}
autoComplete='off'
/>
<input type="submit" value="Send" />
</form>
</div>
)
}
Messages.js
export default function Messages(props) {
return(
<>
{props.messagesData.map((data, i) => {
return <Message key={i} sender={data.sender} message={data.message} />
})}
</>
)
}
Message.js
export default function Message(props) {
return(
<div key={props.key}>
<p>{props.sender}</p>
<p>{props.message}</p>
</div>
)
}
提前感谢您的帮助!
更改房间中的 useEffect 以包含以下内容修复了问题:
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
// let previousData = messagesData
// previousData.push(data)
// setMessagesData(previousData)
setMessagesData(prev => prev.concat([data]))
})
}, [socket])```
我不认为你的 useEffect()
函数会像你想象的那样。
红旗
如果您看到 useEffect()
函数使用在封闭范围(在闭包中)中声明的变量,但这些变量未在 useEffect()
中列出,您的大脑应该会立即发出警告信号s依赖(useEffect()
末尾的[]
)
实际发生了什么
在这种情况下,messagesData
在 useEffect()
内部使用但未声明为依赖项。发生的情况是,在接收到第一个 broadcast
并调用 setMessagesData
之后,messagesData
在 useEffect()
中不再有效。它指的是一个数组,从上次 运行 时的闭包开始,不再分配给 messageData
。当你调用 setMessagesData
时,React 知道该值已更新,并重新渲染。它 运行 是 useState()
行并得到一个新的 messagesData
。 useEffect()
,这是一个 memoized 函数,NOT 不会被重新创建,所以它仍在使用来自先前 运行.
如何修复
清理useEffect()
在我们开始之前,让我们消除函数中的一些噪音:
useEffect(() => {
socket.on('broadcast', data => {
setMessagesData([...messagesData, data])
})
}, [socket])
这在功能上等同于您的代码,减去 console.log()
消息和额外变量。
让我们更进一步,将处理程序变成单行程序:
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket])
添加缺少的依赖项
现在,让我们添加缺少的依赖项!
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket, messagesData])
从技术上讲,我们也依赖于 setMessagesData()
,但是 React has this to say about setState()
functions:
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
厨师太多
useEffect()
功能看起来不错,但我们仍然依赖 messagesData
。这是一个问题,因为每次socket
收到一个broadcast
,messagesData
变化,所以useEffect()
是重新运行。每次re-运行,都会为broadcast
个消息添加一个新的handler/listener,也就是说在收到下一条消息时,每handler/listener调用setMessagesData()
.代码可能仍然意外地工作,至少在逻辑上是这样,因为监听器通常是按照注册的顺序同步调用的,我相信如果在同一个渲染期间进行多次 setState()
调用,React仅使用最终 setState()
调用重新渲染一次。但这肯定是内存泄漏,因为我们无法注销所有这些侦听器。
这个小问题通常最终会成为一个巨大的难题,因为要解决这个问题,我们需要在每次注册新监听器时注销旧监听器。要取消注册一个监听器,我们调用 removeListener()
函数,使用我们注册的相同函数——但我们不再有那个函数了。这意味着我们需要将旧函数保存为状态或记忆它,但现在我们的 useEffect()
函数也有另一个依赖项。事实证明,避免无限重新渲染的连续循环并非易事。
诀窍
事实证明,我们不必跳过所有这些障碍。如果我们仔细观察我们的 useEffect()
函数,我们可以看到我们实际上并没有使用 messagesData
,除了设置新值。我们正在获取旧值并附加到它。
React 开发人员知道这是一个常见的场景,因此实际上有一个内置的帮助程序。 setState()
可以接受一个函数,该函数将立即以先前的值作为参数调用。此函数的结果将是新状态。听起来比实际更复杂,但它看起来像这样:
setState(previous => previous + 1);
或者在我们的具体情况下:
setMessagesData(oldMessagesData => [...oldMessagesData, data]);
现在我们不再依赖 messagesData
:
useEffect(() => {
socket.on('broadcast', data => setMessagesData(oldMessagesData => [...oldMessagesData, data]);
}, [socket])
有礼貌
还记得我们之前讨论过内存泄漏吗?事实证明,我们最新的代码仍然会发生这种情况。该组件可能会被多次挂载和卸载(例如,在单页应用程序中,当用户切换页面时)。每次发生这种情况时,都会注册一个新的侦听器。礼貌的做法是让 useEffect()
return 一个函数来清理。在我们的例子中,这意味着 unregistering/removing 监听器。
首先,在注册之前保存监听器,然后return一个函数来删除它
useEffect(() => {
const listener = data => setMessagesData(oldMessagesData => [...oldMessagesData, data];
socket.on('broadcast', listener);
return () => socket.removeListener('broadcast', listener);
}, [socket])
请注意,如果 socket
更改,我们的侦听器仍将悬空,并且由于在代码中不清楚 socket
的来源,无论更改还必须删除所有旧侦听器,例如socket.removeAllListeners()
或 socket.removeAllListeners('broadcast')
.