Firebase unsubscribe to onSnapshot is not working - javascript

So I'm trying to load multiple elements (RoomItem) and each RoomItem has an onSnapshot listener that listens to real-time changes. Now when I change my Workspace which loads a new set of RoomItems, the previous listeners don't unsubscribe and if there's any update in that RoomItem then react renders that list and not the one which should've been there coming from currentWorkspace.
const [roomLiveStatus, setRoomLiveStatus] = useState(false);
const [unsubscribe, setUnsubscribe] = useState(null);
const getRoomData = (currentWorkspace) => {
const {
roomData,
workspace,
allChannels,
setChannels,
index,
currentUser,
} = props;
const { workspaceId } = workspace;
const workspaceIdLowerCase = workspaceId.toLowerCase();
const { roomId } = roomData;
const roomIdLowerCase = roomId.toLowerCase();
const now = new Date().valueOf();
const query = firebase
.firestore()
.collection(`workspaces/${currentWorkspace.workspaceId}/rooms/${roomId}/messages`);
let unsub;
unsub = query.onSnapshot(
{
includeMetadataChanges: true,
},
function (doc) {
doc.docChanges().forEach((change) => {
if (change.type === "added") {
if (change.doc.data().timestamp >= now) {
console.log("message added ", change.doc.data());
let prevAllChannels = allChannels;
firebase
.firestore()
.collection(`workspaces/${currentWorkspace.workspaceId}/rooms/`)
.doc(`${roomId}`)
.get()
.then((doc) => {
if (doc.exists) {
console.log("updated room data", {
...doc.data(),
roomId: doc.id,
});
prevAllChannels.splice(index, 1, {
...doc.data(),
roomId: doc.id,
lastMessage: change.doc.data(),
});
if(currentWorkspace?.workspaceId === workspaceId) {
switchSort(prevAllChannels);
}
}
});
}
}
if (change.type === "modified") {
console.log("message modified: ", change.doc.data());
let prevAllChannels = allChannels;
firebase
.firestore()
.collection(`workspaces/${workspaceId}/rooms/`)
.doc(`${roomId}`)
.get()
.then((doc) => {
if (doc.exists) {
prevAllChannels.splice(index, 1, {
...doc.data(),
roomId: doc.id,
});
// console.log(prevAllChannels,"prevallchannels",prevSortType,"prevsorttype", props.sortType,"currentsorttype")
if(currentWorkspace?.workspaceId === workspaceId) {
switchSort(prevAllChannels, props.sortType);
}
}
});
}
if (change.type === "removed") {
console.log("message removed: ", change.doc.data());
}
});
}
);
setUnsubscribe(() => unsub);
}
useEffect(() => {
getRoomData(props.currentWorkspace);
}, []);
useEffect(() => {
getRoomData(props.currentWorkspace);
}, [props.sortType, props.currentWorkspace]);
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const prevCurrentWorkspace = usePrevious(props.currentWorkspace)
useEffect(() => {
if(unsubscribe) {
unsubscribe();
setUnsubscribe(null);
}
},[props.workspace, props.currentWorkspace])
useEffect(() => {
return(() => {
if(unsubscribe)
unsubscribe()
setUnsubscribe(null);
})
},[])

Related

Cannot read properties of undefined (reading 'indexOf') in firebase v9 {modular}

I changed my code from firebase v8 to v9, I am able to write the data in db but couldn't get and map it.
My data is stored in db like this : users >> user.uid >> orders >> paymentIntent.id,
each doc saved has amount, basket data, created
Please check mentioned screenshot.
Previous code:
useEffect(() => {
if (user) {
db.collection("users")
.doc(user?.uid)
.collection("orders")
.orderBy("created", "desc")
.onSnapshot((snapshot) => {
// console.log(snapshot);
setOrders(
snapshot.docs.map((doc, i) => ({
key: { i },
id: doc.id,
data: doc.data(),
}))
);
});
} else {
setOrders([]);
}
}, [user]);
My data is stored in db like this : users >> user.uid >> orders >> paymentIntent.id,
each doc saved has amount, basket data, created
Please check mentioned screenshot.
This is firestore inserted data
firestore added data in db, screenshot
This is my new code
import { collection, doc, getDoc, getDocs, onSnapshot, orderBy, query } from 'firebase/firestore';
import React, { useEffect, useState } from 'react'
import { db } from './firebase';
import './Orders.css'
import { useStateValue } from './StateProvider';
import Order from './Order';
function Orders() {
const [{ basket, user}, dispatch] = useStateValue();
const [orders, setOrders] = useState([]);
useEffect(() => {
const getDocs = async () => {
try {
const collRef = await getDocs(collection(db, "users", user?.id, "orders"));
// const collRef = doc(db, "users", user?.id, "orders"));
// const collRef = collection(db, "users", user?.id, "orders"));
const orderedRef = query(collRef, orderBy("created", "desc"));
const docSnap = onSnapshot(orderedRef, (snapshot) => {
snapshot.map((doc) => ({
id: doc.id,
data: doc.data(),
}));
setOrders(docSnap);
});
} catch (err) {
console.log(err.message);
}
};
getDocs();
}, [user]);
return (
<div className="orders">
<h3>Your Orders</h3>
<div className="orders__order">
{orders?.map((order, i) => (
<Order key={i} order={order} />
))}
</div>
</div>
);
}
export default Orders;
Console, screenshot
My guess it that user doesn't have a value, so user?.id becomes undefined. You'll want to still check whether user has a value inside the second snippet too.
For example:
useEffect(() => {
const getDocs = async (user: User) => {
try {
const collRef = await getDocs(collection(db, "users", user.id, "orders"));
...
} catch (err) {
console.log(err.message);
}
};
if (user) {
getDocs(user!);
}
}, [user]);
useEffect(() => {
if (user) {
try {
const collRef = collection(db, "users", user?.uid, "orders");
const orderedRef = query(collRef, orderBy("created", "desc"));
const docSnap = onSnapshot(orderedRef, (querySnapshot) => {
const orderArray = [];
querySnapshot.forEach((doc) => {
orderArray.push({
id: doc.id,
data: doc.data()
})
})
setOrders(orderArray);
});
} catch (err) {
console.log(err.message);
}
} else {
console.log("not found");
setOrders([]);
}
}, [user]);

Trying to figure out how to use socket.io the correct way in a useEffect that is using an axios get request to fetch messages

So far i'm stuck on my useEffect that fetches all the current messages and renders the state accordingly. as of right now it doesn't render the new state until page is refreshed.
const Home = ({ user, logout }) => {
const history = useHistory();
const socket = useContext(SocketContext);
const [conversations, setConversations] = useState([]);
const [activeConversation, setActiveConversation] = useState(null);
const classes = useStyles();
const [isLoggedIn, setIsLoggedIn] = useState(false);
const addSearchedUsers = (users) => {
const currentUsers = {};
// make table of current users so we can lookup faster
conversations.forEach((convo) => {
currentUsers[convo.otherUser.id] = true;
});
const newState = [...conversations];
users.forEach((user) => {
// only create a fake convo if we don't already have a convo with this user
if (!currentUsers[user.id]) {
let fakeConvo = { otherUser: user, messages: [] };
newState.push(fakeConvo);
}
});
setConversations(newState);
};
const clearSearchedUsers = () => {
setConversations((prev) => prev.filter((convo) => convo.id));
};
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
const sendMessage = (data, body) => {
socket.emit("new-message", {
message: data.message,
recipientId: body.recipientId,
sender: data.sender,
});
};
const postMessage = async (body) => {
try {
const data = await saveMessage(body);
if (!body.conversationId) {
addNewConvo(body.recipientId, data.message);
} else {
addMessageToConversation(data);
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
const addNewConvo = useCallback(
(recipientId, message) => {
conversations.forEach((convo) => {
if (convo.otherUser.id === recipientId) {
convo.messages.push(message);
convo.latestMessageText = message.text;
convo.id = message.conversationId;
}
});
setConversations(conversations);
},
[setConversations, conversations],
);
const addMessageToConversation = useCallback(
(data) => {
// if sender isn't null, that means the message needs to be put in a brand new convo
const { message, sender = null } = data;
if (sender !== null) {
const newConvo = {
id: message.conversationId,
otherUser: sender,
messages: [message],
};
newConvo.latestMessageText = message.text;
setConversations((prev) => [newConvo, ...prev]);
}
conversations.forEach((convo) => {
console.log('hi', message.conversationId)
if (convo.id === message.conversationId) {
const convoCopy = { ...convo }
convoCopy.messages.push(message);
convoCopy.latestMessageText = message.text;
console.log('convo', convoCopy)
} else {
return convo
}
});
setConversations(conversations);
},
[setConversations, conversations],
);
const setActiveChat = useCallback((username) => {
setActiveConversation(username);
}, []);
const addOnlineUser = useCallback((id) => {
setConversations((prev) =>
prev.map((convo) => {
if (convo.otherUser.id === id) {
const convoCopy = { ...convo };
convoCopy.otherUser = { ...convoCopy.otherUser, online: true };
return convoCopy;
} else {
return convo;
}
}),
);
}, []);
const removeOfflineUser = useCallback((id) => {
setConversations((prev) =>
prev.map((convo) => {
if (convo.otherUser.id === id) {
const convoCopy = { ...convo };
convoCopy.otherUser = { ...convoCopy.otherUser, online: false };
return convoCopy;
} else {
return convo;
}
}),
);
}, []);
// Lifecycle
useEffect(() => {
// Socket init
socket.on("add-online-user", addOnlineUser);
socket.on("remove-offline-user", removeOfflineUser);
socket.on("new-message", addMessageToConversation);
return () => {
// before the component is destroyed
// unbind all event handlers used in this component
socket.off("add-online-user", addOnlineUser);
socket.off("remove-offline-user", removeOfflineUser);
socket.off("new-message", addMessageToConversation);
};
}, [addMessageToConversation, addOnlineUser, removeOfflineUser, socket]);
useEffect(() => {
// when fetching, prevent redirect
if (user?.isFetching) return;
if (user && user.id) {
setIsLoggedIn(true);
} else {
// If we were previously logged in, redirect to login instead of register
if (isLoggedIn) history.push("/login");
else history.push("/register");
}
}, [user, history, isLoggedIn]);
useEffect(() => {
const fetchConversations = async () => {
try {
const { data } = await axios.get("/api/conversations");
setConversations(data);
} catch (error) {
console.error(error);
}
};
if (!user.isFetching) {
fetchConversations();
}
}, [user]);
const handleLogout = async () => {
if (user && user.id) {
await logout(user.id);
}
};
return (
<>
<Button onClick={handleLogout}>Logout</Button>
<Grid container component="main" className={classes.root}>
<CssBaseline />
<SidebarContainer
conversations={conversations}
user={user}
clearSearchedUsers={clearSearchedUsers}
addSearchedUsers={addSearchedUsers}
setActiveChat={setActiveChat}
/>
<ActiveChat
activeConversation={activeConversation}
conversations={conversations}
user={user}
postMessage={postMessage}
/>
</Grid>
</>
);
};
this is the main part im working on, the project had starter code when i began and was told not to touch the backend so i know its something wrong with the front end code. i feel like im missing something important for the socket.io
import { io } from 'socket.io-client';
import React from 'react';
export const socket = io(window.location.origin);
socket.on('connect', () => {
console.log('connected to server');
});
export const SocketContext = React.createContext();
this is how i have the socket.io setup, if anyone could point me in the right direction that would be cool. I have been reading up on socket.io as much as I can but am still pretty lost on it.
Based on the assumption the backend is working properly...
const addNewConvo = useCallback(
(recipientId, message) => {
conversations.forEach((convo) => {
if (convo.otherUser.id === recipientId) {
convo.messages.push(message);
convo.latestMessageText = message.text;
convo.id = message.conversationId;
}
});
setConversations(conversations);
},
[setConversations, conversations],
);
setConversations(conversations);
This is an incorrect way to set a state using the state's variable, and such it wont do anything. Likely why your code wont change until refresh.
Suggested fix:
const addNewConvo = useCallback(
(recipientId, message) => {
setConversations(previousState => previousState.map(convo => {
if (convo.otherUser.id === recipientId) {
convo.messages.push(message)
convo.latestMessageText = message.text;
convo.id = message.conversationId;
return convo
}
return convo
}))
},
[setConversations, conversations],
);
note: even above could be done more efficiently since I made a deep copy of messages

State is undefined, when I setState() inside component did mount

I am new to react and unable to set state after fetching data from firebase in component did mount function ,keeps getting error contact is not defined inside the component did mount.
constructor(props) {
super(props);
this.state = {
contacts: [
{ name: 'sachin', number: '445343' },
{ name: 'nihil', number: '33335555' },
{ name: 'chicken', number: '434355' },
]
}
this.componentDidMount = this.componentDidMount.bind(this);
}
componentDidMount = () => {
let currentComponent = this;
fire.auth().onAuthStateChanged(function (user) {
if (user) {
console.log(user.uid)
var uid = user.uid;
db.collection(uid)
.get()
.then((querySnapshot) => {
const contacts = querySnapshot.forEach((doc) => {
const data = doc.data()
contacts.push(data)
})
console.log(contacts)
this.setState({contacts});
});
} else {
// No user is signed in.
}
});
}
Try this!
constructor(props) {
super(props);
this.state = {
contacts: [
{ name: 'sachin', number: '445343' },
{ name: 'nihil', number: '33335555' },
{ name: 'chicken', number: '434355' },
]
}
this.componentDidMount = this.componentDidMount.bind(this);
}
componentDidMount = () => {
let currentComponent = this;
fire.auth().onAuthStateChanged(function (user) {
if (user) {
console.log(user.uid)
var uid = user.uid;
db.collection(uid)
.get()
.then((querySnapshot) => {
const contacts = [];
querySnapshot.map(item => contacts .push(item.data));
this.setState({contacts});
});
} else {
// No user is signed in.
}
});
}
First of all you should not bind componentDidMount method in constructor
And second, you are pushing in contacts before initializing it. So Try something like below:-
componentDidMount() {
fire.auth().onAuthStateChanged((user) => {
if (user) {
console.log(user.uid)
var uid = user.uid;
db.collection(uid)
.get()
.then((querySnapshot) => {
const contacts = []; // initialize contants here
querySnapshot.forEach((doc) => {
contacts.push(doc.data())
})
console.log(contacts)
this.setState({contacts});
});
} else {
// No user is signed in.
}
});
}
componentDidMount() {
var user = fire.auth().currentUser;
if (user) {
console.log(user.uid)
var uid = user.uid;
} else {
// No user is signed in.
}
console.log('comp: ' + uid)
this.getData(uid);
}
getData = (e) => {
db.collection(e)
.get()
.then((querySnapshot) => {
const contacts = []; // initialize contants here
querySnapshot.forEach((doc) => {
contacts.push(doc.data())
})
console.log(contacts)
this.setState({contacts});
});
} ``` this worked I think putting the fire.auth().currentuser inside getData function did not let it set the state .

Firebase admin deleteUser function not working

I'm trying to delete all the users in my auth and database using firebase functions. Here's my code for that:
const admin = require("firebase-admin");
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: "----"
});
export const listenToAdminCommands = functions.firestore.document('collection/{docUid}')
.onWrite((change, context) =>
{
const pass: string = '--';
// const before = change.before.exists? change.before.data() : null;
const after = change.after.exists? change.after.data() : null;
if(after !== null && after !== undefined) {
const adminCommandType: string = after['type'];
const adminCommandPass: string = after['pass'];
if(adminCommandType === 'deleteAll' && adminCommandPass === pass) {
adminDeleteAllUsers();
}
}
});
//Admin control
function adminDeleteAllUsers() {
deleteAllUsers(' ');
return null;
}
function deleteAllUsers(nextPageToken: any) {
admin.auth().listUsers(1000, nextPageToken)
.then((listUsersResult: any) => {
//go through each one and check last time signed in
listUsersResult.users.forEach((userRecord: any) => {
const user: any = userRecord.toJSON();
const userUid = user['uid'];
console.log('Deleting user for data delete uid = ', userUid);
admin.auth().deleteUser(userUid)
.then(() => {
console.log('Successfully deleted user', userUid);
})
.catch((error: any) => {
console.log('Error deleting user:', error);
});
db.collection('users').doc(userUid).delete();
});
if (listUsersResult.pageToken) {
// List next batch of users.
listAllUsers(listUsersResult.pageToken);
}
})
.catch((error: any) => {
console.log('Error listing users:', error);
});
}
When the function get executed, no user is deleted. It's like the function never worked. Am I missing something?
Update:
I'm not sure if this is the way to do it, but it's still not working. I tried to handle promises correctly, but I'm not sure if what I'm doing is correct or not.
export const listenToAdminCommands = functions.firestore.document('collection/{docUid}')
.onWrite((change, context) =>
{
const pass: string = '---';
// const before = change.before.exists? change.before.data() : null;
const after = change.after.exists? change.after.data() : null;
if(after !== null && after !== undefined) {
const adminCommandType: string = after['type'];
const adminCommandPass: string = after['pass'];
if(adminCommandType === 'deleteAll' && adminCommandPass === pass) {
return adminDeleteAllUsers();
}
return;
}
return;
});
//Admin control
function adminDeleteAllUsers() {
return deleteAllUsers(' ');
}
function deleteAllUsers(nextPageToken: any) {
return admin.auth().listUsers(1000, nextPageToken)
.then((listUsersResult: any) => {
//go through each one and check last time signed in
listUsersResult.users.forEach((userRecord: any) => {
const user: any = userRecord.toJSON();
const userUid = user['uid'];
console.log('Deleting user for data delete uid = ', userUid);
return admin.auth().deleteUser(userUid)
.then(() => {
console.log('Successfully deleted user', userUid);
return db.collection('users').doc(userUid).delete();
})
.catch((error: any) => {
console.log('Error deleting user:', error);
return;
});
});
if (listUsersResult.pageToken) {
// List next batch of users.
listAllUsers(listUsersResult.pageToken);
}
return;
})
.catch((error: any) => {
console.log('Error listing users:', error);
return;
});
}

Paginating firestore data with realtime additions on top of the result

I want to load my data into chunks of 10 in react. I am listening for document addition using onSnapshot() firestore method. I want to paginate data and at the same time allow the recent addition to come to the top. How to apply this in the code below -
db.collection('palettes').orderBy("createdAt").onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type === "added") {
setPalette( prevPalette => ([
{ id: change.doc.id, ...change.doc.data() },
...prevPalette
]))
setIsLoading(false)
}
})
})
I think you should save state of last document for pagination and realtime updates
Example
const getPalettes = (pageSize, lastDocument) => new Promise((resolve, reject) => {
let query = db.collection('palettes')
.orderBy("createdAt")
if(lastDocument) {
query = query.startAt(lastDocument)
}
query = query.limit(pageSize);
return query.onSnapshot(query => {
const docs = query.docs.map(pr => ({pr.id, ...pr.data()}))
resolve(docs);
});
})
let unsubscribe = getPalettes(10).then(newPalettes => {
setPalette(palettes => [...palettes, newPalettes]);
lastPalette = newPalettes[newPalettes.length -1];
setLastPalette(lastPalette);
unsubscribe();
})
unsubscribe = getPalettes(10, lastPalette).then(newPalettes => {
setPalette(palettes => [...palettes, newPalettes]);
lastPalette = newPalettes[newPalettes.length -1];
setLastPalette(lastPalette);
unsubscribe();
})
const listenForLatestPalettes = (lastDocument, callback) => {
return db.collection('palettes')
.orderBy("createdAt")
.startAt(lastDocument)
.onSnapshot(callback);
}
const callback = snapshot => {
for(let change of snapshot.docChanges()) {
if (change.type === "added") {
setPalette(palettes => {
const palette = { id: change.doc.id, ...change.doc.data() };
return [...palettes.filter(pal => pal.id !== id], palette];
})
}
}
}
unsubscribe = listenForLatestPalettes(lastDocument, callback);

Categories

Resources