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
相关产品推荐
相关产品推荐

