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
Related
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(), [] );
Am using server sent events in an express server like this;
const sendEventDashboard = async (req, res) => {
try {
const orders = await Order.find({ agent_id: req.params.id })
.populate("agent_id")
.sort({ _id: -1 });
res.writeHead(200, {
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
Connection: "keep-alive",
});
const sseId = new Date().toDateString();
const intervalId = setInterval(() => {
writeEvent(res, sseId, JSON.stringify(orders));
}, SEND_INTERVAL);
res.on("close", () => {
clearInterval(intervalId);
res.end();
// console.log("Client closed connection browser");
});
} catch (error) {
console.log(error);
}
};
export const getOrdersStreamDashboard = async (req, res) => {
if (req.headers.accept === "text/event-stream") {
sendEventDashboard(req, res);
} else {
res.json({ message: "Okay" });
}
};
and this is how i use it in a react app using a useEffect hook;
useEffect(() => {
const es = new EventSource(
`${process.env.REACT_APP_SERVER_URL}/weborders/${agentId}/stream_dashboard`
);
es.addEventListener("open", () => {
console.log("Dashboard stream opened!");
});
es.addEventListener("message", (e) => {
const data = JSON.parse(e.data);
setTrackOrderCount(data);
});
return () => {
// es.removeAllEventListeners();
es.close();
es.removeEventListener("message", (e) => {
const data = JSON.parse(e.data);
setTrackOrderCount(data);
});
};
}, [trackOrderCount]);
Everything runs as desired apart from event source always running until when the app/browser crushes. I get no error when it stops running and have to refresh for it to start again. This happens like after 10mins of inactivity or being on that same page for a long duration. Is there a way I can only run sse only when the state in the server is different from that of the client because i think the browser crushes because server sent events continuously run even when there's no event. I tried to remove the dependency array [trackOrderCount] in the useEffect and the setInterval in the server but that didn't solve the issue.
The solution might be in comparing the local and global versions before the event is sent but i've failed to figure out where to put that logic! I the browser's console, this is what i get;
and this will run for sometime then crush!
I'm a newbie in websockets.
I use urql and graphql-ws (migrated from subscriptions-transport-ws) to get graphql subscriptions. The code is following:
export const useUrqlClient = () => {
const headers = useHeaders();
const client = useMemo(() => createUrqlClient(headers), [headers]);
return client;
};
const createUrqlClient = (headers: any = defaultHeaders) => {
return createClient({
url: GRAPHQL_ENDPOINT,
fetchOptions: {
headers
},
exchanges: [
...defaultExchanges,
subscriptionExchange({
forwardSubscription: (operation) => {
return {
subscribe: (sink) => ({
unsubscribe: wsClient(headers).subscribe(operation, sink)
})
}
}
})
]
});
};
const wsClient = (headers: any) => createWSClient({
url: WS_GRAPHQL_ENDPOINT,
connectionParams: () => ({
headers
})
});
const useHeaders = () => {
const [authHeader, setAuthHeader] = useState<object>({});
const { selectedToken } = useAuth();
useEffect(() => {
if (selectedToken) {
setAuthHeader(selectedToken ? { authorization: `Bearer ${selectedToken}` } : {});
}
}, [selectedToken]);
return {
...defaultHeaders,
...authHeader
};
};
Everything works fine BUT is it okay that separate websocket connection is opened for each subscription?
They are closed on leaving the page and another are created but is it expected?
Note: the same behaviour was on approach using subscriptions-transport-ws.
You're running the client in lazy mode which establishes WebSocket connections on-demand, this is the default.
Consider one of the 2 options:
Use the client in lazy = false mode which will establish a connection with the server on create and keep it alive
Use the client's lazyCloseTimeout option which will delay closing the connection in case of new subscriptions.
I am working on a project for an interview and have been asked to create a NextJS app using Socket.io for realtime chat. I have the chat functionality working, but one of my requirements is to have an area where a user can see a list of current users. While I've found examples for Express servers, I cannot seem to work out how to do this using Next's API system. I have two connected issues:
Maintaining a list of users with chosen display names (not just the socket id)
Accessing and returning a current user list whenever a user joins or leaves.
I haven't had any luck scanning the docs.
Here is the server function:
import { NextApiRequest } from 'next';
import { NextApiResponseServerIO } from '../../types/next';
import { Server as ServerIO } from 'socket.io';
import { Server as NetServer } from 'http';
export const config = {
api: {
bodyParser: false
}
}
export default async (req: NextApiRequest, res: NextApiResponseServerIO) => {
if (!res.socket.server.io) {
console.log("** New Socket.io server **")
// adapts the Next net server to http server
const httpServer: NetServer = res.socket.server as any;
const io = new ServerIO(httpServer, {
path: '/api/socketio'
})
io.on('connect', async (socket) => {
socket.join('main')
// where I plan to put the code to send a current list
})
io.on('disconnect', socket => {
socket.leave('main')
})
res.socket.server.io = io;
}
res.end();
}
And the related client code:
useEffect((): any => {
const url = process.env.NEXT_BASE_URL as string | "";
// connect to socket server
const socket = io(url, {
path: "/api/socketio",
});
// log socket connection
socket.on("connect", () => {
dispatch(connect(socket.id));
dispatch(updateId(socket.id))
});
//updates chat on message dispatch
socket.on("message", (message: IMsg) => {
dispatch(receive(message));
});
socket.on('updateUsersList', (users) => {
console.log("Is this the users", users)
})
//server disconnect on unmount
if (socket) return () => dispatch(disconnect(socket));
}, []);
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