You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React聊天应用中如何实现消息已读状态功能?

完善 React 聊天程序消息已读状态实现方案

一、后端代码修正与增强

首先修复后端的拼写错误,并补充消息已读状态同步给发送方的逻辑:

// 存储用户与socket的映射,用于向发送方推送已读状态
const userSocketMap = new Map();

// 用户连接时记录socketId
socket.on('user_connected', (userId) => {
  userSocketMap.set(userId, socket.id);
});

socket.on("read_message", async({message_id})=>{
    const updated_message = await Message.findByIdAndUpdate(
      message_id, 
      {is_read: true}, 
      {new: true}
    );
    if(updated_message){
      // 向当前用户返回更新后的消息
      socket.emit("read_message_success", {
        status: 'success',
        message: updated_message
      });
      
      // 向消息发送方推送已读状态
      const senderSocketId = userSocketMap.get(updated_message.from);
      if(senderSocketId){
        io.to(senderSocketId).emit("message_marked_read", { 
          message_id: updated_message._id 
        });
      }
    }
  })

二、前端组件优化

1. 父组件(聊天页面)状态管理与监听

在持有消息列表状态的父组件中统一处理socket监听和状态更新:

import { useState, useEffect } from 'react';
import MessageContainer from './MessageContainer';

const ChatPage = () => {
  const [messages, setMessages] = useState([]);
  const currentUserId = window.localStorage.getItem('user_id');

  useEffect(() => {
    // 监听自己标记消息已读的成功回调
    socket.on("read_message_success", ({ message }) => {
      setMessages(prev => 
        prev.map(msg => 
          msg._id === message._id ? {...msg, is_read: true} : msg
        )
      );
    });

    // 监听对方标记自己发送的消息已读的通知
    socket.on("message_marked_read", ({ message_id }) => {
      setMessages(prev => 
        prev.map(msg => 
          msg._id === message_id ? {...msg, is_read: true} : msg
        )
      );
    });

    // 清理监听
    return () => {
      socket.off("read_message_success");
      socket.off("message_marked_read");
    };
  }, []);

  // 处理消息进入视口的逻辑
  const handleMessageVisible = (messageId) => {
    const targetMsg = messages.find(msg => msg._id === messageId);
    if(targetMsg && !targetMsg.is_read && targetMsg.from !== currentUserId){
      socket.emit('read_message', { message_id: messageId });
    }
  };

  return (
    <div className="chat-container">
      <MessageContainer 
        messages={messages} 
        onMessageVisible={handleMessageVisible}
      />
      {/* 消息输入框等其他组件 */}
    </div>
  );
};

export default ChatPage;

2. MessageContainer 组件:使用交叉观察器检测可见性

替换原有的自动滚动逻辑,添加交叉观察器检测消息是否进入视口:

import { useEffect, useRef } from 'react';
import Message from './Message';
import NoMessage from './NoMessage';

const MessageContainer = ({ messages, onMessageVisible }) => {
  const messageRef = useRef(null);
  const observerRef = useRef(null);

  // 自动滚动到底部
  useEffect(() => {
    const domNode = messageRef.current;
    if (domNode) {
      domNode.scrollTop = domNode.scrollHeight;
    }
  }, [messages.length]);

  // 初始化交叉观察器,检测消息可见性
  useEffect(() => {
    observerRef.current = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if(entry.isIntersecting){
            const messageId = entry.target.dataset.messageId;
            onMessageVisible(messageId);
            // 标记已读后停止观察该消息
            observerRef.current.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.5 } // 消息50%可见时触发
    );

    // 观察所有消息元素
    const messageElements = messageRef.current?.querySelectorAll('[data-message-id]');
    messageElements?.forEach(el => observerRef.current.observe(el));

    return () => {
      observerRef.current?.disconnect();
    };
  }, [messages, onMessageVisible]);

  return (
    <div
      id="messages"
      className="scroll-smooth flex flex-col h-full space-y-4 p-3 overflow-y-auto scrollbar-thumb-blue scrollbar-thumb-rounded scrollbar-track-purple scrollbar-thin scrolling-touch"
      ref={messageRef}
    >
      {messages.length > 0 ? (
        messages.map((message) => (
          <Message message={message} key={message._id} />
        ))
      ) : (
        <NoMessage />
      )}
    </div>
  );
};

export default MessageContainer;

3. Message 组件:简化逻辑并添加数据属性

移除原有的 useEffect 监听,添加用于检测的 data 属性:

import React from 'react';
import OutcomingMessage from './OutcomingMessage';
import IncomingMessage from './IncomingMessage';

const Message = ({ message }) => {
  const isMe = window.localStorage.getItem('user_id') === message.from;

  return isMe ? (
    <OutcomingMessage 
      message={message} 
      data-message-id={message._id} 
    />
  ) : (
    <IncomingMessage 
      message={message} 
      data-message-id={message._id} 
    />
  );
};

// 确保子组件将data属性传递到根节点
// 示例:OutcomingMessage组件
// const OutcomingMessage = ({ message, ...props }) => (
//   <div {...props} className="outcoming-message">
//     {/* 消息内容 */}
//   </div>
// );

export default React.memo(Message);

核心改进点说明

  1. 修正后端拼写错误:将 road_message 改为正确的 read_message_success
  2. 基于视口可见性标记已读:使用交叉观察器,只有当消息进入用户视口才标记为已读,符合真实阅读场景
  3. 统一socket监听管理:在父组件集中处理socket事件,避免重复添加监听导致的内存泄漏
  4. 双向同步已读状态:消息接收方标记已读后,发送方也能实时看到消息已读状态
  5. 优化key值:使用消息ID作为列表项key,避免index导致的渲染异常

内容的提问来源于stack exchange,提问作者hamid reza

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 19:47:20