Estoy creando una aplicación con una función de chat usando express.js, react.js y socket.io. Aquí hay una versión mínima con el error reproducido en Github .
El problema es que el socket.on() del servidor siempre se dispara dos veces, sin ninguna razón aparente para mí.
Por lo que puedo decir, el cliente solo envía un único .emit , pero el .on del servidor siempre se dispara dos veces (y estoy bastante seguro de que el controlador de eventos no está vinculado dos veces), independientemente de cuántos clientes estén conectados, o de hecho, cómo he refactorizado el código.
El formulario del cliente que envía (entrada + botón) agrega el mensaje a su lista de mensajes locales y luego envía MESSAGE_NEW al reductor.
Aquí está primero el archivo Socket.js .
import React from 'react' import socketio from 'socket.io-client' export const socket = socketio.connect('ws://localhost:3001') export const SocketContext = React.createContext() Y aquí está el código del controlador de envío de Chat.js (esto parece funcionar correctamente, se envía solo una vez):
const handleSubmit = e => { e.preventDefault() if (message === '') return setMessages(messages => [...messages, message]) dispatch({ type: 'MESSAGE_NEW', payload: message }) setMessage('') } Aquí está /Reducer.js , que .emit es la señal (también parece funcionar bien, disparando solo una vez).
import { useContext } from 'react' import { SocketContext } from './Socket' export default function Reducer(state, action) { const socket = useContext(SocketContext) switch (action.type) { case 'MESSAGE_NEW': // this only fires once, as it should console.log('emitting socket') socket.emit('message:new', action.payload) break default: return state } } Pero aquí está el bit relevante de /server/index.js , aquí es donde las cosas van mal. No importa lo que haya intentado, el .on siempre se dispara dos veces.
io.on('connection', socket => { console.log('New client connected:', socket.id) socket.on('message:new', message => { // this always fires twice, i do not understand why console.log('message:new: ', message, socket.id) socket.broadcast.emit('message:new', message) }) }) Actualización: descubrí que mover socket.emit de Reducer.js a handleSubmit en Chat.js hace que funcione bien. No entiendo por qué, ya que el Reductor no dispara dos veces.
En https://github.com/Heilemann/sockettest/blob/c1df22349ac1877ed625a01b14f48730b7af122d/client/src/Chat.jsx#L14-L22 , agrega un detector de eventos y luego intenta eliminarlo nuevamente. Pero como escribes la función del controlador
message => { setMessages([...messages, message]) } cada vez, la eliminación no tiene efecto, ya que el controlador que se eliminará no es idéntico al que agregó. Debe asignar la función de controlador a una variable y usar esa variable en .on y .off .
Incluso si esto no soluciona el problema, vale la pena hacerlo.
Su componente se está renderizando dos veces, llamando al socket dos veces y dando dos mensajes de conexión.
Asegúrese de que se renderice una vez usando su useEffect como este
En tu chat.js importa el socket.io
import socketio from 'socket.io-client' let socket useEffect(() => { socket = socketio.connect('ws://localhost:3001') return () => { socket.off('message:new', listener) } },[])En su controlador de envío
const handleSubmit = e => { e.preventDefault() if (message === '') return setMessages(messages => [...messages, message]) socket.emit('message:new', message) //dispatch({ type: 'MESSAGE_NEW', payload: message }) setMessage('') }Pasar una matriz vacía como segundo argumento significa que useEffect solo se llamará una vez. La conclusión es asegurarse de que el cliente socket.io se llame una vez. props y los cambios de estado hacen que el componente se vuelva a renderizar, lo que a su vez hace que el zócalo se conecte varias veces. Puedes explorar otros patrones más limpios. Sin embargo, esto es lo que tienes que hacer.
Según la documentación de React en modo estricto :
El modo estricto no puede detectar automáticamente los efectos secundarios, pero puede ayudarlo a detectarlos haciéndolos un poco más deterministas. Esto se hace mediante una doble invocación intencional de las siguientes funciones:
- Métodos
constructorde componentes de clase,renderyshouldComponentUpdate- Componente de clase método estático
getDerivedStateFromProps- Cuerpos de componentes de función
- Funciones de actualización de estado (el primer argumento de
setState)- Funciones pasadas a
useState,useMemoouseReducer
Observe la última línea: las funciones que se pasan a useReducer se invocarán dos veces; es por eso que su mensaje se envía dos veces. Puede confirmar esto actualizando su reductor así:
case "MESSAGE_NEW": alert("I'm running"); Notará que esta alerta aparece dos veces, gracias a React.StrictMode .
Si elimina el envoltorio <React.StrictMode> en src/App.js , el mensaje de test ya no se repetirá; sin embargo, no está solucionando el problema subyacente aquí.
El problema subyacente es que está haciendo que su reductor (que debería ser puro) realice un efecto secundario impuro (en este caso, enviando un mensaje a través de un websocket). Además, la función reducer también tiene una llamada useContext ; esto tampoco es operativo desde la perspectiva de React y no es una buena práctica.
Como se ha encontrado en su pregunta, simplemente mueva el socket.emit a su handleSubmit y debería estar listo para comenzar. Alternativamente, puede considerar una configuración más sofisticada si desea ir por la ruta reductora; algunas configuraciones populares incluyen Redux + Redux Thunk o Redux + Redux Saga. Su experiencia puede ser diferente.
Depuré su código y encontré el problema con el reductor activándose dos veces.
La solución es eliminar las etiquetas <React.StrictMode> alrededor de la etiqueta de su aplicación en index.js .
Más información sobre StrictMode : https://reactjs.org/docs/strict-mode.html