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
Related
I have a question about some weird behavior when using React state.
If I change the default type of the groupedFormErrors variable in the helper function to [] instead of {} the state in the page element formErrors doesn't update. Would be great if somebody could explain why this happens.
Page element
const [formErrors, setFormErrors] = useState();
const signUpHandler = async (e) => {
e.preventDefault();
const response = await submitForm();
if (response.status === 422) {
setFormErrors(groupFormErrors(response));
}
};
Helper function
export default function GroupFormErrors(response) {
let groupedFormErrors = {}; // <<<<<<<<<<<<<<<<<<<<< This here
if (response.status === 422) {
response.data.forEach((error) => {
if (groupedFormErrors.hasOwnProperty(error.param)) {
groupedFormErrors[error.param].push(error.msg);
} else {
groupedFormErrors[error.param] = [];
groupedFormErrors[error.param].push(error.msg);
}
});
}
return groupedFormErrors;
}
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]);
I am trying to develop a live chat app using web sockets and react, but after I try submitting several messages (around 30) the web socket gets stuck on the CONNECTING state. I have it set up so when it does send a message it disabled the send button to prevent users from spamming messages too fast but I unfortunately still get the same issue.
// id is a uuid() string
const ws = new WebSocket(`ws://localhost:3001/chat/${id}`);
useEffect(() => {
ws.onmessage = function(evt){
try{
const user_id = parseInt(evt.data.split("")[0]);
const message = evt.data.slice(1);
const currentTime = new Date();
const currentUTC = currentTime.toUTCString();
const timestamp = new Date(currentUTC);
setMessages(messages => [...messages, {user_id, message, timestamp}])
} catch(e){
console.log(e);
}
}
ws.onclose = function(evt){
console.log("DISCONNECTED!!")
ws.close();
}
ws.onerror = function(evt){
console.log(evt);
ws.close();
}
}, []);
useEffect(() => {
async function postMessageToAPI() {
const messsageToSend = {
unique_id: id,
message: formData.message,
user_id: user.id,
group_chat_id: room.id
}
// Convert to unviersal time UTC and send it to database
let currentUTC = new Date();
currentUTC.toUTCString();
messsageToSend.timestamp = currentUTC;
await AnonChatApi.sendChatMessage(messsageToSend);
}
if(sendMessage){
ws.onopen = function(){
// add user_id to the start of the message string
const message = `${user.id}` + formData.message;
ws.send(message);
}
postMessageToAPI();
resetFormData();
setTimeout(() => {
setSendMessage(false)
}, 1000);
}
}, [sendMessage]);
const goBackHome = () => {
ws.close();
history.push('/');
}
I can see you're using Hooks, so you must also be using Function Components.
Am I correct in thinking that the code to initialize the websocket
const ws = new WebSocket(`ws://localhost:3001/chat/${id}`);
is at the top of the function?
As a reminder, the function which defines your Function Component is run whenever your component is rendered. Anything that isn't saved in state is lost. This includes your websocket - a new one will be created every render, your async functions may sending data on an old websocket (from a previous render), and React may warn you in the console that you have a memory leak.
useEffect is the proper approach here, but the websocket also needs to be saved in state.
YourFunctionComponent() {
const [ws, setWs] = useState(null);
useEffect(() => {
if (ws == null) {
setWs(new WebSocket(`ws://localhost:3001/chat/${id}`));
}
return () => {
// A function returned from useEffect will
// get called on component unmount.
// Use this function to clean up your connection and
// close your websocket!
// clean up, e.g.
// ws.send('closing due to unmount!');
ws.close();
setWs(null);
}
}, [ws, setWs]);
// Add `ws` as a dependency in the useEffect()s you posted above
useEffect(() => {
ws.onmessage = function(evt){
[--snip--]
}
}, [ws]);
useEffect(() => {
async function postMessageToAPI() {
[--snip--]
}
}, [sendMessage, ws]);
}
I have been trying to create a authentication system in svelte , and signup is a multi-step process so need to save api response from step 1 and pass along , each step is a different route .Have came across store in svelte but somehow it just return undefined when fetching the data using get . Below is the demo code which returns the same ouput.
index.svelte
<script>
import signUpStore from "./hobby-store.js";
let data = {
name: "Rahul",
age: "something"
};
signUpStore.setSignUp(data);
// let result = signUpStore.getSignUp();
// console.log(result); //undefined
</script>
<p>
<strong>
Try editing this file (src/routes/index.svelte) to test live reloading.
</strong>
</p>
About.svelte
<script>
import signUpStore from "./hobby-store.js";
import { onMount } from "svelte";
let result = signUpStore.getSignUp();
console.log("server side : ", result); //undefined
onMount(() => {
console.log("client side : ", result); // undefined
});
</script>
<p>This is the 'about' page. There's not much here.</p>
hobby-store.js
import {
writable,
get
} from 'svelte/store'
const signUp = writable()
const signUpStore = {
subscribe: signUp.subscribe,
setSignUp: (items) => {
signUp.set(items)
// console.log('items : ', items, signUp)
},
addSignUp: (data) => {
signUp.update(items => {
return items.concat(data)
})
},
getSignUp: () => {
get(signUp)
}
}
export default signUpStore;
Just need to save this data in session or any persistent storage that svelte or sapper provides and reset it on successfull action.
Example session.js store below with logging:
import { writable } from 'svelte/store';
import { deepClone } from './../utilities/deepClone.js';
const newSession = {
a; 0, b: 0, x: 0
};
function sessionStore() {
const { subscribe, set, update } = writable(deepClone(newSession));
let logging = false;
return {
subscribe, // $session...
update: (obj) => {
update(o => { // session.update({a:1, b:2});
const merged = Object.assign(o, obj);
if (logging) console.log('session update', merged);
return merged;
});
},
set: (key, value) => { // session.set('x', 9)
update(o => {
const merged = Object.assign(o, {[key]: value});
if (logging) console.log('session set', merged);
return merged;
});
},
reset: () => { // session.reset()
set(deepClone(newSession));
},
set log(bool) { // setter: session.log = true;
logging = bool === true;
}
};
};
export const session = sessionStore();
Example.svelte
<script>
import { session } from './session.js';
session.log = true;
$: console.log('reactive log', $session);
session.set('x', 10);
session.reset();
<script>
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());
},[])