There is a request to Firebase, using then I add new elements to the array. If you output console.log, then there are elements, but lenght = 0 and loops do not work.
export const useLastMessageDialogs = (messagesStatus: StatusDialogType): UserMessageType[] => {
const messages: string[] = [];
useEffect(() => {
const querySideDialogs = query(
collection(firestoreDb, 'questions'),
where('status', '==', messagesStatus)
);
onSnapshot(querySideDialogs, (dialogs) => {
dialogs.docChanges().forEach((dialog) => {
getDocs(
query(
collection(firestoreDb, 'questions', dialog.doc.id, 'messages'),
orderBy('timestamp', 'desc'),
limit(1)
)
).then((response) => {
response.forEach((message) => {
messages.push('bebebe');
});
});
});
});
}, [messagesStatus]);
console.log(messages); // [0: "bebebe", 1: "bebebe" length: 2]
console.log(messages.length); // 0
return [];
};
I took it to a separate service and connected it via redux-saga
Related
I'm trying to filter a players array by comparing the names property to a roster array and rendering the matches from the players array.
When a user selects a year the getCollegeRoster() function returns players who have a year property matching the selection. From there I'm trying to filter and display the .name matches from the players array, but it seems i'm unable to update playerStateValue. I'm using recoil to manage state. Any insight would be much appreciated.
const getCollegeRoster = async () => {
setLoading(true);
try {
const playersQuery = query(
collection(firestore, `colleges/${communityData.id}/rosters`),
where("year", "==", selection),
);
const playerDocs = await getDocs(playersQuery);
const collegeRoster = playerDocs.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setRoster(collegeRoster as unknown as Roster[]);
console.log('collegePlayers', roster);
} catch (error: any) {
console.log("getCollegePlayers error", error.message);
}
setLoading(false);
};
const onSelection = (newSelection: any) => {
setSelection(newSelection);
console.log('newselect', newSelection)
getCollegeRoster();
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name)
})
);
setPlayerStateValue((prev) => ({
...prev,
players: filteredArray as Player[],
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray as Player[],
},
playerUpdateRequired: false,
}));
} ```
also tried adding setplayerstatevalue into the getcollegeroster function:
onst getCollegeRoster = async () => {
console.log("WE ARE GETTING Players!!!");
console.log('communitydata', communityData);
setLoading(true);
try {
const playersQuery = query(
collection(firestore, `colleges/${communityData.id}/rosters`),
where("year", "==", selection),
);
const playerDocs = await getDocs(playersQuery);
const collegeRoster = playerDocs.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setRoster(collegeRoster as unknown as Roster[]);
console.log('collegePlayers', roster);
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name)
})
);
setPlayerStateValue((prev) => ({
...prev,
players: filteredArray as Player[],
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray as Player[],
},
playerUpdateRequired: false,
}));
} catch (error: any) {
console.log("getCollegePlayers error", error.message);
}
setLoading(false);
};
{playerStateValue.players.map((player: Player, index) => (
<PlayerItem
key={player.id}
player={player}
// postIdx={index}
onPlayerVote={onPlayerVote}
userVoteValue={
playerStateValue.playerVotes.find((item) => item.playerId === player.id)
?.voteValue
}
onSelectPlayer={onSelectPlayer}
/>
I believe I found the problem. getCollegeRoster() is an async function which means it is being executed asynchronous.
That function is responsible for updating roster and you are using roster inside your filtering while it is being asynchronously updated.
There is no guarantee that roster is updated by the time you filter your array.
You have two ways of solving this. Either you execute setPlayerStateValue() inside of getCollegeRoster() or you wait for getCollegeRoster() to be done by preappending it with await and your onSelection function with async:
const onSelection = async (newSelection: any) => {
setSelection(newSelection);
console.log("newselect", newSelection);
await getCollegeRoster();
const filteredArray = [...playerStateValue.players].filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name),
}));
...
Edit:
Another scenario I can think of is that playerStateValue might be the culprit. In React setState works asynchronous as well, if you access playerStateValue via curr/prev-callback, it is guaranteed that you are accessing the values of the current state object while updating the state. Try this use of setPlayerStateValue:
setPlayerStateValue((prev) => {
const filteredArray = prev.players.filter((el) => ({
name: el.name,
match: [...roster].some((f) => f.name === el.name),
})) as Player[];
return {
...prev,
players: filteredArray,
playersCache: {
...prev.playersCache,
[communityData?.id!]: filteredArray,
},
playerUpdateRequired: false,
};
});
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
So I need to convert data from Firebase 9 snapshot to an array.
I've came up with something like this, but it doesn't return anything.
Check comments in my code.
I suppose, I should use promises, but can't figure out the right way.
const fetchNewMessages = async (threadId: string, latestTimestamp: any) => {
const q = query(threadRef(threadId), orderBy('timestamp', 'asc'), startAfter(latestTimestamp));
const messages: IMessage[] = [];
onSnapshot(q, (querySnapshot) => {
const docs = querySnapshot.docChanges();
docs.forEach((change) => {
if (change.type === "added")
messages.push(fetchDoc(change.doc));
});
console.log(messages.length); // here I get 34
});
console.log(messages.length); // here I get 0
return messages.reverse();
};
try to wait for "onSnapshot"
const fetchNewMessages = (threadId: string, latestTimestamp: any) => {
const q = query(threadRef(threadId), orderBy('timestamp', 'asc'), startAfter(latestTimestamp));
const messages: IMessage[] = [];
return new Promise((resolve) => {
onSnapshot(q, (querySnapshot) => {
const docs = querySnapshot.docChanges();
docs.forEach((change) => {
if (change.type === "added")
messages.push(fetchDoc(change.doc));
});
resolve(messages.reverse());
});
});
};
I'm trying to wrap my head around how to use Promise.all() in this code. I've read on articles that you can run async operations in parallel with Promise.all() to optimize for speed. Here's the current code in nested for-loops (bad):
type ListGroup = {
listId: string
groupIds: Array<string>
}
const listsAndGroups: Array<ListGroup> = []; // <-- put everything here
const { lists } = await mailchimp.get('/lists');
for (const list of lists) {
const listObj = { listId: list.id };
const { categories } = await mailchimp.get(
`/lists/${list.id}/interest-categories`,
);
for (const category of categories) {
const { interests } = await mailchimp.get(
`/lists/${list.id}/interest-categories/${category.id}/interests`,
);
Object.defineProperty(listObj, 'groupIds', {
value: interests.map((interest) => interest.id),
enumerable: true,
});
}
listsAndGroups.push(listObj);
}
Here's how I'm doing so far, I think I'm just running blindly here without really knowing what I'm doing:
const listsAndGroups: Array<ListGroup> = await getListsGroups(); // <-- put everything here
const getListsGroups = async () => {
const { lists } = await mailchimp.get('/lists');
const listGroups = lists.map((list) =>
getCategories(list.id).then((groups) =>
groups.map((group: Record<'groupIds', string>) => {
return {
listId: list.id,
...group,
};
}),
),
);
return Promise.all(listGroups);
};
const getCategories = async (listId: string) => {
const { categories } = await mailchimp.get(
`/lists/${listId}/interest-categories`,
);
const groups = categories.map((category) =>
getInterests(listId, category.id),
);
return Promise.all(groups);
};
const getInterests = async (listId: string, categoryId: string) => {
const { interests } = await mailchimp.get(
`/lists/${listId}/interest-categories/${categoryId}/interests`,
);
return { groupIds: interests.map((interest) => interest.id) };
};
You could simplify your operation many way, Here is one:
type ListGroup = {
listId: string
groupIds: Array<string>
}
const listsAndGroups: Array<ListGroup> = []; // <-- put everything here
const { lists } = await mailchimp.get('/lists');
const pandingLists = lists.map(list =>
mailchimp.get(`/lists/${list.id}/interest-categories`)
.then(data => [data, { listId: list.id }])
);
for (const [{ categories }, listObj] of await Promise.all(pandingLists)) {
const batch = categories.map(({ id }) =>
mailchimp.get(`/lists/${listObj.listId}/interest-categories/${id}/interests`).then(interests => {
Object.defineProperty(listObj, 'groupIds', {
value: interests.map(({ id }) => id),
enumerable: true,
});
}));
await Promise.all(batch).then(() => listsAndGroups.push(listObj));
}
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]);