I've emitted two events on user joined & left (user_joined and user_left). It's working on the server-side but not working on the client-side.
Server-side code: (it's working, showing console.log on every connection)
io.on('connection', function (socket) {
const id = socket.id;
/**
* User Join Function
*/
socket.on('join', function ({ name, room }) {
const { user } = addUser({id, name, room}); // add user to users array
socket.join(user.room);
socket.emit('user_joined', users); // emit event with modified users array
console.log(id, 'joined')
})
/**
* User Disconnect function
*/
socket.on('disconnect', () => {
removeUser(id); // remove user form users array
socket.emit('user_left', users); // emit event with modified users array
console.log(id, 'left')
})
})
Client-side code: (Not firing on user_joined or user_left)
const [players, setPlayers] = useState([]);
const ENDPOINT = 'localhost:5000';
socket = io(ENDPOINT);
useEffect(() => {
const name = faker.name.firstName() + ' ' + faker.name.lastName();
socket.emit('join', {name, room: 'global'}); // it's working fine
return () => {
socket.emit('disconnect');
socket.off();
}
}, [])
useEffect(() => {
socket.on('user_joined', (users) => {
setPlayers(users);
}); // >>> Not Working <<<
socket.on('user_left', (users) => {
setPlayers(users);
}); // >>> Not Working <<<
console.log(socket) // it's working fine
}, [players]);
The socket instance needs to be created only once. In your case, it is getting created on every re-render. Also you do not need 2 useEffects.
Put the creation of socket instance and merge your 2 useEffects into 1 and provide an empty array as dependency. With this, your useEffect is executed only once and not on every re-render.
Try this
const [players, setPlayers] = useState([]);
useEffect(() => {
const ENDPOINT = 'localhost:5000';
socket = io(ENDPOINT);
const name = faker.name.firstName() + ' ' + faker.name.lastName();
socket.emit('join', {name, room: 'global'});
socket.on('user_joined', (users) => {
setPlayers(users);
});
socket.on('user_left', (users) => {
setPlayers(users);
});
console.log(socket);
return () => {
socket.emit('disconnect');
socket.off();
}
}, []);
...
If you want to use the socket instance in other places of your component then make use of useRef. With useRef, you always get the same instance unless you mutate it.
create socket with refs
...
const [players, setPlayers] = useState([]);
const ENDPOINT = 'localhost:5000';
const socketInstance = useRef(io(ENDPOINT));// in react, with useRef, you always get the same instance unless you mutate it.
useEffect(() => {
// socketInstance.current = io(ENDPOINT);
const name = faker.name.firstName() + ' ' + faker.name.lastName();
socketInstance.current.emit('join', {name, room: 'global'});
socketInstance.current.on('user_joined', (users) => {
setPlayers(users);
});
socketInstance.current.on('user_left', (users) => {
setPlayers(users);
});
console.log(socketInstance.current);
return () => {
socketInstance.current.emit('disconnect');
socketInstance.current.off();
}
}, []);
...
Related
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]);
import ...
let socket;
const Game = () => {
const ...
const [userCount, setUserCount] = useState(0);
const scrollToBottom = () => {
endToMessages.current?.scrollIntoView({ behavior: "smooth" });
};
const { _id, username } = JSON.parse(localStorage.getItem("user")).user;
useEffect(() => {
socket = io(process.env.REACT_APP_BASE_URL);
socket.emit("game_lobby", { id: _id, username, room: gameInfo.name });
return () => socket.disconnect();
}, [_id, username, gameInfo.name]);
useEffect(() => {
socket.on("message", ({ user, text }) => {
setMessages((prev) => [...prev, { user, text }]);
scrollToBottom();
});
socket.on("user_count",({count}) => {
setUserCount(count);
})
}, []);
console.log("Current user count",userCount);
const sendMessage = () => {
...
};
return (
<div className="game section">
...
</div>
);
};
export default Game;
Server side:
import { Server } from "socket.io";
const socketApi = (server) => {
const io = new Server(server, {
cors: {
origin: ["https://mook-f2b4e.web.app", "http://localhost:3000"],
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
socket.on("disconnect", () => {
console.log(`user ${socket.id} had left`);
});
socket.on("game_lobby", async ({ id, username, room }) => {
console.log("We have a new connetion.");
socket.join(room);
const roomClients = await (await io.in(room).fetchSockets()).length;
console.log(roomClients);
io.to(room).emit("user_count", { count: roomClients });
socket.on("disconnect", () => {
io.to(room).emit("user_count", { count: roomClients });
});
socket.emit("message", {
user: "Admin",
text: `${username} welcome to ${room} room`,
});
});
socket.on("send_message", ({ name, message, room }) => {
io.to(room).emit("message", { user: name, text: message });
});
});
return io;
};
export default socketApi;
Hi all.When I try to get user count when component mount but I can't.First time when component did mount I get 0 value. If someone else joins the room then I can get its current value. If I could explain properly I mean let's say there are 3 people in the room and I joined that room later. Now there are 4 people in the room but I got 0 value from console.log("Current user count", userCount) if the 5th person joins the room then I can get the current value from the server.
I believe you would need the server to have a variable to keep track of active users. So connection increment a variable for onlineUsers, then on disconnect subtract one from the onlineUsers
See reference here => https://stackoverflow.com/a/10276446/3124019
I was given this Eslint error:
Assignments to the '_engine' variable from inside React Hook useCallback will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useCallback.eslint(react-hooks/exhaustive-deps)
from this code:
const RtcEngineInit = useCallback(async () => {
const {appId} = appInit;
_engine = await RtcEngine.create(appId);
await _engine.enableAudio();
_engine.addListener('UserOffline', (uid: any, reason: any) => {
console.log('UserOffline', uid, reason);
const {peerIds} = appInit;
setAppInit((prevState) => ({
...prevState,
peerIds: peerIds.filter((id) => id !== uid),
}));
});
_engine.addListener(
'JoinChannelSuccess',
(channel: any, uid: any, elapsed: any) => {
console.log('JoinChannelSuccess', channel, uid, elapsed);
setAppInit((prevState) => ({
...prevState,
joinSucceed: true,
}));
},
);
}, []);
React.useEffect(() => {
RtcEngineInit();
}, [RtcEngineInit]);
could someone explain me why this is happening and help me to solve that? thanks.
As the error suggests, You should not have the RTC Engine inside the render loop. All the statements inside the render loop get executed again. To avoid this. You can have the RTC engine inside a useRef hook.
const App: React.FC = () => {
let engine = useRef<RtcEngine | null>(null);
const appid: string = 'APPID';
const channelName: string = 'channel-x';
const [joinSucceed, setJoinSucceed] = useState<boolean>(false);
const [peerIds, setPeerIds] = useState<Array<number>>([]);
useEffect(() => {
/**
* #name init
* #description Function to initialize the Rtc Engine, attach event listeners and actions
*/
async function init() {
if (Platform.OS === 'android') {
//Request required permissions from Android
await requestCameraAndAudioPermission();
}
engine.current = await RtcEngine.create(appid);
engine.current.enableVideo();
engine.current.addListener('UserJoined', (uid: number) => {
//If user joins the channel
setPeerIds((pids) =>
pids.indexOf(uid) === -1 ? [...pids, uid] : pids,
); //add peer ID to state array
});
engine.current.addListener('UserOffline', (uid: number) => {
//If user leaves
setPeerIds((pids) => pids.filter((userId) => userId !== uid)); //remove peer ID from state array
});
engine.current.addListener('JoinChannelSuccess', () => {
//If Local user joins RTC channel
setJoinSucceed(true); //Set state variable to true
});
}
init();
}, []);
return <UI />
};
export default App;
The full example at:
https://github.com/technophilic/Agora-RN-Quickstart/blob/sdk-v3-ts/src/App.tsx
const socketUsers = [];
io.on("connection", (socket) => {
socket.on("user", (userdata) => {
const aUser = { socket: socket, user: userdata };
socketUsers.push(aUser);
});
socket.on("disconnect", () => {
delete socketUsers[socket.id];
console.log(socketUsers);
});
});
The socket and user field is also assigned to the aUser then it's pushed into the socketUsers array. This happens after the user logs in because we need their userdata.
When the user disconnects, the console logs the array for me but the user is still there (both socket and userdata fields).
What am I doing wrong?
You are indexing the array wrong, pushing new sockets onto it won't make those sockets' ids be indexes of the array, from what i can tell you probably should use an object instead.
const socketUsers = {};
io.on("connection", (socket) => {
socket.on("user", (userdata) => {
const aUser = { socket: socket, user: userdata };
socketUsers[socket.id] = aUser;
});
socket.on("disconnect", () => {
delete socketUsers[socket.id];
console.log(socketUsers);
});
});
If you are so insistent on using an array, you would have to manually search it for index of the element you're looking for
const socketUsers = [];
io.on("connection", (socket) => {
socket.on("user", (userdata) => {
const aUser = { socket: socket, user: userdata };
socketUsers.push(aUser);
});
socket.on("disconnect", () => {
socketUsers.splice(socketUsers.findIndex(elem => elem.socket.id === socket.id), 1);
console.log(socketUsers);
});
});
I want to test a function that returns a user by ID from a list of users!!
There is a file responsible for working with the list of users:
const users = [];
const getUser = (id) => users.find((user) => user.id == id);
module.exports = { users, addUser, removeUser, getUser, getUsers };
Unfortunately, I did not find a solution on how to test this function. Expected result is undefined, because the users array is empty. I do not understand how I can replace an array of users for testing.
const { getUser } = require('../users');
describe('Socket', () => {
let socketId;
beforeEach(() => {
socketId = 'qwertyqwerty';
})
test('getUser', () => {
const user = getUser(socketId);
expect(user).toEqual({id: 'qwertyqwerty',user:{username: 'Max'}});
});
})
Conjured a decision!!! in short. used a babel-plugin-rewire. And here's how to implemen:
users.js
import Helper from './Helper';
const users = [];
const user = {
getUser: (id) => users.find((user) => user.id == id),
}
module.exports = user;
And test file:
const User = require('../users');
User.__Rewire__('users', [{id:'qwertyqwerty',user:{username: 'Max'}},{id:'asdfghasdfgh',user:{username: 'Andy'}}]);
describe('Socket', () => {
let socketId;
beforeEach(() => {
socketId = 'qwertyqwerty';
})
test('getUser is user error', () => {
const user = User.getUser(socketId);
expect(user).toEqual({id: 'qwertyqwerty',user:{username: 'Max'}});
});
})
Thanks to Always Learning, for the quick and correct answer )))