React/Socket.io 不显示作为 prop 传递的最新消息

dev*_*opp 5 socket.io reactjs react-hooks react-state

我正在使用 React 和 socket.io 开发一个聊天应用程序。后端是express/node。相关组件是: Room.js --> Chat.js --> Messages.js --> Message.js

从服务器接收到的消息数据以状态存储在 Room.js 中。然后它通过 Chat.js 传递到 Messages.js,并在其中映射到一系列 Message.js 组件。

当收到消息时,它们才会出现,但只有在我再次开始在表单中输入并触发 messageChangeHandler() 后才会出现。有什么想法为什么当收到新消息并将其添加到 Room.js 中的状态时消息不会重新呈现?我已经确认状态和道具正在更新它们应该在的所有地方——它们只是在 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}
        />

    )
}
Run Code Online (Sandbox Code Playgroud)

聊天.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>
    )
}
Run Code Online (Sandbox Code Playgroud)

消息.js

export default function Messages(props) {
    return(
        <>
        {props.messagesData.map((data, i) => {
            return <Message key={i} sender={data.sender} message={data.message} />
        })}
        </>
    )
}
Run Code Online (Sandbox Code Playgroud)

消息.js

export default function Message(props) {
    return(
        <div key={props.key}>
            <p>{props.sender}</p>
            <p>{props.message}</p>
        </div>
    )
}
Run Code Online (Sandbox Code Playgroud)

预先感谢您的任何帮助!

Cod*_*ing 5

我不认为你的useEffect()函数做了你认为它做的事情。

\n

红旗

\n

如果您看到一个useEffect()函数使用在封闭范围(在闭包中)中声明的变量,但这些变量未在useEffect()\ 的依赖项中列出([]在 的末尾useEffect()) ,您的大脑应该立即产生一个危险信号

\n

实际发生了什么

\n

在这种情况下,messagesData在内部使用useEffect()但未声明为依赖项。发生的情况是,在第一个broadcast被接收并被setMessagesData调用之后,messagesData在 内不再有效useEffect()。它引用了一个数组,来自上次运行时的闭包,该数组不再被分配messageData。当你调用 时setMessagesData,React 知道该值已更新,并重新渲染。它运行该useState()线路并获得一个新的messagesData. useEffect(),这是一个记忆函数,不会重新创建,因此它仍然使用messagesData之前运行的函数。

\n

如何修复它

\n

清理useEffect()

\n

在开始之前,让我们消除函数中的一些噪音:

\n
    useEffect(() => {\n        socket.on(\'broadcast\', data => {\n            setMessagesData([...messagesData, data])\n        })\n    }, [socket])\n
Run Code Online (Sandbox Code Playgroud)\n

这在功能上等同于您的代码,减去console.log()消息和额外的变量。

\n

让我们更进一步,将处理程序变成单行代码:

\n
    useEffect(() => {\n        socket.on(\'broadcast\', data => setMessagesData([...messagesData, data]));\n    }, [socket])\n
Run Code Online (Sandbox Code Playgroud)\n

添加缺少的依赖项

\n

现在,让我们添加缺少的依赖项!

\n
    useEffect(() => {\n        socket.on(\'broadcast\', data => setMessagesData([...messagesData, data]));\n    }, [socket, messagesData])\n
Run Code Online (Sandbox Code Playgroud)\n

从技术上讲,我们也依赖于setMessagesData(),但是React 对于函数有这样的说法setState()

\n
\n

React 保证 setState 函数身份是稳定的,并且在重新渲染时不会改变。这就是为什么从 useEffect 或 useCallback 依赖项列表中省略它\xe2\x80\x99 是安全的。

\n
\n

厨师太多

\n

useEffect()功能看起来不错,但我们仍然依赖messagesData. 这是一个问题,因为每次socket收到一个broadcastmessagesData都会发生变化,所以useEffect()要重新运行。每次重新运行时,它都会为broadcast消息添加一个新的处理程序/侦听器,这意味着当收到下一条消息时,每个处理程序/侦听器都会调用setMessagesData(). 代码可能仍然会意外地工作,至少在逻辑上是这样,因为侦听器通常按照注册的顺序同步调用,而且我相信如果在setState()同一个渲染期间进行多次调用,React 只会重新渲染一次使用最后的setState()调用。但这肯定会发生内存泄漏,因为我们无法注销所有这些侦听器。

\n

这个小问题通常会导致解决起来非常痛苦,因为要解决这个问题,我们需要在每次注册新侦听器时取消注册旧侦听器。为了取消注册侦听器,我们使用removeListener()我们注册的相同函数调用函数 - 但我们不再有该函数了。这意味着我们需要将旧函数保存为状态或将其记忆化,但现在我们的useEffect()函数还有另一个依赖项。事实证明,避免无限重新渲染的连续循环并非易事。

\n

窍门

\n

事实证明,我们不必跨越所有这些障碍。如果仔细观察我们的函数,我们会发现除了设置新值之外,useEffect()我们实际上并没有使用。messagesData我们正在获取旧值并附加到它上面。

\n

React 开发人员知道这是一个常见的场景,因此实际上有一个内置的帮助器。setState()可以接受一个函数,该函数将立即以先前的值作为参数进行调用。该函数的结果将是新的状态。听起来比实际更复杂,但它看起来像这样:

\n
setState(previous => previous + 1);\n
Run Code Online (Sandbox Code Playgroud)\n

或者在我们的具体情况下:

\n
setMessagesData(oldMessagesData => [...oldMessagesData, data]);\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们不再依赖于messagesData

\n
    useEffect(() => {\n        socket.on(\'broadcast\', data => setMessagesData(oldMessagesData => [...oldMessagesData, data]);\n    }, [socket])\n
Run Code Online (Sandbox Code Playgroud)\n

有礼貌

\n

还记得之前我们讨论过内存泄漏吗?事实证明,使用我们最新的代码仍然会发生这种情况。该组件可能会多次安装和卸载(例如,在单页应用程序中,当用户切换页面时)。每次发生这种情况时,都会注册一个新的侦听器。礼貌的做法是返回useEffect()一个可以清理的函数。在我们的例子中,这意味着取消注册/删除监听器。

\n

首先,在注册之前保存侦听器,然后返回一个函数将其删除

\n
    useEffect(() => {\n        const listener = data => setMessagesData(oldMessagesData => [...oldMessagesData, data];\n        socket.on(\'broadcast\', listener);\n        return () => socket.removeListener(\'broadcast\', listener);\n    }, [socket])\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,如果发生更改,我们的侦听器仍然会悬空socket,并且由于代码中不清楚其socket来源,因此无论发生什么更改,都必须删除所有旧侦听器,例如socket.removeAllListeners()socket.removeAllListeners(\'broadcast\')

\n