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]);
Related
This component is for counting views at page level in Next.js app deployed on AWS Lambda
function ViewsCounter({ slug }: { slug: string }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, { method: "POST" })
.catch(console.log);
registerView();
}, [slug]);
return (
<>
{views}
</>
);
}
This one is for displaying views on homepage
function ViewsDisplay({ slug }: { slug: string }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
return (
<>
{views}
</>
);
}
While it works as expected on localhost, looks like it displays only the first fetched value and doesn't revalidate it for some reason.
When visiting the page, Counter is triggered correctly and the value is changed in DB.
Probably it has something to do with mutating, any hints are appreciated.
useSWR won't automatically refetch data by default.
You can either enable automatic refetch using the refreshInterval option.
const { data } = useSWR(`/api/views/${slug}`, fetcher, { refreshInterval: 1000 });
Or explicitly update the data yourself using a mutation after the POST request to the API.
function ViewsCounter({ slug }: { slug: string }) {
const { data, mutate } = useSWR(`/api/views/${slug}`, fetcher);
const views = new Number(data?.total);
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, { method: "POST" })
.then(() => {
mutate();
})
.catch(console.log);
registerView();
}, [slug]);
return (<>{views}</>);
}
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
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();
}
}, []);
...
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
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());
},[])