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

React结合Socket.IO实现用户正在输入提示功能失效问题求解

问题修复方案

核心问题排查

  • 前端socket事件监听参数不匹配:后端get-typing/dur-typing事件传递的是单个对象参数{current, writer},但前端监听时拆成了(current, writer)两个独立参数接收,导致writer取值为undefined,后续逻辑无法执行
  • useEffect依赖错误:ConversationProvider的监听useEffect错误地把typing状态加入依赖,每次输入状态更新都会重新注册事件,导致重复监听、逻辑冲突
  • 事件清理逻辑错误:现有清理逻辑仅按状态判断移除部分事件,组件卸载/effect重新执行时没有无差别移除所有已注册事件,引发内存泄漏和事件异常
  • 输入组件逻辑bug:提交消息时触发的事件名写错为stop-emit,应该是stop-typing;同时recipients.push(user)会直接修改原对象,且push返回的是数组长度而非新数组,导致会话数据异常

修复代码

1. ConversationProvider 会话上下文修复

import React, {useContext, useEffect, useState, useMemo, useCallback} from 'react'
import {useSocket} from './socket'
import {Auth} from './context'

const ConversationsContext = React.createContext()

export function useConversation(){
    return useContext(ConversationsContext)
}

export default function ConversationProvider( {children} ){
    const socket = useSocket()
    const [conversations, setConversations] = useState([])
    const [currentConversation, setCurrentConversation] = useState(null)
    const [typing, setTyping] = useState([])
    const {user} = Auth()

    const addMessageToConversation = useCallback(({ current, sender, text}) => {
      setConversations(prevConversations => {
          let madeChange = false
          const newMessage = { sender, text }
          const newConversations = prevConversations.map(conversation => {
            if( conversation._id === current._id ){
              madeChange = true
              return {
                ...conversation,
                messages: [...conversation.messages, newMessage]
              }
            }
            return conversation
          })
          if (madeChange) {
            return newConversations
          } else {
            return [
              ...prevConversations,
              { ...current, messages: [newMessage] }
            ]
          }
        })
      }, [setConversations])

    // 移除typing依赖,避免重复注册事件
    useEffect(() => {
        if (socket == null) return
        // 接收参数改为单个对象解构
        socket.on('receive-message', addMessageToConversation)
        socket.on('get-typing', ({current, writer})=>{
          console.log("get-typing", current, writer);
          setTyping( prev=>{
            let ok = false;
            const nw = prev.map( i=>{
              if( i.current._id === current._id ){
                ok = true;
                return {current, writers: [...i.writers, writer]}
              }
              else return i;
            } )
            if(ok) return nw;
            else return [...prev, {current, writers: [writer]}]
          })
        })
        socket.on('dur-typing', ({current, writer})=>{
          console.log("dur-typing", current, writer);
          setTyping( prev=>{
            const nw = prev.map( i=>{
              if( i.current._id === current._id ){
                const wr =  i.writers.filter( t => t._id !== writer._id );
                return {current, writers: wr }
              }
              else return i;
            } )
            return nw;
          })
        })
        // 无差别移除所有注册的事件,不需要状态判断
        return () =>{
          socket.off('receive-message', addMessageToConversation)
          socket.off('get-typing')
          socket.off('dur-typing')
        }
    }, [socket, addMessageToConversation])

    function sendMessage(recipients, current, text) {
        socket.emit('send-message', { recipients, sender: user, current, text }, ({err})=>{
          if(err) alert(" Try again! ")
        });
        addMessageToConversation({ current, sender:user, text })
    }

    function rnd_arr(arr) {
        var currentIndex = arr.length,  randomIndex;
        while (currentIndex !== 0) {
          randomIndex = Math.floor(Math.random() * currentIndex);
          currentIndex--;
          [arr[currentIndex], arr[randomIndex]] = [arr[randomIndex], arr[currentIndex]];
        }
        return arr;
    }

    const formatted = useMemo ( ()=> conversations.map( i=>{
        const { recipients, groupName, ...other } = i;
        const all = recipients.filter( t=> t._id !== user._id )
        const logos = rnd_arr(all)
        let l = [], name = ""
        l.push( logos[0].logo )
        name = logos[0].username
        if( logos.length > 1 ){
            l.push(logos[1].logo)
            if( groupName.length > 0 ) name = groupName
            else name += ',...'
        }
        return {recipients: all, name, logo: l, isGroup: all.length > 1,groupName, isAdmin: i.admins.includes(user._id), ...other }
    } ), [conversations])
    
    const t = useMemo( ()=>{
      if( currentConversation === null ) return null;
      else  return formatted[formatted.findIndex( t => t._id === currentConversation._id )]
    }, [formatted,currentConversation] )

    const value = {
        setConversations,
        conversations: formatted,
        setCurrentConversation,
        currentConversation: t,
        sendMessage,
        typing
    }

    return (
        <ConversationsContext.Provider value={value}>
            { children }
        </ConversationsContext.Provider>
    )
}

2. Bottom 输入组件修复

import {Input, Button } from 'antd'
import { useConversation } from "../../conversation";
import { SendOutlined } from '@ant-design/icons'
import {Auth} from '../../context'
import {useState, useEffect} from 'react'
import {useSocket} from '../../socket'

export default function Bottom(){
    const [text, setText] = useState('')
    const [ok, setok] = useState(false)
    const {sendMessage, currentConversation} = useConversation()
    const {user} = Auth()
    const socket = useSocket()
    useEffect( ()=>{
      if(text.length > 0 && !ok ) { 
        socket.emit('send-typing', {currentConversation, user })
        setok(true)
      }
      if(text.length === 0 && ok){
        socket.emit('stop-typing', {currentConversation, user})
        setok(false)
      }  
    },[text, ok, socket, currentConversation, user] )

    function handleSubmit() {
        const {recipients, ...other} = currentConversation;
        // 修复recipients处理逻辑,不要直接修改原对象
        sendMessage(
            currentConversation.recipients.map(r => r._id),
            {recipients: [...recipients, user], ...other},
          text
        )
        setok(false)
        // 修复错误的事件名
        socket.emit('stop-typing', {currentConversation, user})
        setText('')
    }

    return (<div className="open_bottom">
          <Input.Group compact >
            <Input.TextArea placeholder="Write message" value={text} style={{ resize: 'none' }} onChange={(e)=>setText(e.target.value)} 
              onPressEnter={handleSubmit}
            />
            <Button type="primary" onClick={handleSubmit} > <SendOutlined/> </Button>
          </Input.Group>
    </div>)
}

可选优化

可以给输入检测加300ms防抖,避免用户每次输入字符都触发socket事件,减少服务端压力:

useEffect( ()=>{
  const timer = setTimeout(()=>{
    if(text.length > 0 && !ok ) { 
      socket.emit('send-typing', {currentConversation, user })
      setok(true)
    }
    if(text.length === 0 && ok){
      socket.emit('stop-typing', {currentConversation, user})
      setok(false)
    }
  }, 300)
  return () => clearTimeout(timer)
},[text, ok, socket, currentConversation, user] )

内容的提问来源于stack exchange,提问作者Azizjon Nuryagdyyev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 16:45:03