How to use useEffect callback with event callbacks
By Flavio Copes
Fix a React effect event callback that reads stale state by subscribing with the right dependencies and removing the old listener during cleanup.
I was running some code like this:
useEffect(() => {
if (!socket) return
socket.on('newuserconnected', (username) => {
console.log(connectedusers)
})
}, [socket])
to initialize a callback for an event newuserconnected on a socket.io connection.
I assumed that after doing so, any time I called that event on the server, the client-side (React app) would print the current value at runtime of the variable connectedusers (imagine I was updating it somewhere else in the app).
But no, the value of that variable was “stuck in time” at the moment I defined that event.
The callback closes over the value from the render that created it. Adding connectedusers to the dependency array gives the callback the current value, but there is one important detail: remove the old listener before the effect subscribes again.
useEffect(() => {
if (!socket) return
const handleNewUser = (username) => {
console.log(connectedusers)
}
socket.on('newuserconnected', handleNewUser)
return () => {
socket.off('newuserconnected', handleNewUser)
}
}, [socket, connectedusers])
Without that cleanup, every state change can add another listener and the callback may run multiple times.
If the handler only needs to update state from its previous value, a functional state update can avoid reading the closed-over value:
setConnectedUsers((users) => [...users, username])
The React hooks linter is useful here. If an effect reads a reactive value, include it as a dependency unless you deliberately restructure the code so the effect no longer reads it.