state used inside socket.on keeps its default value - reactjs

const [allUsers, setUsers] = useState<ChatUser[]>([]);
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
console.log("all users : ", allUsers);
}, [allUsers]);
useEffect(() => {
const sessionID = localStorage.getItem("sessionID");
if (sessionID) {
socket.auth = { sessionID, username: "AOA_A", isAOA_A: true };
socket.connect();
} else {
socket.auth = { username: "AOA_A" };
socket.connect();
socket.on("connect error", (err) => {
if (err.message === "invalid username") {
console.log("error");
}
});
}
socket.on("session", ({ sessionID, userID }) => {
console.log("session");
// attach the session ID to the next reconnection attempts
socket.auth = { sessionID };
// store it in the localStorage
localStorage.setItem("sessionID", sessionID);
// save the ID of the user
socket.userID = userID;
});
socket.on("users", (data: ChatUser[]) => {
setUsers(() => {
const _users_: any[] = [];
data.forEach((user: ChatUser) => {
user.self = user.userID === socket.userID;
user.messages = [];
/* initReactiveProperties(user); */
_users_.push(user);
});
// put the current user first, and sort by username
_users_.sort((a, b) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
return _users_;
});
});
socket.on("private message", ({ message, from }) => {
console.log("setting messages", allUsers.length);
for (let i = 0; i < allUsers.length; i++) {
const user = allUsers[i];
if (user.userID === from) {
user.messages.push({
content: message.content,
from: from,
dateTime: "",
});
const _allUsers = [...allUsers];
_allUsers[currentUser.key] = currentUser!.user;
setUsers(_allUsers);
// if (user !== this.selectedUser) {
// user.hasNewMessages = true;
// }
break;
}
}
});
return () => {
socket.off("connect");
socket.off("disconnect");
socket.off("users");
socket.off("user connected");
socket.off("user disconnected");
socket.off("private message");
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
I want to setup a private messaging with MERN stack an socket.io .
At component rendering , it connect to server an the server will emit the list of all actif users , at the this point , the state is successfully updated , the first useEffect will log the updated state but when the socket.on("privated message") is called , when log the value of allUsers inside it is empty.

Related

Expected first argument to collection() to be a CollectionReference, a DocumentReference or FirebaseFirestore - REactJS-Firebase-Firestore

I am trying to write to my database in reactjs using firebase, but I keep getting this error:
Expected first argument to collection() to be a CollectionReference, a DocumentReference or FirebaseFirestore
Here is my db code structure:
const waitListCol = collection(serchDB, DBTypes.users);
class SerchDS {
addUsers = (details) => {
return waitListCol.path;
}
updateUser = (id, updated) => {
const email = doc(serchDB, DBTypes.users, id);
return updateDoc(email, updated);
}
deleteUser = (id) => {
const email = doc(serchDB, DBTypes.users, id);
return deleteDoc(email);
}
getAllUsers = () => {
return getDocs(waitListCol);
}
getUser = (id) => {
const user = doc(serchDB, DBTypes.users, id);
return getDoc(user);
}
}
Here is how I passed the function:
const handleWaitlist = async (e) => {
e.preventDefault();
setError("");
if(username.length <= 2){
setError({error: true, msg: "Username is too short", bad: true});
return;
} else if(username.length > 2){
setLoading(true);
const id = SerchID();
const details = {email, username, id}
setSnack(true);
console.log(details);
setSnackMsg("You made it to the game!");
await SerchDS.addUsers(details);
console.log(SerchDS())
try {
// const col = collection(serchDB, DBTypes.users);
// const docRef = await setDoc(col, {
// emailAddress: email,
// userName: username,
// userID: SerchID()
// });
// await SerchDS.addUsers(email, username);
// console.log("Document written with ID: ", docRef.id);
} catch (e) {
console.error("Error adding document: ", e);
}
setLoading(false);
// closeWaitList();
}
export default new SerchDS();
So, I have my db class:
const waitListCol = collection(serchDB, DBTypes.users);
class SerchDS {
addUsers = (details) => {
return waitListCol.path;
}
updateUser = (id, updated) => {
const email = doc(serchDB, DBTypes.users, id);
return updateDoc(email, updated);
}
deleteUser = (id) => {
const email = doc(serchDB, DBTypes.users, id);
return deleteDoc(email);
}
getAllUsers = () => {
return getDocs(waitListCol);
}
getUser = (id) => {
const user = doc(serchDB, DBTypes.users, id);
return getDoc(user);
}
}
export default new SerchDS();
and here is the function implementation:
const handleWaitlist = async (e) => {
e.preventDefault();
setError("");
if(username.length <= 2){
setError({error: true, msg: "Username is too short", bad: true});
return;
} else if(username.length > 2){
setLoading(true);
const id = SerchID();
const details = {email, username, id}
setSnack(true);
console.log(details);
setSnackMsg("You made it to the game!");
await SerchDS.addUsers(details);
console.log(SerchDS())
try {
// const col = collection(serchDB, DBTypes.users);
// const docRef = await setDoc(col, {
// emailAddress: email,
// userName: username,
// userID: SerchID()
// });
// await SerchDS.addUsers(email, username);
// console.log("Document written with ID: ", docRef.id);
} catch (e) {
console.error("Error adding document: ", e);
}
setLoading(false);
// closeWaitList();
}

How can I make my function wait until the user data is updated in my useContext provider?

new to react here, I want a new user to enter their details on their first sign in. This includes enterting a username, name, profile picture etc.
When they have submitted their details, I wait for confirmation from firebase and then I want to forward them to their profile (the link structure is domain/p/:username).
However, every time I try it, it ends up trying to head to domain/p/undefined?
When I use react dev tools to inspect, I can see that the username was successfully sent up to my state provider, so I think it's just a matter of timing thats the problem.
Heres the welcome page functions:
//The first method begins the update and checks if the username already exists.
const update = async (e) => {
if (
firstName.trim() === "" ||
lastName.trim() === "" ||
username.trim() === "" ||
bio.trim() === "" ||
addressOne.trim() === "" ||
city.trim() === "" ||
county.trim() === "" ||
postCode.trim() === "" ||
photos.length === 0
) {
window.alert("Invalid data!\nOnly Address line 2 can be empty");
} else {
var usernameRef = db
.collection("users")
.where("username", "==", username);
usernameRef.get().then((docs) => {
if (docs.size === 1) {
docs.forEach((doc) => {
if (doc.id === currentUser.uid) {
sendUpdate();
} else {
window.alert("Username taken");
}
});
} else {
sendUpdate();
}
});
}
};
//This method puts the initial data into firebase except the profile picture
function sendUpdate() {
setLoading("loading");
db.collection("users")
.doc(currentUser.uid)
.set(
{
username: username,
name: firstName,
surname: lastName,
bio: bio,
address1: addressOne,
address2: addressTwo,
notifications: [],
city: city,
county: county,
postcode: postCode,
newUser: false,
},
{ merge: true }
)
.then(() => {
updatePhoto();
})
.catch((err) => console.log(err));
}
//This method uploads the profile picture, then gets the downloadURL of the photo just uploaded and puts it into the user document created in method 2.
//It also trys to send the user to their profile afterwards, but it always ends up as undefined.
const updatePhoto = async () => {
const promises = [];
var userREF = db.collection("users").doc(currentUser.uid);
photos.forEach((photo) => {
const uploadTask = firebase
.storage()
.ref()
.child(
`users/` + currentUser.uid + `/profilePicture/profilePicture.jpg`
)
.put(photo);
promises.push(uploadTask);
uploadTask.on(
firebase.storage.TaskEvent.STATE_CHANGED,
(snapshot) => {
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
if (snapshot.state === firebase.storage.TaskState.RUNNING) {
console.log(`Progress: ${progress}%`);
}
},
(error) => console.log(error.code),
async () => {
const downloadURL = await uploadTask.snapshot.ref.getDownloadURL();
userREF
.update({
profilePicture: downloadURL,
})
.then(async () => {
updateUserData().then(() => {
setLoading("complete");
setTimeout(() => {
history.push("/p/" + userData.username);
}, 3000);
});
});
}
);
return "completed";
});
};
Here is my AuthContext provider: (the function UpdateUserData() is what updates the data after its been put into firebase)
import React, { useContext, useState, useEffect } from "react";
import { auth, db } from "../firebase";
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [userData, setUserData] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
async function updateUserData() {
if (currentUser) {
var userData = db.collection("users").doc(currentUser.uid);
await userData
.get()
.then((doc) => {
if (doc.exists) {
setUserData(doc.data());
return "success";
}
})
.catch((error) => {
console.log("Error getting document:", error);
return "error";
});
}
}
function logout() {
setUserData();
return auth.signOut();
}
function resetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email);
}
function updatePassword(password) {
return currentUser.updatePassword(password);
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
if (user) {
var userData = db.collection("users").doc(auth.currentUser.uid);
userData
.get()
.then((doc) => {
if (doc.exists) {
setUserData(doc.data());
}
})
.catch((error) => {
console.log("Error getting document:", error);
});
}
});
return unsubscribe;
}, []);
const value = {
currentUser,
userData,
updateUserData,
login,
signup,
logout,
resetPassword,
updateEmail,
updatePassword,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
And as you can see, once the undefined page has been attempted to load, we can see the username did in fact end up in userData from my context provider:
TIA!
You can resolve this issue by move the redirect link out side of you updatePhoto and put it in useEffect (or any other option base on code flow) then just set an state or check the needed data like userdata.userName is already exists, if its undefined prevent redirect and you can display loader component for example, else execute redirect...
Basic Example:
useEffect(() => {
if(userData.username){
history.push("/p/" + userData.username);
}
}, [userData.username])
const myUpdateFunction = useCallBack(() => {
fetch().then(v => {
setUserData(v);
})
}, [])

Socket.io connected property is always false

I am trying to create a private messaging app. The socket connects at first but then when I try to emit any event from the client side, it shows that socket.connected property is false.
Please help me out.
Here's is my client side code, Please note that socket.on("users") part works correctly because all of it happens when the socket it connected. It means the connection part is happening correctly. After that whenever I try to call a function that emits a socket event, it shows that socket.connected property is false and doesnt do anything.
Any help would be appreciated.
var connectionOptions = {
transports: ["websocket"],
autoConnect: false,
};
socket = io("http://localhost:3001", connectionOptions);
socket.on("connection _error", (err) => {
if (err.message === "invalid username") {
console.log("ERROR");
}
});
socket.on("users", (users) => {
users.forEach((user) => {
user.self = user.userID === socket.id;
//initReactiveProperties(user);
});
socket.on("user connected", (user) => {
// TODO
setUsers((existingusers) => [...existingusers, user]);
console.log(user);
});
// put the current user first, and then sort by username
users = users.sort((a, b) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
//console.log(users);
});
socket.on("private message", ({ content, from }) => {
console.log(content);
});
useEffect(() => {
const username = localStorage.getItem("username");
console.log(username);
socket.auth = { username };
socket.connect();
}, []);
function SendMessage() {
socket.emit("test", "hello");
// selectedChatUser
console.log(socket.connected);
if (selectChatUser) {
socket.emit("private message", {
content: "hello there",
to: selectChatUser.userID,
});
console.log("Message Sent");
}
}
And here is my server side code:
const app = require("express")();
const httpServer = require("http").createServer(app);
const cors = require("cors");
app.use(cors());
const options = {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
};
const io = require("socket.io")(httpServer, options);
io.use((socket, next) => {
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid usernmae"));
}
socket.username = username;
next();
});
io.on("connect", (socket) => {
console.log("New connection");
const users = [];
for (let [id, socket] of io.of("/").sockets) {
users.push({
userID: id,
username: socket.username,
});
}
socket.broadcast.emit("user connected", {
userID: socket.id,
username: socket.username,
});
socket.emit("users", users);
socket.on("test", () => {
console.log("test");
});
socket.on("private message", ({ content, to }) => {
console.log(content);
console.log("hello there");
socket.to(to).emit("private message", {
content,
from: socket.id,
});
});
});
httpServer.listen(3001, () => {
console.log("Server has started");
});
// https://socket.io/
Following line will re-run every time your component renders, losing reference to the socket that was actually connected:
socket = io("http://localhost:3001", connectionOptions);
You can use a ref to persist it between renders:
const socketRef = useRef();
socketRef.current = socket;
// use socketRef.current everywhere else in your code

the problem: the code below work flawlessly in dev but after uploaded to Heroku it's working sometimes,

I am trying to get socket.io to work with Heroku but it doesn't play well.
the problem: the code below work flawlessly in dev but after uploaded to Heroku it's working sometimes,
1.don't see anything weird on logs.
2.the data saved to DB and will appear after refresh
3.refresh helps to sockets sometimes.
4.there is no pattern to it, sometime it will work ok for an hour and some time won't last a minute
5.heroku features:enable http-session-affinity also done
server:
const mongoose = require("mongoose");
const Rooms = mongoose.model("Rooms");
const Chat = mongoose.model("Chats");
const jwt = require("jwt-then");
const socketChat = (app, io) => {
io.use(async (socket, next) => {
try {
const token = socket.handshake.query.token;
const payload = await jwt.verify(token, process.env.SECRET_KEY);
socket.userId = payload.id;
socket.id = payload.id;
socket.name = payload.username;
console.log({ socketisd: socket.userId, name: socket.name });
next();
} catch (err) { }
});
io.on("connection", (socket) => {
console.log("Connected: " + socket.name);
socket.on("disconnect", () => {
console.log("Disconnected: " + socket.name);
});
socket.on("joinRoom", async ({ roomId },callback) => {
socket.join(roomId);
console.log(` ${socket.name} joined room: ` + roomId);
socket.to(roomId).emit("live", { name: socket.name, live: true, roomId });
callback({
status: "ok"
});
});
socket.on("leaveRoom", async ({ roomId },callback) => {
socket.leave(roomId);
console.log(` ${socket.name} left room: ` + roomId);
socket.to(roomId).emit("live", { name: socket.name, live: false, roomId });
callback({
status: "ok"
});
});
socket.on("typing", async ({ msg, roomId }) => {
let name = "";
if (msg.text && msg.text.trim().length > 0) {
let length = msg.text.length;
name = length > 0 ? socket.name : "";
}
socket.to(roomId).emit("typingclient", { name });
});
socket.on(
"chatroomMessage",
async ({ roomId, message, name, profileImg, timestamp, type, date }) => {
if (message.trim().length > 0) {
io.to(roomId).emit("newMessage", {
roomId,
user: socket.userId,
message,
name,
type,
date,
profileImg,
timestamp,
recived: true,
});
let room = await Rooms.findById(roomId).populate("messages");
if (type === "reject") {
await Chat.findOneAndUpdate(
{ roomId, type: "dateConfirm" },
{ type: "reject", message },
{ new: true }
);
}
else {
const newMessage = new Chat({
roomId,
date,
type,
user: socket.userId,
message,
name,
profileImg,
timestamp,
recived: true,
});
await newMessage.save();
room.messages.push(newMessage);
await room.save();
}
let theOtherGuy =await room.users.find((user) => user != socket.userId);
io.to(theOtherGuy).emit("room", room);
}
}
);
});
};
module.exports = socketChat;
client:
/**
* Sends message with emit socket to server
* #param {Object} event Default Browser Event Object
* #param {String} text content of message
* #param {String} date Date for schedualing
* #param {String} type type of the message (reject,request etc...)
*/
const sendMessage = (event, text, date = null, type = null) => {
event && event.preventDefault();
if (socket) {
socket.emit("chatroomMessage", {
roomId,
date,
type,
name: currentUser.user.username,
profileImg: currentUser.user.profileImageUrl,
timestamp: new Date(),
recived: false,
message: text,
});
setText("");
socket.emit("typing", {
msg: "",
roomId,
});
}
};
React.useEffect(() => {
if (socket) {
socket.emit("joinRoom", {roomId},(answer)=>
console.log("joinRoom",roomId,answer)
);
socket.on("newMessage", (message) => {
console.log({message})
if (message.type === "reject")
setMessages((prevMessages) => [...prevMessages.filter(m => m.type !== 'dateConfirm'), message]);
else
setMessages((prevMessages) => [...prevMessages, message]);
});
socket.on("live", (message) => {
console.log(message)
message.live ? setSucess(`user ${message.name} has connected`) : setErr(`user ${message.name} has left`)
});
socket.on("typingclient", (name) => {
setTyping(name);
});
}
return () => {
if (socket) {
socket.emit("leaveRoom", {roomId},(answer)=>
console.log("leaveRoom",roomId,answer)
);
}
//Component Unmount
};
//eslint-disable-next-line
}, [socket]);
and main where i define my socket:
const [socket, setSocket] = React.useState(null);
const setupSocket = () => {
console.log("socket4")
const token = sessionStorage.getItem("jwtToken");
if (token && !socket) {
const newSocket = io("/", {
query: {
token: sessionStorage.getItem("jwtToken"),
},
path: '/socket'
});
newSocket.on("disconnect", () => {
// setSocket(null);
// makeToast("error", "Socket Disconnected!");
});
newSocket.on("connect", () => {
// makeToast("success", "Socket Connected!");
console.log("Socket Connected");
});
setSocket(newSocket);
}
};
React.useEffect(() => {
if (currentUser && !socket) setupSocket();
//eslint-disable-next-line
}, [currentUser, socket]);
ststic.json:
{
"root":"build/",
"routes":{
"/**":"index.html"
},
"proxies":{
"/api/":{"origin":"${API_URL}"},
"/socket/":{"origin":"${SOCKET_URL}"}
}
}
it looks like the Io object did not like
socket.id = payload.id;
I have removed it and everything is working now.
I think it may have resulted from different keys in the Io object resulting in unexpected behavior.

Trying to modify a data from a React Promise Response changes globally

I have created a codesandbox with a simplified version of my problem
https://codesandbox.io/s/new-react-context-api-ei92k
I get something from a fetch (in this case a user)
I then create a local copy of this user and make some changes to it
The problem: Any changes update my initial user object
Can someone tell me how this is possible? and how can I avoid this?
import React, { useState, useEffect } from "react";
import { AppSessionContext } from "./AppContext";
import Header from "./Header";
const user = {
userName: "jsmith",
firstName: "John",
lastName: "Smith",
isAdmin: true
};
const loadProfile = () => Promise.resolve(user);
function createUserWithNewName(userToUpdate) {
userToUpdate["userName"] = "Dummy";
return userToUpdate;
}
const App = () => {
const [user, setUser] = useState({});
const [Loaded, setLoaded] = useState(false);
var amendedUser = {};
useEffect(() => {
loadProfile()
.then(user => {
setUser(user);
console.log(user);
})
.then(() => {
amendedUser = createUserWithNewName(user);
console.log(amendedUser);
console.log(user);
})
.then(setLoaded(true));
}, []);
if (!Loaded) {
return "Loading";
}
return (
<AppSessionContext.Provider value={{ user }}>
<div className="App">
<Header />
</div>
</AppSessionContext.Provider>
);
};
export default App;
snippet of production code
loadTableDefault() {
fetch(defaultUrl(), {method: 'GET'})
.then(res => res.json())
.then(response => {
this.setState({
data: response,
})
return response
})
.then(response => {
this.setState({
table_data: formatResponsePretty(response),
})
})
.catch(error => console.error('Error:', error));
}
formatResponsePretty
export function formatResponsePretty(oldData) {
const newData = {
...oldData,
};
// consider re-writting the flask response to this format
const obj = { allocations: [] };
var theRemovedElement = ''
var ports = []
ports = Object.values(newData['allocations']['columns']);
ports.shift();
var dataArray = ['allocations', 'conditions', 'liquidity', 'hedging']
for (const index of dataArray) {
for (const i of newData[index]['data']) {
theRemovedElement = i.shift();
if (index === 'allocations') {
obj[index][theRemovedElement] = i
}
else {
obj[theRemovedElement] = i;
}
}
}
const rows = []
let index = 0;
Object.keys(obj).forEach(element => {
index = formatting.findIndex(x => x.name === element)
if (formatting[index] && formatting[index]['type'] === 'number') {
var new_obj = obj[element].map(function (el) {
return Number(el * formatting[index]['multiplier']).toFixed(formatting[index]['decimal']) + formatting[index]['symbol']
})
rows.push(new_obj)
}
else if (formatting[index] && formatting[index]['type'] === 'string') {
rows.push(obj[element])
}
else if (formatting[index] && formatting[index]['type'] === 'boolean') {
// there should be logic here to display true or false instead of 1 and 0
// this could be in the upload
rows.push(obj[element])
}
else {
rows.push(obj[element])
}
})
const arrOfObj = createRecords(ports, rows)
return {obj: obj, ports: ports, rows: rows, arrOfObj: arrOfObj}
}
In createUserWithNewName() you are updating the original user object and returning it.
You instead want to create a new object with all the old user properties, but with just the username changed. Thankfully, object destructuring makes this super easy:
function createUserWithNewName(oldUser) {
const newUser = {
...oldUser,
userName: 'Dummy',
};
return newUser;
}
This will copy all the properties of oldUser to a new object and then just update userName!
You're also going to want to pass user down to that second .then() as it won't currently be available in there:
.then(user => {
setUser(user);
console.log(user);
return user;
})
.then(user => {
amendedUser = createUserWithNewName(user);
console.log(user, amendedUser);
})
Update CodeSandbox link: https://codesandbox.io/s/new-react-context-api-tgqi3

Resources