React - state variable not reflecting changes - reactjs

I am working on adding websockets to my Node/React app to automatically reflect changes to all the clients. So I have a websockets helper module that has onclose, onopen and onmessage events as well as a readyState function. So my component that needs the updated websocket values makes a call to that module and gets back data. That data variable is coming over empty, but when I console it out in the onmessage event in the module itself, it has all the info I want.
So here is how I call the websocket module in my component:
const onConnected = (socket) => {
socket.send(
JSON.stringify({
eventType: 'clientCount'
})
);
};
const { socket, readyState, reconnecting, data } = useWebsocket({
url: wsURL + ':' + process.env.REACT_APP_WS_PORT,
onConnected
});
I have a useEffect that should spit out the updated values from data:
useEffect(() => {
console.log('data changed!!!!');
console.log({ data });
console.log({ socket });
console.log({ readyState });
if (data) {
setNumberClients(data.numberClients);
setNumberIpads(data.numberIpads);
}
}, [data, readyState]);
And finally here is my websockets module itself:
import { useState, useEffect, useRef } from 'react';
export default function useWebsocket({ url, onConnected }) {
const [data, setData] = useState([]);
const [reconnecting, setReconnecting] = useState(false);
const socket = useRef(null);
useEffect(() => {
console.log('running socket hook');
socket.current = new WebSocket(url);
socket.current.onopen = () => {
console.log('connected');
onConnected(socket.current);
};
socket.current.onclose = () => {
console.log('closed');
if (socket.current) {
if (reconnecting) return;
setReconnecting(true);
setTimeout(() => setReconnecting(false), 2000);
socket.current.close();
socket.current = undefined;
}
};
socket.current.onmessage = (e) => {
const wsData = JSON.parse(e.data);
console.log('message received ', wsData);
//setData((prev) => [...prev, wsData]);
setData(wsData);
};
return () => {
socket.current.close();
socket.current = null;
};
}, [reconnecting, url]);
const readyState = () => {
if (socket.current) {
switch (socket.current.readyState) {
case 0:
return 'CONNECTING';
case 1:
return 'OPEN';
case 2:
return 'CLOSING';
case 3:
return 'CLOSED';
default:
return;
}
} else {
return null;
}
};
return {
socket: socket.current,
readyState: readyState(),
reconnecting,
data
};
}
So data is always an empty array when I console it out in my component. But in the websockets module, it(wsData) has the info I need.
One More Thing: I am following the tutorial here: https://github.com/devmentorlive/websocket-direct-chat-client/tree/2-as-a-hook/src/chat
Update 2: I have a github repo showing the exact issue here: https://github.com/dmikester1/websockets-test
Use Server and Start scripts to kick things off.

It's not a problem on front-end side.
I think you have used useWebsocket hook twice - once in the SchedulePage and again in the ClientCountContainer so that you can check if 2 clients are displayed.
The problem is that the socket client you defined in ClientCountContainer component is not receiving the message from the server.
After looking at the websocket server, I noticed that it broadcasts messages to websocket clients that are saved in clients array. Not all the websocket client is saved in this array, but only the client which sends {eventType: 'connect'} message to the server is saved in that array.
The websocket client you created using useWebsocket hook in SchedulePage component is saved in clients array on websocket server, because it first sends {eventType: 'connect'} message to the server. But the websocket client you created in ClientCountContainer is not saved in that array.
Therefore, the messages containing clientCount information is not sent to the second websocket client which is defined in ClientCountContainer.
To get rid of this from happening, you can simply add this code snippet in the ClientCountContainer.js, which sends {eventType: 'connect} message to the server so that this client can be added to broadcast array.
.....
const onConnected = (socket) => {
// Add these lines
socket.send(
JSON.stringify({
eventType: 'connect'
})
);
// This is the original line
socket.send(
JSON.stringify({
eventType: 'clientCount'
})
);
};
......
Please let me know if you have further issues.
Thanks.

I think that the useEffect would have to run all the time, not only when ur url changed or when it is reconnecting.
Try giving this a try: https://stackoverflow.com/a/60161181/10691892

Related

Connection leak with a Socket.IO component in React

I have a React application that uses Socket.IO. The Socket instance is in a React component.
I have been noticing that the action of logging out and logging into my application, which should unmount the component and close the connection and then remount and reopen the connection leads to a socket leak/creation of duplicate socket connection. I have also managed to get the application into a state where it quickly spews off new connections leading to starvation, but have not been able to replicate. This hit production once.
Here is the client code:
const Socket = React.memo(() => {
const [isLoadingSocket, setIsLoadingSocket] = useState<boolean>(false)
const socketRef = useRef<SocketIO<ServerToClientEvents, ClientToServerEvents> | null>(null)
const socketNeedsRestart = isFocused ? !isLoadingSocket : false
async function initializeSocket() {
const token = await getToken()
setIsLoadingSocket(true)
if (socketRef.current) {
socketRef.current.disconnect()
}
socketRef.current = io(`${SOCKET_HOST}`, {
secure: true,
reconnectionDelay: 5000,
transports: ['websocket', 'polling'],
path: ENVIRONMENT !== Environment.local ? '/api/socket.io/' : '',
auth: {
token,
},
})
console.log(`socket initialized`)
}
useEffect(() => {
if (socketNeedsRestart) {
initializeSocket()
}
}, [socketNeedsRestart]) //eslint-disable-line
useEffect(() => {
if (socketRef.current) {
socketRef.current.on(SocketLifecycleEvent.Connect, () => {
console.log('socket connected')
setIsLoadingSocket(false)
})
socketRef.current.on(SocketMessage.UsersOnline, (message) => {
updateOnlineUsers(message.onlineUserIDs)
})
}
return () => {
if (socketRef.current) {
socketRef.current.off(SocketLifecycleEvent.Connect)
socketRef.current.off(SocketLifecycleEvent.ConnectionError)
socketRef.current.off(SocketLifecycleEvent.Disconnect)
}
}
}, [isLoadingSocket])
useEffect(() => {
socketRef.current?.disconnect()
}, [])
return <></>
})
export default Socket
The component is used once in the page that a user gets to after login. I can provide server code but it doesn't do anything except notify all users every time someone connects. What's causing the connection leak? How can I re-create the rapid-fire leak?
Is your last useEffect doing what you expect it to do? It looks like this is supposed to be the cleanup on unmount but you are not returning a function there.
Did you try something like this?:
useEffect( () => () => socketRef.current?.disconnect(), [] );

React/Socket.io chat app not working on heroku

I have a chat app I made using React for the frontend, DRF for the backend and I also have a node.js server within the React app for socket.io The issue is that the chat doesn't work basically (it works fine locally). When a message is sent it's not emitted and only shows up when I refresh since it's then pulled from the DB instead. I have gone through many threads on here for this issue but can't figure out what I'm doing wrong.
My server:
const server = require("http").createServer();
const io = require("socket.io")(server, {
cors: {
origin: "*",
},
});
const PORT = process.env.PORT || 5000;
const NEW_CHAT_MESSAGE_EVENT = "newChatMessage";
io.on("connection", (socket) => {
console.log('Client connected')
// Join a conversation.
const {roomId} = socket.handshake.query;
socket.join(roomId);
// Listen for new messages
socket.on(NEW_CHAT_MESSAGE_EVENT, (data) => {
io.in(roomId).emit(NEW_CHAT_MESSAGE_EVENT, data);
});
// Leave the room if the user closes the socket
socket.on("disconnect", () => {
socket.leave(roomId);
});
});
server.listen(PORT, (error) => {
if (error) throw error;
console.log(`Listening on port ${PORT}`);
});
Hook I made for the frontend:
const NEW_CHAT_MESSAGE_EVENT = "newChatMessage"; // Name of the event
const SOCKET_SERVER_URL = `https://<my-react-frontend>.herokuapp.com`;
export const useChat = () => {
const socketRef = useRef();
const {messages, setMessages, activeConvo, headerConvo, reloadSideBar, setReloadSideBar} = useActiveConvo()
const roomId = activeConvo
useEffect(() => {
console.log('useChat useEffect ran')
// Creates a WebSocket connection
socketRef.current = socketIOClient(SOCKET_SERVER_URL, {
query: {roomId},
});
// Listens for incoming messages
socketRef.current.on(NEW_CHAT_MESSAGE_EVENT, (message) => {
const incomingMessage = {
message: message.body,
created_by: localStorage.getItem('currentUserID'),
};
console.log('messages set in useChat useFfect')
setMessages((messages) => [...messages, incomingMessage]);
});
// Destroys the socket reference
// when the connection is closed
return () => {
socketRef.current.disconnect();
};
}, [roomId]);
// Sends a message to the server that
// forwards it to all users in the same room
const sendMessage = (messageBody) => {
socketRef.current.emit(NEW_CHAT_MESSAGE_EVENT, {
body: messageBody,
senderId: socketRef.current.id,
});
const fetchContents = {
message: messageBody,
created_by: localStorage.getItem('currentUserID'),
convo_id: activeConvo ? activeConvo : headerConvo
}
fetch(`https://<my-drf-backend>.herokuapp.com/api/save-message/`, authRequestOptions(('POST'), fetchContents))
.then(response => response.json())
.then(setReloadSideBar(reloadSideBar + 1))
.catch(error => console.log(error))
};
return {messages, sendMessage};
};
The issue in most of the threads appeared to be either still using the localhost url on the frontend or not using process.env.PORT in the server but it's still busted after fixing that. I also saw someone mention in another thread that the folder structure was the issue so I tried having the server file in the root of the react app and having it in it's own folder under "src", no dice.
In case anyone faces this same issue, I solved it by putting the server in a separate app on heroku

Adding data in React component via websocket

I'm trying to add a data to a table of some operations in React via WebSockets. I even get the new data from WebSocket successfully. I stuck with the question how to add a new data to existing. When I recieve new data in websocket response, my const operationList becomes empty.
Have a look at my code:
const [operationsList, setOperationsList] = useState([{}] )
// Here I get the existing data from backend API and store to operationsList. It works
async function fetchOperations(activeID) {
if (activeID !== false) {
const response = await axios.get(
`http://127.0.0.1:8000/api/operations/?made_by=${activeID}`
)
setOperationsList(response.data)
}
}
useEffect(() => {
setIsOperationsLoading(true)
fetchOperations(activeID)
.then(() => {setIsOperationsLoading(false)})
.catch((e) => {setOperationsError(e)})
},[activeID])
// Here I subscribe to websockets to get new data for adding to operationsList
useEffect(() => {
const ws = new WebSocket('ws://127.0.0.1:8000/ws/')
ws.addEventListener('message', (e) => {
const response = JSON.parse(e.data)
console.log(response) // Here I see new data. It's ok
console.log(operationsList) // All of the sudden operationsList become empty
})
ws.onopen = () => {
ws.send(JSON.stringify({
action: "subscribe_to_operations_activity",
request_id: new Date().getTime(),
}))
}
}, [])
I thought that in my second useEffect I could just add response data from WebSocket like
setOperationsList([response, operationsList]). But operationsList is empty, so I've got just a new data in the table. How to fix it?
The second useEffect hook runs only once when the component mounts, you are logging the initial operationsList state value closed over in callback scope. In other words, it's a stale enclosure over the operationsList state.
I'm guessing it's at this point you are wanting to append response to the operationsList state. You can use a functional state update to correctly access the previous state and append to it.
You may also want to unsubscribe to the "message" event in the useEffect hook's cleanup function. This is to prevent resource leaks and attempts to update state of unmounted components.
useEffect(() => {
const ws = new WebSocket('ws://127.0.0.1:8000/ws/');
const handler = (e) => {
const response = JSON.parse(e.data);
setOperationsList(operationsList => [
...operationsList, // <-- shallow copy previous state
response, // <-- append new data
]);
};
ws.addEventListener('message', handler);
ws.onopen = () => {
ws.send(JSON.stringify({
action: "subscribe_to_operations_activity",
request_id: new Date().getTime(),
}));
};
return () => {
ws.removeEventListener('message', handler);
};
}, []);

socket.on function is not being called for the first time when react component mounted

I am trying to create a simple real time chat app to practise with socket-io. I am using React to create UI and i am using NodeJS with Express web server. I have ChatPage React component to show messages.
I am emitting an event newMessage when a user joined to a room and i am sending a welcome message as data from server to client. At that time i am routing to ChatPage component and i am subscribing to updateUserList event in componentWillMount but this event is not coming up to that callback i defined. After some trials i realized a weird thing about this event. I subscribed this event from another .js file and i could see data that i sent from server.
// server.js
socket.on('joinRoom', (data, callback) => {
const { username, roomName } = data
if (username === '' || roomName === '') {
if (callback) {
return callback({ error: 'Username and password required!' })
}
return
}
socket.join(roomName, (err) => {
if (err && callback) {
return callback({ error: 'Could not connect to room.' })
}
socket.emit('newMessage', generateMessage('Admin', 'Welcome to chat app'))
if (callback) {
callback()
}
})
})
// ChatPage.js
componentWillMount() {
const socket = io('http://localhost:3400')
socket.on('newMessage', (message) => {
console.log('newMessage', message)
})
}
// event is coming to here instead!
import socketIOClient from "socket.io-client";
export const getSocketIOClient = () => {
const endpoint = "http://localhost:3400"
const socket = socketIOClient(endpoint)
socket.on('newMessage', (er) => {
console.log('newMessage', message)
})
return socket;
}
I faced similar porblem in my project. There I noticed few things which are missing in your code.
You need to call back join room in client code as well in componentWillMount() method.
You need to initialize socket in state variable. I don't know exactly why it doesn't work when we initialize in const variable but it works when we use state variable instead. I verified this code, it works fine even in the first time react component mount.
Here, is sample code:
import React from 'react'
import io from 'socket.io-client';
class SocketExample extends React.Component {
state = {
chatMessage: '',
chatMessages: [],
socket: io.connect(`http://localhost:3001`),
}
submitChatMessage = (e) => {
e.preventDefault()
if (this.state.chatMessage.length > 0) {
this.state.socket.emit('newMessage', {
username: this.props.currentUser.username,
message: this.state.chatMessage,
});
this.setState({
chatMessage: '',
})
}
}
componentDidMount(){
this.state.socket.on('connect', () => {
this.state.socket.emit('join', { uniqueValue: chatRoomName });
});
this.state.socket.on("newMessage", data => {
this.setState({
chatMessages: [...this.state.chatMessages, data],
});
});
}
render() {
return (
<div>{this.state.chatMessages.map(chat => {
return <p>{chat.username + ': ' + chat.message}</p>
})}</div>
)
}
}
export default SocketExample

Where and how to add SignalR in redux?

I'm creating a react app which implements SignalR and so far I have my connection and all the listeners in the component where I need them. The problem is that I have action creators in Redux which just make a request and get the response in order to call my server and send the data to all the other clients. Once the server emits the event to all clients, one of my listeners gets the data and calls an action creator which just dispatches an action to refresh my redux state.
I feel like I'm not using the action creators in the right way because I have one action creator which just makes the request and gets the response to return it and it's not changing the state.
If a had the socket connection in the store, I would just have to call one action creator and the logic to emit or listen to socket event, would be in other place.
This is my component,
// --- component.js ---
state = {
connection: null,
};
async componentDidMount() {
// handles any network exception and show the error message
try {
await this.setupConnection();
} catch (error) {
this.showNetworkError(`Whoops, there was an error with your network connection. Please reload the page`);
}
setupConnection = () => {
let { connection } = this.state;
this.setState({
connection: (connection = new HubConnectionBuilder().withUrl(HUB_URL).build()),
});
/**
* LISTENERS that are called from the server via websockets
*/
connection.on('InsertTodo', data => {
// action creator
this.props.add(data);
});
connection.on('UpdateTodo', data => {
// action creator
this.props.update(data);
});
}
createTodo = async todo => {
const { connection} = this.state;
// action creator
const createdTodo = await this.props.createTodo(todo);
if (createdTodo) {
// the below sentence calls the server to emit/send the todo item to all other clients
// and the listener in the setupConnection function is executed
connection.invoke('EmitTodoCreate', createdTodo);
} else {
// there was a problem creating the todo
}
};
This is the action creator
// --- actionCreators.js ----
// ------------------------
export const add = todo => {
return async (dispatch) => {
dispatch({
type: ADD_TODO,
payload: todo,
});
};
};
export const createTodo = todo => {
return async (dispatch) => {
dispatch({
type: START_REQUEST,
});
const response = await postTodo(todo);
const result = await response.json();
if (response.ok) {
dispatch({
type: SUCCESS_REQUEST,
});
// returns the todo item created in order to be sent to the server via websockets
return result;
}
dispatch({
type: FAILURE_REQUEST,
error: result.error,
});
return null;
};
};
I think the best solution is to implement a Redux middleware. It is easy, and you can use authentication for establishing connection, and you can dispatch action creators based on different messages emitted from SignalR.
Per the Redux FAQ, the right place for websockets and other similar connections is in Redux middleware.
This is my custom middleware that establishes the connection, and registers the handlers. Please note that I only would like to receive data, and not interested in sending data. I use REST APIs to send data to server.
import {
JsonHubProtocol,
HttpTransportType,
HubConnectionBuilder,
LogLevel
} from '#aspnet/signalr'; // version 1.0.4
// action for user authentication and receiving the access_token
import { USER_SIGNED_IN } from '../actions/auth';
const onNotifReceived = res => {
console.log('****** NOTIFICATION ******', res);
};
const startSignalRConnection = connection => connection.start()
.then(() => console.info('SignalR Connected'))
.catch(err => console.error('SignalR Connection Error: ', err));
const signalRMiddleware = ({ getState }) => next => async (action) => {
// register signalR after the user logged in
if (action.type === USER_SIGNED_IN) {
const urlRoot = (window.appConfig || {}).URL_ROOT;
const connectionHub = `${urlRoot}/api/service/hub`;
const protocol = new JsonHubProtocol();
// let transport to fall back to to LongPolling if it needs to
const transport = HttpTransportType.WebSockets | HttpTransportType.LongPolling;
const options = {
transport,
logMessageContent: true,
logger: LogLevel.Trace,
accessTokenFactory: () => action.user.access_token
};
// create the connection instance
const connection = new HubConnectionBuilder()
.withUrl(connectionHub, options)
.withHubProtocol(protocol)
.build();
// event handlers, you can use these to dispatch actions to update your Redux store
connection.on('OperationProgress', onNotifReceived);
connection.on('UploadProgress', onNotifReceived);
connection.on('DownloadProgress', onNotifReceived);
// re-establish the connection if connection dropped
connection.onclose(() => setTimeout(startSignalRConnection(connection), 5000));
startSignalRConnection(connection);
}
return next(action);
};
export default signalRMiddleware;
And inside my store.js file
import signalRMiddleware from '../middlewares/signalRMiddleware';
...
createStore(rootReducer, {}, composeEnhancers(applyMiddleware(signalRMiddleware)));

Resources