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()末尾的[]

实际发生了什么

在这种情况下,messagesDatauseEffect() 内部使用但未声明为依赖项。发生的情况是,在接收到第一个 broadcast 并调用 setMessagesData 之后,messagesDatauseEffect() 中不再有效。它指的是一个数组,从上次 运行 时的闭包开始,不再分配给 messageData。当你调用 setMessagesData 时,React 知道该值已更新,并重新渲染。它 运行 是 useState() 行并得到一个新的 messagesDatauseEffect(),这是一个 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收到一个broadcastmessagesData变化,所以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').