socket.on event firing multiple times in react.js - javascript

I'm emitting socket event from my sever end point & listen that event on react.js client with socket.on() but i found my socket.on event firing multiple times when emit event.I read many question related this issue on stack overflow but did't succeed.
Here relavant code:
server
currentUsers: async function (req, res, next) {
try {
let io = req.app.get("socketio") // get socketio instance
const uoid = req.body.uoid;
const uuid = req.body.uuid || req.decoded.uuid
const beacon_name = req.body.beacon_name
if (uuid !== undefined && beacon_name !== undefined && uoid !== undefined) {
let find = await knex('current_users').where(knex.raw('uuid = ? and uoid = ?', [uuid, uoid])).catch((err) => { return Promise.reject(err) })
if (find.length == 0) {
let result = await knex('current_users').insert({ uuid: uuid, uoid: req.body.uoid, beacon_name: beacon_name, created_at: helper.currentTimeStamp(), in_at: helper.currentTimeStamp(), in: 1,out: 0 }).catch((err) => { return Promise.reject(err) })
console.log('result', result)
let getResult = await knex('users').select('users.id', 'users.name', 'users.email','users.mobile_number', 'users.auth_type', 'users.uuid', 'users.role','current_users.beacon_name','current_users.id as ob_id','beacons_info.beacon_room','current_users.in_at','current_users.out_at').innerJoin('current_users', 'users.uuid', '=', 'current_users.uuid').innerJoin('outlets','outlets.id','=','current_users.uoid').innerJoin('beacons_info', 'beacons_info.name', '=', 'current_users.beacon_name').where(knex.raw('current_users.id = ?',result))
io.emit('in_users',getResult)
res.end()
}
}
} catch (err) {
console.log("err =====>", err)
}
}
client
import React from "react";
import socket from "../../../../utils/socket.io"; // get socket
import EventEmitter from 'events';
class CurrentUsers extends React.Component {
_isMounted = false;
constructor(props) {
super(props);
this.outlet_id = sessionStorage.outlet_id ? sessionStorage.outlet_id : "";
this.selecteId = null;
this.in_users = [];
this.state = {
loading: true,
data: [],
editData: {
name: "",
date: "",
room: ""
}
};
}
componentDidMount() {
console.log("calling component did mount");
this._isMounted = true;
this.setState({ loading: true });
socket.emit('request-current-users-list',this.outlet_id)
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
socket.on('get-current-users-list',(data)=>{
this.setState({ data: data,loading: false})
})
console.log(EventEmitter.listenerCount(socket, 'in_users'));
socket.on('in_users', (data) => {
console.log("=== in ===", data)
})
return (
// template html code
);
}
}
here socket.on(in_users) event firing multiple times.

Put all of your socketio listerners in React inside componentDidMount ,
Its because re-renders, React re-renders multiple times when ever any state changes ,so basically your socketio listerers just keep adding up. That is why you are getting multiple events fired. You just need to add your socketio listeners once , so add your listeners inside componentDidMount()

Somehow it keeps adding the listener each time the socket.on is fired. I tried this:
socket.off('MY_EVENT').on('MY_EVENT', () => doThisOnlyOnce());
I found it on code grepper, and it worked for me.
EDIT:
socket.on is fired on each render. so turning it off and on isn't such an efficient way of doing it. A better way would do it would be to run socket.on on first render.
useEffect(()=>{
socket.on('MY_EVENT', () => doThisOnlyOnce());
},[])

Related

React renders repeatedly on event

I'm using sockets in my website and there's an event where one user can send a word to the server, which emits (art-addpic) an image URL corresponding to that word to everyone, but only the user with isArtist=true gets to respond to the event.
The artist's page is supposed to update an existing list of image URLs (optionImages) with the received URL once. But when the event is received, all images in the list are replaced by the received URL. Furthermore, the component rendering the list of images ArtBoard is not re-rendered with updated URLs.
I'm new to React. Where am I going wrong?
I've checked the server and the event art-addpic is broadcasted only once.
Arena.js: (The webpage where this happens):
import React, { useEffect, useState } from "react";
import Leaderboard from "../comps/Leaderboard";
import { io } from "socket.io-client";
import Service from "../Service";
import DetBoard from "../comps/DetBoard";
import ArtBoard from "../comps/ArtBoard";
const username = "Nick"
const roomkey="abc"
let userid;
if(localStorage.getItem('userid')){
userid = localStorage.getItem('userid')
}
else{
userid = Service.makeid(5);
localStorage.setItem('userid', userid);
}
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update the state to force render
}
// const [userid,setUserId] =
const socket = io('http://localhost:3001', {query:"username="+username+"&roomkey="+roomkey+"&userid="+userid});
const Arena = (props)=>{
const [isArtist, setIsArtist] = useState(false);
const [focusImage, setFocusImage] = useState('https://i.imgur.com/61HsZCU.jpeg')
const [players, setPlayers] = useState([]);
const [optionImages, setOptionImages] = useState([
'https://i.imgur.com/61HsZCU.jpeg',
'https://i.imgur.com/61HsZCU.jpeg',
'https://i.imgur.com/61HsZCU.jpeg',
'https://i.imgur.com/61HsZCU.jpeg',
'https://i.imgur.com/61HsZCU.jpeg'
])
useEffect(()=>{
socket.on('connect',()=>{
console.log("connected")
})
socket.on('players', (data)=>{
data = JSON.parse(data)
console.log(data)
setPlayers(data)
})
socket.on('artist', (data)=>{
if(data===userid){
console.log('You are an artist, Mr White.')
setIsArtist(true);
}
else{
setIsArtist(false);
}
})
socket.on('art-addpic', (data)=>{
data = JSON.parse(data)
console.log(data)
let tempOps =optionImages;
tempOps.splice(0, 1);
tempOps.push(data.url)
console.log(tempOps)
setOptionImages(tempOps);
})
}, [
optionImages
]);
if(isArtist){
return(
<div>
<Leaderboard players={players}></Leaderboard>
{/* <ArtBoard></ArtBoard> */}
<ArtBoard socket={socket} focusImage={focusImage} optionImages={optionImages} setOptionImages={setOptionImages}/>
</div>
);
}
else{
return (
<div>
<Leaderboard players={players}></Leaderboard>
{/* <ArtBoard></ArtBoard> */}
<DetBoard socket={socket} focusImage={focusImage}/>
</div>
);
}
}
export default Arena;
You've at least a few issues:
No clean up function returned from the useEffect hook to unsubscribe the socket connections, so they remain open.
optionImages state mutations.
Updating the optionImages state retriggers the useEffect callback which creates more subscriptions.
Hook Code
useEffect(() => {
socket.on('connect', () => {
console.log("connected");
});
socket.on('players', (data) => {
data = JSON.parse(data);
console.log(data);
setPlayers(data);
});
socket.on('artist', (data) => {
if (data === userid) {
console.log('You are an artist, Mr White.');
setIsArtist(true);
} else {
setIsArtist(false);
}
});
socket.on('art-addpic', (data) => {
data = JSON.parse(data);
console.log(data);
let tempOps = optionImages; // (2) tempOps is reference to optionImages state
tempOps.splice(0, 1); // (2) mutation!
tempOps.push(data.url); // (2) mutation!
console.log(tempOps);
setOptionImages(tempOps); // (2,3) saved state reference back into state
});
// (1) missing cleanup function
}, [optionImages]); // (3) state updated in hook
From what I can tell, the main issue is with the 'art-addpic' event. It seems like you want to remove the first element from the optionImages state and add a new URL to the end.
If this is the case then I have the following suggestions:
Return a cleanup function to unsubscribe the socket connections.
Remove all useEffect hook dependencies so the hook run once when the component mounts to establish the socket subscriptions, and clean them up when unmounting.
Use a functional state update for optionImages to remove the state as an external dependency.
Hook Code
useEffect(() => {
socket.on('connect', () => {
console.log("connected");
});
socket.on('players', (data) => {
const parsedData = JSON.parse(data);
console.log(parsedData);
setPlayers(parsedData);
});
socket.on('artist', (data) => {
setIsArtist(data === userid);
});
socket.on('art-addpic', (data) => {
const parsedData = JSON.parse(data);
console.log(parsedData);
setOptionImages(optionImages =>
// Shallow copy into array, append URL, slice & keep last 4 elements
[...optionImages, parsedData.url].slice(-4),
);
});
return () => {
socket.removeAllListeners();
}
}, []);
useEffect(() => {
if (isArtist) {
console.log('You are an artist, Mr White.');
}
}, [isArtist]);

Can't use new redux state right after fetching a response from Socket.IO

I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.

React useState hook not consistently updating

I started integrating websockets into an existing React/Django app following along with this example (accompanying repo here). In that repo, the websocket interface is in websockets.js, and is implemented in containers/Chat.js.
I can get that code working correctly as-is.
I then started re-writing my implementation to use Hooks, and hit a little wall. The data flows through the socket correctly, arrives in the handler of each client correctly, and within the handler can read the correct state. Within that handler, I'm calling my useState function to update state with the incoming data.
Originally I had a problem of my single useState function within addMessage() inconsistently firing (1 in 10 times?). I split my one useState hook into two (one for current message, one for all messages). Now in addMessage() upon receiving data from the server, my setAllMessages hook will only update the client where I type the message in - no other clients. All clients receive/can log the data correctly, they just don't run the setAllMessages function.
If I push to an empty array outside the function, it works as expected. So it seems like a problem in the function update cycle, but I haven't been able to track it down.
Here's my version of websocket.js:
class WebSocketService {
static instance = null;
static getInstance() {
if (!WebSocketService.instance) {
WebSocketService.instance = new WebSocketService();
}
return WebSocketService.instance;
}
constructor() {
this.socketRef = null;
this.callbacks = {};
}
disconnect() {
this.socketRef.close();
}
connect(chatUrl) {
const path = `${URLS.SOCKET.BASE}${URLS.SOCKET.TEST}`;
this.socketRef = new WebSocket(path);
this.socketRef.onopen = () => {
console.log('WebSocket open');
};
this.socketRef.onmessage = e => {
this.socketNewMessage(e.data);
};
this.socketRef.onerror = e => {
console.log(e.message);
};
this.socketRef.onclose = () => {
this.connect();
};
}
socketNewMessage(data) {
const parsedData = JSON.parse(data);
const { command } = parsedData;
if (Object.keys(this.callbacks).length === 0) {
return;
}
Object.keys(SOCKET_COMMANDS).forEach(clientCommand => {
if (command === SOCKET_COMMANDS[clientCommand]) {
this.callbacks[command](parsedData.presentation);
}
});
}
backend_receive_data_then_post_new(message) {
this.sendMessage({
command_for_backend: 'backend_receive_data_then_post_new',
message: message.content,
from: message.from,
});
}
sendMessage(data) {
try {
this.socketRef.send(JSON.stringify({ ...data }));
} catch (err) {
console.log(err.message);
}
}
addCallbacks(allCallbacks) {
Object.keys(SOCKET_COMMANDS).forEach(command => {
this.callbacks[SOCKET_COMMANDS[command]] = allCallbacks;
});
}
state() {
return this.socketRef.readyState;
}
}
const WebSocketInstance = WebSocketService.getInstance();
export default WebSocketInstance;
And here's my version of Chat.js
export function Chat() {
const [allMessages, setAllMessages] = useState([]);
const [currMessage, setCurrMessage] = useState('');
function waitForSocketConnection(callback) {
setTimeout(() => {
if (WebSocketInstance.state() === 1) {
callback();
} else {
waitForSocketConnection(callback);
}
}, 100);
}
waitForSocketConnection(() => {
const allCallbacks = [addMessage];
allCallbacks.forEach(callback => {
WebSocketInstance.addCallbacks(callback);
});
});
/*
* This is the problem area
* `incoming` shows the correct data, and I have access to all state
* But `setAllMessages` only updates on the client I type the message into
*/
const addMessage = (incoming) => {
setAllMessages([incoming]);
};
// update with value from input
const messageChangeHandler = e => {
setCurrMessage(e.target.value);
};
// Send data to socket interface, then to server
const sendMessageHandler = e => {
e.preventDefault();
const messageObject = {
from: 'user',
content: currMessage,
};
setCurrMessage('');
WebSocketInstance.backend_receive_data_then_post_new(messageObject);
};
return (
<div>
// rendering stuff here
</div>
);
}
There is no need to rewrite everything into functional components with hooks.
You should decompose it functionally - main (parent, class/FC) for initialization and providing [data and] methods (as props) to 2 functional childrens/components responsible for rendering list and input (new message).
If you still need it ... useEffect is a key ... as all code is run on every render in functional components ... including function definitions, redefinitions, new refs, duplications in callbacks array etc.
You can try to move all once defined functions into useEffect
useEffect(() => {
const waitForSocketConnection = (callback) => {
...
}
const addMessage = (incoming) => {
setAllMessages([incoming]);
};
waitForSocketConnection(() => {
...
}
}, [] ); // <<< RUN ONCE

Handle Apollo Client errors via React Context API

I have some component which is executing some function from api file. For example :
class Modal extends React.Component {
componentDidMount() {
this.saveModel();
}
saveModel = () => {
return this.setState({loadingRequest: true}, async () => {
await Model.graphqlMutation(request).then(() => {
return this.closeAfterSave();
});
});
}
render() {
return <div>Some divs...</div>
}
}
I am using Apollo Client so I defined my client instance with links
const errorLink = onError(({networkError, graphQLErrors, operation, forward}) => {
if (graphQLErrors && graphQLErrors[0].message === 'Unauthorized' && localStorage.getItem('authToken')) {
return promiseToObservable(refreshToken()).flatMap((res) => {
return forward(operation);
});
}
});
const link = ApolloLink.from([
errorLink,
authLink,
batchHttpLink,
]);
export const client = new ApolloClient({
link,
cache: new InMemoryCache(),
defaultOptions: defaultOptions,
});
The problem: in errorLink I am handling errors but I can't pass it down to component which sent request. I would p.ex. display some message. I know React Context API and I did it, connected with my components, but I don't know how to connect to it from client.
Thanks for suggestions.

React Native Can't perform a React state update on an unmounted component firebase

I have this code which listen to changes in the Realtime database when new child added.
mUserRef = firebase.database().ref("Messages").child(conversation);
mUserRef.on("child_added", snapshot => {
const message = {
key: snapshot.key,
...snapshot.val()
};
this.onReceive(message);
});
and this code will be triggered when received new child added to the database ref
let cy = this;
firebase
.database()
.ref("LastMessages")
.child(key)
.once("value")
.then(function(snapshot) {
//...some code
});
the problem is i can't unsubscribe off the realtime listener when i use componentWillUnmount
Code for unsubscribe
constructor(props) {
super(props);
this.state = {
ready: false,
messages: []
};
}
_isMounted = false;
componentDidMount = async () => {
this._isMounted = true;
}
async retriveMessages(conversation) {
let cy = this;
cy.setState({
ready: true
});
let mUserRef = null;
if (this._isMounted) {
mUserRef = firebase
.database()
.ref("Messages")
.child(conversation);
mUserRef.on("child_added", snapshot => {
const message = {
key: snapshot.key,
...snapshot.val()
};
this.onReceive(message);
});
} else {
mUserRef.child(conversation).off("child_added");
}
}
componentWillUnmount() {
this._isMounted = false;
}
Solutions i tried:
Unsubscribing from Firebase Realtime Database
componentwillunmount to unsubscribe from firestore
how to remove firebase database listener...
How to stop Firebase Listener when i pass to another screen?
componentWillUnmount():
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().
Therefore do the following:
componentWillUnmount() {
mUserRef.off("child_added");
this._isMounted = false;
}

Categories