How can I unsubscribe from Firebase onSnapShot in an RTK Query? - javascript

I'm creating an app in React Native that depends on realtime updates. When a user updates the data in their session, another user needs to immediately see that update without refreshing their app. I'm using RTK Query to manage my store, but can't figure out how to close the memory leak if I use onSnapShot in my query. Is there another Redux solution I need to consider?
I've tried passing the data through props to manage the data but the management becomes a little complicated when using complex components.
I start with the following in the component but want to move it to the api:
export const teamPlayersApi = createApi({
reducerPath: "teamPlayers",
baseQuery: fakeBaseQuery(),
tagTypes: ["TeamPlayer"],
endpoints: (builder) => ({
fetchTeamPlayersByMatchId: builder.query({
async queryFn(matchId) {
let players = [];
return new Promise((resolve, reject) => {
const playersRef = query(
collection(db, "teamPlayers"),
where("playerMatchId", "==", matchId)
);
//real time update
onSnapshot(playersRef, (snapshot) => {
players = snapshot.docs.map((doc) => ({
id: doc.id,
player: doc.data()
}));
resolve({ data: players });
});
});
}
})
})
})

This has two different parts:
a queryFn getting the initial data using onValue. This is the point where your query enters a loading state and finishes at some point with a first value.
a onCacheEntryAdded lifecycle function that calls onSnapshot, updates values and holds the subscription. This is the point where a subscription is generated and your data is updated with new values while your component uses the cache entry.
export const teamPlayersApi = createApi({
reducerPath: "teamPlayers",
baseQuery: fakeBaseQuery(),
tagTypes: ["TeamPlayer"],
endpoints: (builder) => ({
fetchTeamPlayersByMatchId: builder.query({
async queryFn(matchId) {
let players = [];
return {
data: await new Promise((resolve, reject) => {
const playersRef = query(
collection(db, "teamPlayers"),
where("playerMatchId", "==", matchId)
);
// probably more logic here to get your final shape
onValue(
playersRef,
(snapshot) => resolve(snapshot.toJSON()),
reject
);
}),
};
},
async onCacheEntryAdded(
matchId,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
let unsubscribe = () => {};
try {
await cacheDataLoaded;
const playersRef = query(
collection(db, "teamPlayers"),
where("playerMatchId", "==", matchId)
);
unsubscribe = onSnapshot(playersRef, (snapshot) => {
players = snapshot.docs.map((doc) => ({
id: doc.id,
player: doc.data(),
}));
updateCachedData((draft) => {
// or whatever you want to do
draft.push(players);
});
});
} catch {}
await cacheEntryRemoved;
unsubscribe();
},
}),
}),
});

Related

how to efficiently retrieve data from firebase/Firestore subcollection?

I'm using firestore to store posts each post could have simple properties such as {title: 'hi', comment: true} I'm able to easily fetch the user's specific posts since my collection structure looks like this: posts/user.id/post/post.name so an example will be posts/1234sofa/post/cool day
with this way of structuring, I'm able to easily fetch data for the user, but I'm having trouble with two things how do I fetch and display all posts for my main feed, and what's the most effective way of doing this? here is my current function for fetching user-specific data:
const submitpost = async () => {
try {
const collectionRef=collection(db,`posts`,user.uid.toString(),'post')
await addDoc(collectionRef, {
post: post,
timestamp: serverTimestamp(),
canComment: switchValue,
user: user.uid,
avatar: user.photoURL,
username: user.displayName,
});
toast({ title: "posted", status: "success", duration: 2000 });
} catch (error) {
console.log(error);
}
};
this specific function creates a structure like this in firebase posts are just takes and take is singular post respectively I just changed the name so its easier to understand:
now here is how im fetching the data for my spefic user:
const [user] = useAuthState(auth);
const [takes, settakes] = useState([]);
const getData = async () => {
// if user is present run function
if (user) {
// const docRef = doc(db, "users", user.uid);
// const collectionRef = collection(docRef, "takes");
// const querySnapshot = await getDocs(collectionRef);
try {
const docRef = doc(db, "posts", user.uid);
const collectionRef = collection(db,'posts',user.uid,'takes');
const querySnapshot = await getDocs(collectionRef);
const data = querySnapshot.docs.map((d) => ({
id: d.id,
...d.data(),
}));
settakes(data);
} catch (error) {
console.log(error);
}
//
}
};
here is the function that doesn't work when fetching all data for main feed:
const [user]=useAuthState(auth)
const [allfeed, setallfeed] = useState([])
const getData = async () => {
if(user){
const collectionRef = collection(db, "posts");
const querySnapshot = await getDocs(collectionRef);
const data = querySnapshot.docs.map((d) => ({
id: d.id,
...d.data(),
}));
// get data from firebase
setallfeed(data)
}
}
useEffect(() => {
getData()
console.log('ran');
console.log(allfeed);
// rerun when user is present
}, [user]);
when I console log the allfeed it returns an empty array so my main problem is how to do I get all the data from the posts collection meaning posts/userid/post/post.title I need to get these for every user. and secondly is there a more efficient way to structure my data?
I would suggest using the onSnapshot() method if you want realtime updates from a collection or a specific document.
setState() does not make changes directly to the state object. It just creates queues for React core to update the state object of a React component. If you add the state to the useEffect, it compares the two objects, and since they have a different reference, it once again fetches the items and sets the new items object to the state. The state updates then triggers a re-render in the component. And on, and on, and on...
If you just want to log your data into your console then you must use a temporary variable rather than using setState:
const getData = async () => {
if(user){
// Using `getDocs`
const collectionRef = collection(db, "posts");
const querySnapshot = await getDocs(collectionRef);
const data = querySnapshot.docs.map((d) => ({
id: d.id,
...d.data(),
}));
console.log(data)
// ============================================= //
// Using `onSnapshot()`
const q = query(collection(db, "posts"));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const data = querySnapshot.docs.map(d => ({
id: d.id,
...d.data()
}))
console.log(data)
});
}
}
useEffect(() => {
getData();
}, []);
You could also use multiple useEffect() to get the updated state of the object:
const getData = async () => {
if(user){
// Using `getDocs`
const collectionRef = collection(db, "posts");
const querySnapshot = await getDocs(collectionRef);
const data = querySnapshot.docs.map((d) => ({
id: d.id,
...d.data(),
}));
setallfeed(data)
// ============================================= //
// Using `onSnapshot()`
const q = query(collection(db, "posts"));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const data = querySnapshot.docs.map(d => ({
id: d.id,
...d.data()
}))
setallfeed(data)
});
}
}
useEffect(() => {
getData();
}, [])
useEffect(() => {
console.log(allfeed);
}, [allfeed]);
If you want to render it to the component then you should call the state in the component and map the data into it. Take a look at the sample code below:
const getData = async () => {
if(user){
// Using `getDocs`
const collectionRef = collection(db, "posts");
const querySnapshot = await getDocs(collectionRef);
const data = querySnapshot.docs.map((d) => ({
id: d.id,
...d.data(),
}));
setallfeed(data)
// ============================================= //
// Using `onSnapshot()`
const q = query(collection(db, "posts"));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const data = querySnapshot.docs.map(d => ({
id: d.id,
...d.data()
}))
setallfeed(data)
});
}
}
useEffect(() => {
getData()
}, []);
return (
<div>
<p>SomeData: <p/>
{items.map((item) => (
<p key={item.id}>{item.fieldname}</p>
))}
</div>
);
For more information you may checkout these documentations:
Get data with Cloud Firestore
Get realtime updates with Cloud Firestore

React native firebase duplicate chat messages

I'm developing a chat application using react native and firebase firestore db, I have almost implemented that part but stuck where I need to listen to changes in my messages collection and get the latest message for current roomId, but everytime the listener listens, it duplicates the last message. I think I may know the reason as I'm using 3 useEffects, 1st for initial room status checking like whether a room is created between current 2 users or not, 2nd for initially fetching all messages if the room exists between the two users, 3rd for realtime listener. Below is the relevant code:
const [messages, setMessages] = useState([]);
const [roomId, setRoomId] = useState('');
useEffect(() => {
// logic to setRoomId
// ...
if (item?.members?.includes(userDetails?.uid)) {
setRoomId(item.roomId);
return true;
}
// ...
}, []);
// fetch all messages initially
useEffect(() => {
if (roomId) {
const q = query(
collection(db, 'messages'),
where('roomId', '==', roomId),
orderBy('createdAt', 'asc'),
);
getDocs(q)
.then(result => {
const messages = result.docs.map(doc => {
const data = doc.data();
return {
id: data?.id,
roomId: data?.roomId,
sentBy: data?.sentBy,
text: data?.text,
user: data?.user,
createdAt: data?.createdAt,
};
});
setMessages(messages);
})
.catch(error => console.log(error));
}
}, [roomId]);
// realtime message listener
useEffect(() => {
const unsub = onSnapshot(
query(
collection(db, 'messages'),
where('roomId', '==', roomId),
orderBy('createdAt', 'asc'),
),
snapshot => {
if (snapshot.docs.length > 0) {
const lastMessage = snapshot.docs[snapshot.docs.length - 1];
const data = lastMessage.data();
const newMessage: MessageType = {
id: data?.id,
roomId: data?.roomId,
sentBy: data?.sentBy,
text: data?.text,
user: data?.user,
createdAt: data?.createdAt,
};
setMessages((prevMessages: MessageType[]) => [
...prevMessages,
newMessage,
]);
}
},
);
return () => unsub();
}, [roomId]); // please read below --> COMMENT-1
COMMENT-1: if I don't provide roomId here as a dependency, it does not duplicates, but as setRoomId is async in nature, it doesn't re-render the ui and roomId is still '', so I have to add roomId as a dependency here in order to fire it so that roomId is not '', if I add this dependency, the duplication of last message happens. So can anyone help provide a better or an optimal solution to this problem?
EDIT-1
I could do this and it works but I don't think it would be an optimal solution because whenever a new message arrives, it will map through all the messages from the beginning and set in setMessages and just imagine someone has 100k to 1M messages...
// realtime message listener
useEffect(() => {
const unsub = onSnapshot(
query(
collection(db, 'messages'),
where('roomId', '==', roomId),
orderBy('createdAt', 'asc'),
),
snapshot => {
if (snapshot.docs.length > 0) {
const _messages = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: data?.id,
roomId: data?.roomId,
sentBy: data?.sentBy,
text: data?.text,
user: data?.user,
createdAt: data?.createdAt,
};
});
setMessages(_messages);
}
},
);
return () => unsub();
}, [roomId]);
Your best option for this would be to filter or add a check to see if the message id exists in that 3rd useEffect like so,
useEffect(() => {
const unsub = onSnapshot(
query(
collection(db, 'messages'),
where('roomId', '==', roomId),
orderBy('createdAt', 'asc'),
),
snapshot => {
if (snapshot.docs.length > 0) {
const lastMessage = snapshot.docs[snapshot.docs.length - 1];
const data = lastMessage.data();
const newMessage: MessageType = {
id: data?.id,
roomId: data?.roomId,
sentBy: data?.sentBy,
text: data?.text,
user: data?.user,
createdAt: data?.createdAt,
};
if(!messages.find(mess => mess.id === newMessage.id)){
setMessages((prevMessages: MessageType[]) => [
...prevMessages,
newMessage,
]);
}
}
},
);
return () => unsub();
}, [roomId]); // please read below --> COMMENT-1

(React + Firestore) Component executing before Context authentication?

I have a problem with one of my components. The problem I think I have is that my component executes before my user context stores the currentUser. My code only works when doing a hot reload.
The watchlist component gets all the values from the watchlist array where the document matches the currentUser.uid.
UserContext.js:
const [currentUser, setCurrentUser] = useState(null)
const [watchlist, setWatchlist] = useState(null)
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user)
})
return unsubscribe
}, [])
const getWatchlist = async () => {
const userRef = await getDoc(doc(db, 'users', currentUser.uid))
setWatchlist(userRef.data().watchlist)
console.log(userRef.data().watchlist)
}
These values are the ids of objects I then GET from an API, these are then pushed to the watchlistData array.
CryptoContext.js
export const getWatchlistData = async (list) => {
const watchlistData = []
for (const item of list) {
const result = await axios.get(
`${coingecko}/coins/${item}`
)
watchlistData.push(result.data)
}
return watchlistData
}
And this is how my Watchlist component code currently looks.
WatchlistItems.jsx
const { watchlist, getWatchlist, currentUser } = useContext(UserContext)
const { dispatch } = useContext(CryptoContext)
useEffect(() => {
if (currentUser) {
dispatch({type: 'SET_LOADING'})
const getWatchlistDataFromAPI = async () => {
await getWatchlist()
const watchlistData = await getWatchlistData(watchlist)
dispatch({type: 'GET_WATCHLIST', payload: watchlistData})
console.log(watchlistData)
}
getWatchlistDataFromAPI()
}
}, [currentUser])
If I refresh the page I get "Uncaught (in promise) TypeError: the list is not iterable", but if I do a hot reload, watchlist, and watchlistData both console.log with the correct data.
This is my first post and so please let me know if I've left anything out.
Thank you in advance for any help :)

Array.map() doesn't render anything in React

I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);

Firebase database ref on not updating at first

I'm building a chat app with react native and firebase.
When the user sign in, previous messages have to load at first launch but it provides empty arrays to promise at first then when I connect again, it shows up...
I'm using snack and I have to change code to see the chat histroy(In order to update the device).
How can I load at first launch?
edit:
firebase structure
userName:
-rooms:
-otherUser:
-messages
-name
Code:
firebase.database().ref(`userName/rooms`).on('child_added', snapshot => callback(parse(snapshot)));;
const parse = snapshot => {
//console.log(snapshot);
const { name } = snapshot.val();
const { key: _id } = snapshot;
const rooms = {
_id,
name
};
return rooms;
};
And this is happening in my useEffect:
const loadRequiredMaterials = async () => {
return new Promise(async (resolve, reject) => {
//var loadedRooms = [...rooms]
var loadedRooms = []
await on(oldRooms => {
loadedRooms = [oldRooms, ...loadedRooms]
});
await resolve([loadedRooms]);
})
}
if (firebase.auth().currentUser) {
console.log(firebase.auth().currentUser)
loadRequiredMaterials()
.then((result)=> {
console.log(result)
setRooms(result[0]);
})
}

Categories

Resources