I'm building an app with a chat feature using express.js, react.js and socket.io. Here is a minimal version with the bug reproduced on Github.
The problem is the server's socket.on()
always fires twice, for no reason that is apparent to me.
As far as I can tell the client only ever sends a single .emit
, but the server's .on
always fires twice (and I'm fairly certain that event handler isn't bound twice), regardless of how many clients are connected, or indeed how I've refactored the code.
The sending client's form (input + button) appends the message to its local messages list and then dispatches MESSAGE_NEW
to the reducer.
Here's first the Socket.js
file.
import React from 'react'
import socketio from 'socket.io-client'
export const socket = socketio.connect('ws://localhost:3001')
export const SocketContext = React.createContext()
And here is the submit handler code from Chat.js
(this seems to work correctly, dispatching just once):
const handleSubmit = e => {
e.preventDefault()
if (message === '') return
setMessages(messages => [...messages, message])
dispatch({ type: 'MESSAGE_NEW', payload: message })
setMessage('')
}
Here's /Reducer.js
, which .emit
's the signal (also seems to work fine, firing just once).
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
}
}
But here is the relevant bit from /server/index.js
, this is where things go wrong. No matter what I've tried, the .on
always fires twice.
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)
})
})
Update: I found out that moving the socket.emit
from Reducer.js
to handleSubmit
in Chat.js
makes it work just fine. I don't understand why since the Reducer doesn't fire twice.
As per React documentation's on strict mode:
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
,render
, andshouldComponentUpdate
methods- Class component static
getDerivedStateFromProps
method- Function component bodies
- State updater functions (the first argument to
setState
)- Functions passed to
useState
,useMemo
, oruseReducer
Notice the last line: functions passed to useReducer
will be invoked twice; that is why your message is being sent twice. You can confirm this by updating your reducer like so:
case "MESSAGE_NEW":
alert("I'm running");
You'll notice this alert appearing twice, thanks to React.StrictMode
.
If you remove the <React.StrictMode>
wrapper in src/App.js
the test
message will no longer repeat; however, it's not fixing the underlying issue here.
The underlying issue is that you're making your reducer (which should be pure) perform an impure side-effect (in this case, sending a message via a websocket). Additionally, the reducer function also has a useContext
call – this is also a no-op from React's perspective and is not good practice.
As you've come across in your question, Simply move the socket.emit
into your handleSubmit
and you should be good to go. Alternatively, you can consider a more sophisticated setup if you wish to go the reducer route; some popular configurations include Redux + Redux Thunk, or Redux + Redux Saga. Your mileage may vary.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With