This is part of my code, what I want to do is this component at any time can receive a message on any of the conversations. Sending a message triggers a Socket event which triggers this code below, but I can't seem to get the "latest" conversations, as the useEffect only triggers when the component mounts (at that point my conversations array has zero length).
What I was thinking is that I should include "conversations" on the useEffect's dependency but that would create multiple websocket connection, one each time a Socket.io event is triggered because it does change the state. Is this the best solution? Thanks in advance!
const [conversations, setConversations] = useState<Array<Conversations>>([]);
useEffect(() => {
async function getConversations() {
try {
const { data } = await axios.get("/api/conversations/");
if (data.success) {
setConversations(data.details);
}
} catch (err) {}
}
getConversations();
socketInstance.on("connect", () => {
console.log("Connecting to Sockets...");
socketInstance.emit("authenticate", Cookies.get("token") || "");
});
socketInstance.on("ackAuth", ({ success }) => {
console.log(
success
? "Successfully connected to Sockets"
: "There has been an error connecting to Sockets"
);
});
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
(conv: Conversations) => {
return conv.conversationId === data.conversationId;
}
);
modifiedConversation.messages.push({
from: {
firstName: data.firstName,
lastName: data.lastName,
profilePhoto: data.profilePhoto,
userId: data.userId,
},
content: data.content,
timeStamp: data.timeStamp,
});
const updatedConversations = [
...conversations.filter(
(conv) => conv.conversationId !== data.conversationId
),
modifiedConversation,
];
setConversations(updatedConversations);
});
}, []);
While attaching and removing the socket listeners every time conversations changes is a possibility, a better option would be to use the callback form of the setters. The only time you reference the state, you proceed to update the state, luckily. You can change
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
to
socketInstance.on("newMessage", (data) => {
setConversations(conversations => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
You should also not mutate the state, since this is React. Instead of
modifiedConversation.messages.push({
do
const modifiedConversationWithNewMessage = {
...modifiedConversation,
messages: [
...modifiedConversation.messages,
{
from: {
// rest of the object to add
Related
I'm working with WebSocket and having an issue with a function showing incorrect data, All my code used to work with a Class-based component, I'm only trying to convert a class component that actually calls the connect method of Websocket to a functional-based component. The Websocket is class-based for instance.
So I have a WebSocket which on socket New Message sets callbacks.
socketNewMessage(data) {
console.log(data);
const parsedData = JSON.parse(data);
const command = parsedData.command;
if (command === "userChatGroups") {
this.callback[command](parsedData.chatGRoups);
}
if (command === "new_userChatGroups") {
this.callback[command](parsedData.chatGRoup);
}
}
and here are the callbacks defined -
addCallbacks(userChatGroups, newUserChatGroup) {
this.callback["userChatGroups"] = userChatGroups;
this.callback["new_userChatGroups"] = newUserChat;
}
and then websocket finally return -
const WebSocketInstance = Websocketservice.getInstance();
export default WebSocketInstance;
Now the class-based component which I'm trying to convert to functional based -
let's call this component Chats-
this calls the connect method and a Websocket instance is returned.
useEffect(() => {
if (loggedInUserDetail) {
WebSocketInstance.connect(loggedInUserDetail[0].id);
}
}, [loggedInUserDetail]);
I have a helper function which checks the status of websocket -
function waitForSocketConnection(callback) {
setTimeout(() => {
if (WebSocketInstance.state() === 1) {
console.log("connection is secure");
callback();
return;
} else {
console.log("waiting for connection");
waitForSocketConnection(callback);
}
}, 10);
}
and I check the status and map my callbacks - here is the problem-
useEffect(() => {
waitForSocketConnection(() => {
WebSocketInstance.addCallbacks(
setChatGroups,
addNewChatGroup
);
});
}, [loggedInUserDetail]);
I have a state to manage CHatGroups -
const [groups, setGroups] = useState([]);
the setChatGroups (which initially loads all the groups the users are associated with works fine and sets the state of chatsGroups) as -
const setChatGroups = useCallback((userChatGroups) => {
setGroups(userChatGroups); //whatever data it recieved from the websocket.
}, []);
but the function addNewChatGroup always shows groups value as an empty array [] (it was updated earlier with setChatGroups). If I manually check the value of groups it is an array of n length, but in addNewChatGroup function, it always shows an empty array with the initial value [].
const addNewChatGroup = useCallback(
(newCHatGroup) => {
console.log(groups); **error -> this is always empty array**
// here I have to update. add the received value with the previous
},
[groups] // callbacks used hoping this will bind
);
In the class-based component I used to set callbacks on the constructor and used to bind with this, but I'm not able to do it here, can anyone help what I'm missing?
I'm sure it is a binding issue. maybe. May I know the reason for this binding failure?
Well if I understood correctly your problem, it could be link to multiple things.
The problem actually is I don't have a clear view on all your components, maybe you can try to paste a sandbox link or something like that with a "simple structure".
I tried to reproduced a typescript version, I don't know if it could help:
class MyWebSocket {
private static _instance: MyWebSocket;
public callbacks: any = {};
public connected: boolean = false;
public socketNewMessage(data: any): void {
const parsedData = JSON.parse(data);
console.log('new message received:', parsedData);
const command = parsedData.command;
if (command === "new_userChatGroups") {
this.callbacks[command](parsedData.newGroupAdded);
}
}
public addCallbacks(elements: {command: string, func: Function}[]) {
console.log('adding callbacks...', elements);
elements.forEach(element => {
this.callbacks[element.command] = element.func;
});
}
public connect(): void {
setTimeout(() => this.connected = true, 1100);
}
public static getInstance(): MyWebSocket {
return this._instance || (this._instance = new MyWebSocket());
}
}
class SocketUtils {
static waitForSocketConnection(callback: any): void {
const waitingInterval = setInterval(() => {
if (MyWebSocket.getInstance().connected) {
console.log('socket is connected! processing callback...');
clearInterval(waitingInterval);
callback();
return;
} else {
console.log('socket is not connected after 1sec, waiting...');
}
}, 1000);
}
}
class Chat {
groups: string[] = ['group of JStw'];
new_userChatGroups(group: string) {
this.groups.push(group);
}
}
class Main {
constructor() {
const myChat = new Chat();
MyWebSocket.getInstance().connect();
// waiting connections.
SocketUtils.waitForSocketConnection(() => {
console.log('waitForSocketConnection is triggered, adding callbacks...');
// adding callbacks
MyWebSocket.getInstance().addCallbacks([{command: 'new_userChatGroups', func: myChat.new_userChatGroups.bind(myChat)}]);
});
// Waiting 5min to dispatch an message
setTimeout(() => {
// testing eventing after getting connection
MyWebSocket.getInstance().socketNewMessage(JSON.stringify({command: 'new_userChatGroups', newGroupAdded: 'group of Ranu Vijay'}));
}, 5000);
setTimeout(() => {
console.log('program finished, results of chat groups:', myChat.groups);
}, 10000);
}
}
new Main();
Output:
I'm more specialized on functional component by using react so without a link to investigate all your code, it will be complex to help.
I don't know if you are using a library, but there is this one: https://www.npmjs.com/package/react-use-websocket which seems to be really ultra simple to use without managing socket connection/disconnection.
For me if I had to implement it, I would say:
Component ChatGroup which is using websocket hook const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl); and contains the state groups, setGroups.
Component Chat which can use the sendMessage from props of ChatGroup component and call it from this component if a join a group.
Then your "parent" component is managing the state and is controlling the data.
I'm currently building a simple chat feature in my app using redux saga. Chat successfully entered from the emit server to the client
But the problem is, so when I move from the chat page and then back to the chat page, the last chat data sent will be duplicated to 2. Then when I move again and return to the chat page, the sent chat will be 3 and so on.
i think this happened maybe because the listener loop on fetchKonsultasi() function was stacked
Here is a dispatch function onGetKonsultasi() to listen to emits and onGetListKonsultasi() to retrieve chat list data from the server. Chat data is stored in the dataListKonsultasi, when there is an emit from the server, the dataListKonsultasi state will be updated
Someone help me, I really appreciate your help
index.js
useEffect(() => {
onGetListKonsultasi()
onGetKonsultasi()
}, [])
------
Konsultasi.propTypes = {
dataKonsultasi: PropTypes.object,
dataListKonsultasi: PropTypes.object,
onGetKonsultasi: PropTypes.func,
onGetListKonsultasi: PropTypes.func,
}
const mapStateToProps = ({ konsultasiReducer }) => ({
dataKonsultasi: konsultasiReducer.dataKonsultasi,
dataListKonsultasi: konsultasiReducer.dataListKonsultasi,
})
const mapDispatchToProps = dispatch => ({
onGetKonsultasi: () => dispatch(getKonsultasi()),
onGetListKonsultasi: () => dispatch(getListKonsultasi()),
})
export default connect(mapStateToProps, mapDispatchToProps)(Konsultasi)
saga.js
function createSocketConnection(url) {
return io(url)
}
function createSocketChannel(socket) {
return eventChannel(emit => {
const eventHandler = event => {
emit(event)
}
const errorHandler = errorEvent => {
emit(new Error(errorEvent.reason))
}
socket.on("AdminReceiveMessage", eventHandler)
socket.on("error", errorHandler)
const unsubscribe = () => {
socket.off("AdminReceiveMessage", eventHandler)
}
return unsubscribe
})
}
function* fetchKonsultasi() {
const socket = yield call(createSocketConnection, env.SOCKET_URL)
const socketChannel = yield call(createSocketChannel, socket)
while (true) {
try {
const response = yield take(socketChannel)
yield put(updateListKonsultasiSuccess(response))
} catch (err) {
console.log("socket error: ", err)
}
}
}
function* fetchListKonsultasi() {
try {
const response = yield call(getListKonsultasi)
yield put(getListKonsultasiSuccess(response))
} catch (error) {
yield put(getListKonsultasiFail(error))
}
}
export function* watchSocket() {
yield takeEvery(GET_KONSULTASI, fetchKonsultasi)
yield takeEvery(GET_LIST_KONSULTASI, fetchListKonsultasi)
}
function* konsultasiSaga() {
yield all([fork(watchSocket)])
}
reducer.js
case GET_LIST_KONSULTASI_SUCCESS:
return {
...state,
dataListKonsultasi: action.payload,
}
case UPDATE_LIST_KONSULTASI_SUCCESS:
return {
...state,
dataListKonsultasi: {
users: { ...state.dataListKonsultasi.users },
data: [...state.dataListKonsultasi.data, action.payload],
},
dataListKonsultasi Structure :
{
"users": {
......
},
"data": [
{
..chat_data...
},
{
..chat_data...
},
]
}
acutely the problem come from when you change tab with your router you didn't lost your data that's you add into your store in socket connection, then each time you come into your page you get new data and combine last data with new data and that's happening.
good way to solve this problem is, create an action to clear data list from store and call this action inside of closing socket connection, by this approach every time you lose connection with your socket you clear data and re write data with connecting again.
case CLEAR_LIST:
return {
...state,
dataListKonsultasi: {
users: {},
data: [],
},
}
and then
socket.on("disconnect", () => {
// call your clear action here
});
Hey there Muhammand so over here is where I think might help you out
case UPDATE_LIST_KONSULTASI_SUCCESS:
return {
...state,
dataListKonsultasi: {
users: { ...state.dataListKonsultasi.users },
//Here you are always spreading the messages in the store state and
//Then what is happening here is you are adding all the data fetched
// previous data + action.payload from the server so every time you load
//the messages page youre going to be adding a new batch of messages to
//the store state
data: [...state.dataListKonsultasi.data, action.payload],
},
I think try creating a action for clearing your messages data every time you unmount the messages page from the store then whenever you come back to the messages page it should be good Let me know if this helps :D
e.g:
case CLEAR_DATA_LIST: {
return {
...state,
dataListKonsultasi: {
...state.dataListKonsultasi,
data: [],
},
Then either call this when unmounting or in your socket.disconnect and if you want to clear the users data too just handle that according to what you want to acheive :D Might be cool to keep the users data and not clear it then you can always show the amount of users online etc
Problem
The event to send data to renderer is triggered on a file change with chokidar. When the file watcher is triggered and the process sends the event to the renderer.
My problem is that when the EmitterEvent is triggered I input the current state of my useState() variable but only the initial state is passed to my function.
Edited: my problem is that I can not pass the updated data variable to the updateData(newData) function which is called from the emitter inside the preload.js.
Question
How can I pass the state variable data to the call ?
Is there a way that I can change my preload.js in order for the api.receive function to return a string in order not to have to pass a function to the emitter ? (please check the updateData(newData) function for more info)
Is there a better way to achieve this ?
This could also help me to initialize the data for the first render.
preload.js
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
// whitelist channels
let validChannels = ["file", "save"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["file", "save"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
}
electron.js
function ReadNodesFileToIpc(path) {
fs.readFile(path, (error, data) => {
win.webContents.send("file", data);
});
}
Code in component that receives data
function MyForceGraphComponent(){
const [data, setData] = useState({ nodes: [{ id: 0, otherinfo: [] }], links: [] });
var isDataInittiallized = 0;
...
function updateData (newData, data) {
if (data.nodes.length !== 1){
// do stuff
setData({ nodes: data.nodes, links: data.links });
}else{
if (!isDataInittiallized){
setData({ nodes: newData.nodes, links: newData.links });
isDataInittiallized = 1;
}
}
}
...
useEffect(() => {
...
window.api.receive("file", (bytesArray) => {
var newData = JSON.parse(String.fromCharCode.apply(null, bytesArray));
updateData(newData); // Check function bellow
});
...
}, []);
}
updateData(newData) (a function inside my components fuction)
isDataInittiallized is a variable inside the component that the change got passed to the emitter
data is my variable from the useState() function that the change did NOT got passed to the emitter even though the setData() previously changed the data successfully. So the length remains 1 and it contains the same elements from when it was firstly initialized .
Other info
Have tried to play with passing the data variable to the receive function without any success.
Most probably when the emitter is getting set the function passed (the one that does JSON.parse) is getting passed along and never changed after.
Since the issue is related to stale data inside the updateData function, I suggest to make the following updates:
// Simplified for the sake of brevity
function MyForceGraphComponent() {
const [data, setData] = useState({ nodes: [{ id: 0 }] })
// isDataInitialized needs to be tracked just as any other state
const [isDataInitialized, setIsDataInitialized] = useState(false)
// Wrap `updateData` in `React.useCallback` to prevent stale data
const updateData = useCallback(
(nextData) => {
// skip the update, no need to update the state with existing data
if (isDataInitialized) return;
setData({ nodes: nextData.nodes })
setIsDataInitialized(true) // set to `true` to prevent future updates
},
[isDataInitialized, setIsDataInitialized]
)
const handleReceivedData = useCallback(
(bytesArray) => {
const nextData = JSON.parse(...)
updateData(nextData)
},
[updateData]
)
useEffect(() => {
window.api.receive('file', handleReceivedData);
}, [handleReceivedData])
}
Have a look at this example that mimics what you're trying to do:
CodeSandbox
There some error in you code that i found. When you call updateData(newData), data is null, data.nodes will not work. you can modify you code, "if (data
&& data.nodes && data.nodes.length !== 1)"
wondering if anyone can assist me in this matter. I'm following the documentation for https://rnfirebase.io/firestore/usage. it does not work for my use case for some reason.
I just need to set the data, which it works and then read it back so i can push it onto my state and i'll render it.
I just can't read the data back properly. This addItemFunction is trigger when when user click on a button to add.
const addItemFunction = async (numb,exercise) =>{
firestore().collection(userEmail).get().then((snap) =>{
if(!snap.empty){
var finalID = uuid.v4();
firestore().collection(userEmail).doc(final).update({
[finalID]:{
exe:[exercise],
num:[numb],
}
}).then(() =>{
//RETURN SNAPSHOT NOT WORKING
console.log('user_added');
firestore().collection(userEmail).doc(final).onSnapshot(documentSnapshot =>{
console.log("here" + documentSnapshot.data());
});
}
Thanks for your time.
If you are using react with hooks I would suggest you put the onSnapshot listener in a useEffect hook:
useEffect(() => {
const unsubscribe = firestore
.collection(collectionName)
.doc(docId)
.onSnapshot(
(documentSnapshot) => {
const document = documentSnapshot.data();
console.log(document)
},
(error: Error) => {
throw error;
}
);
return () => unsubscribe();
}, [ docId, collectionName]);
this approach will separate concerns and the snapshots will run every time there is a change on the document, then where I put the console.log you could set the document to state.
Another approach will be to use get() instead of onSnapshot like:
const addItemFunction = async (numb,exercise) =>{
firestore().collection(userEmail).get().then((snap) =>{
if(!snap.empty){
var finalID = uuid.v4();
firestore().collection(userEmail).doc(final).update({
[finalID]:{
exe:[exercise],
num:[numb],
}
}).then(() =>{
console.log('user_added');
firestore().collection(userEmail).doc(final).get().then(() => {
console.log("here" + documentSnapshot.data());
})
}
}
}
this approach will not subscribe to changes and it will return the new updated document every time you call the addItemFunction
I have a server backend written in Python with Flask-SocketIO. I'm utilizing it's room feature to make private conversations. Upon a join room event the server fires the following function to let the frontend know where to send messages to specific user:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient}, to=sid)
where sid is the private room created only for the user when connecting to a socket. Then I want to keep this information in React state in a map, like this:
function ChatWindow({ username, token }) {
const [responses, setResponses] = useState([]);
const [roomsMap, setRoomsMap] = useState(new Map());
const [currentRoom, setCurrentRoom] = useState("");
const [messageValue, setMessageValue] = useState("");
var socket = null;
useEffect(() => {
socket = socketIOClient(ENDPOINT);
});
useEffect(() => {
socket.on("global response", (data) => {
setResponses((responses) => [...responses, data]);
});
socket.on("room name response", (data) => {
console.log(`joined ${data.roomName} with ${data.recipient}`);
setCurrentRoom((currentRoom) => data.roomName);
setRoomsMap((roomsMap) => roomsMap.set(data.recipient, data.roomName));
});
return () => socket.close();
}, []);
const sendMessage = () => {
if (messageValue.length < 1) {
return;
}
socket.emit("global message", {
user_name: username,
message: messageValue,
timestamp: Date.now(),
});
setMessageValue("");
};
const joinRoom = (recipient) => {
socket.emit("join", {
token: token,
username: username,
recipient: recipient,
});
// setCurrentRoom(() => roomsMap.get(recipient));
};
const leaveRoom = (recipient) => {
socket.emit("leave", {
token: token,
username: username,
recipient: recipient,
});
const newRooms = roomsMap;
newRooms.delete(recipient);
console.log(`left room with ${recipient}`);
newRooms.forEach((val, key) => console.log(`${val}:${key}`));
setRoomsMap(newRooms);
};
const checkUser = (userToCheck) => {
if (userToCheck === username) {
return styles.chatFromUser;
} else {
return styles.chatToUser;
}
};
return (...);
}
export default ChatWindow;
Sadly, React doesnt react to the socket emitting message, even though it can be seen in network tab in developer tools. The global response works fine.
When I alter the backend function to:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient})
React suddenly works as expected. I'm trying to understand why it happens, especially when the browser seems to see the incoming messages as stated above, so it's most likely my bad coding or some React/Javascript thing.
Thank You for any help in advance.
The problem was that socket sometimes was created multiple times, therefore, the socket that useEffect was currently listening wasn't necessarily the one in the room. So I made one, global socket to fix this and whole thing now works.