Component not updating even after state is changed - javascript

I have this friends component where I search the database for the user's friends (stored in a string separated by commas) and display them in a scrollable div. You can also add a new friend. Everything is working fine in the database and the backend. However, the component isn't being re-rendered when the state changes. It's dependent on 'allFriends' so it should re-render when that gets updated. Do you guys have any idea what's going on here? I'm guessing it has something to do with asynchronicity but I can't seem to pinpoint it. I added a comment for each code block to try to make it a little easier to read.
import React, { useState, useEffect } from 'react';
import SingleFriend from './SingleFriend';
import './friends.css';
const Friends = ({username}) => {
const [allFriends, setAllFriends] = useState([]);
const [unsortedFriends, setUnsortedFriends] = useState([]);
const [friendFilter, setFriendFilter] = useState('');
const [friendSearch, setFriendSearch] = useState('');
//-----------------------------------------------------------------------------------
// Start fetching friends on component mount
//-----------------------------------------------------------------------------------
useEffect(() => {
fetchFriends();
}, [])
//-----------------------------------------------------------------------------------
// Sort the friends when fetching has finished/unsortedFriends has updated
//-----------------------------------------------------------------------------------
useEffect(() => {
const onlineFriends = [];
const offlineFriends = [];
unsortedFriends.forEach(f => {
if (f.status === 'online') {
onlineFriends.push(f)
} else {
offlineFriends.push(f)
}
})
setAllFriends(onlineFriends.concat(offlineFriends));
},[unsortedFriends])
//-----------------------------------------------------------------------------------
// Get the string of friends that is stored in the database for the user
// Convert to array of friends
// Pass the array to 'fetchFriendData()'
//-----------------------------------------------------------------------------------
const fetchFriends = () => {
let allFriendNames = [];
fetch(`http://localhost:8000/getFriends?username=${username}`)
.then(res => res.json())
.then(friends => {
if (friends !== null && friends !== '') {
allFriendNames = friends.split(',');
fetchFriendData(allFriendNames);
}
})
}
//-----------------------------------------------------------------------------------
// Search the database for each of the user's friends
// Return those users, and add their username & online status to a temporary array
// Assign that array to the unsortedFriends useState hook
//-----------------------------------------------------------------------------------
const fetchFriendData = (allFriendNames) => {
let allF = [];
for (let friend of allFriendNames) {
fetch(`http://localhost:8000/findFriend?username=${friend}`)
.then(res => res.json())
.then(user => {
if (user.socketid) {
allF.push({name: user.username, status: 'online'})
} else {
allF.push({name: user.username, status: 'offline'})
}
})
.catch(err => console.log(err))
}
document.querySelector('.addFriendInput').value = '';
setUnsortedFriends(allF);
}
//-----------------------------------------------------------------------------------
// Called on button press to add friend
//-----------------------------------------------------------------------------------
const addFriend = () => {
let friendList = '';
let friendArray = [];
//-----------------------------------------------------------------------------------
// Search the database to check if the name that was
// entered matches any users in the databse
//-----------------------------------------------------------------------------------
fetch(`http://localhost:8000/findFriend?username=${friendSearch}`)
.then(res => res.json())
.then(user => {
if (user.username) {
//-----------------------------------------------------------------------------------
// If friend exists, grab the user's friend list string
// Make a temporary string and array with the new friend
//-----------------------------------------------------------------------------------
fetch(`http://localhost:8000/getFriends?username=${username}`)
.then(res => res.json())
.then(friends => {
if (friends !== null && friends !== '') {
friendArray = friends.split(',');
if (friendArray.includes(friendSearch)) {
throw new Error('Problem getting friend')
} else {
friendArray.push(friendSearch);
friendList = friends.concat(`,${friendSearch}`);
}
} else {
friendList = friendSearch;
friendArray = [friends];
}
//-----------------------------------------------------------------------------------
// Update the user's friend list with the new friends string
// Pass the updated friends array to 'fetchFriendData'
//-----------------------------------------------------------------------------------
fetch('http://localhost:8000/addFriend', {
method: 'put',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: username,
friendlist: friendList
})
})
.then(fetchFriendData(friendArray))
.catch(err => console.log(err))
})
.catch(err => console.log(err))
}
})
.catch(err => console.log(err))
}
return (
<div className='friendsContainer'>
<div className='friendsSection'>
<h2>Friends</h2>
<input onChange={(e) => setFriendFilter(e.target.value)} type='text' placeholder='Enter a username'/>
<div className='friendsListContainer'>
{
allFriends.length
?
<div className='friendsList'>
{
//-----------------------------------------------------------------------------------
// Map through the user's friends
// Return a single friend div w/ their username and status
//-----------------------------------------------------------------------------------
allFriends.map(f => {
if (f.name.toLowerCase().includes(friendFilter.toLowerCase())) {
return <SingleFriend key={f.name} name={f.name} status={f.status}/>
} else return null
})
}
</div>
: <h4 className='noFriends'>No friends have been added</h4>
}
</div>
<div className='addFriend'>
<h3 className='addFriendText' >Add a friend</h3>
<input className='addFriendInput' onChange={(e) => setFriendSearch(e.target.value)} type='text' placeholder='Enter a username'/>
<button onClick={addFriend} >Add</button>
</div>
</div>
</div>
)
}
export default Friends;

you need wait fetch response data
const fetchFriendData = async (allFriendNames) => {
let allF = [];
for (let friend of allFriendNames) {
const response = await fetch(`http://localhost:8000/findFriend?username=${friend}`)
const user = await response.json()
if (user.socketid) {
allF.push({name: user.username, status: 'online'})
} else {
allF.push({name: user.username, status: 'offline'})
}
}
document.querySelector('.addFriendInput').value = '';
setUnsortedFriends(allF);
}

Related

Updating state with axios response data in reactjs

I am building a website using nextjs and axios. Users can apply to become a member and then be approved by admins. In the admin dashboard I initially load the users and the unapproved users and display them in a list.
When an admin clicks on a button the unapproved user should be approved. The functionality works. The only aspect I can't figure out is how to update the state.
Here is my code:
const AdminIndex = () => {
const [users, setUsers] = useState([])
const [unapprovedUsers, setUnapprovedUsers] = useState([])
useEffect(() => {
loadUnapprovedUsers()
loadUsers()
}, [])
const loadUnapprovedUsers = async () => {
const { data } = await axios.get('/api/admin/unapprovedUsers')
setUnapprovedUsers(data)
}
const loadUsers = async () => {
const { data } = await axios.get('/api/admin/users')
setUsers(data)
}
const approveUnapprovedUser = async (email) => {
try {
const { data } = await axios.put(
`/api/admin/approveUnapprovedUser/${email}`
)
setUnapprovedUsers([]) // only remove the approved user
setUsers(...data) // include the approved user into the array
} catch (err) {
console.log(err)
}
}
}
I am trying to remove the approved user from the unapprovedUsers array and try to add the user to the users array, hence updating the UI. The response returned by axios is an object, which doesn't make things easier.
I would be very thankful for any kind of help!
Just try to filter the unapprovedUsers with the users that don't have that email, also add the approved user to users state
const AdminIndex = () => {
const [users, setUsers] = useState([])
const [unapprovedUsers, setUnapprovedUsers] = useState([])
useEffect(() => {
loadUnapprovedUsers()
loadUsers()
}, [])
const loadUnapprovedUsers = async () => {
const { data } = await axios.get('/api/admin/unapprovedUsers')
setUnapprovedUsers(data)
}
const loadUsers = async () => {
const { data } = await axios.get('/api/admin/users')
setUsers(data)
}
const approveUnapprovedUser = async (email) => {
try {
const { data } = await axios.put(
`/api/admin/approveUnapprovedUser/${email}`
)
setUnapprovedUsers(prev => prev.filter(user => user.email !== email)) // only remove the approved user
setUsers(prev => [...prev, data]) // include the approved user into the array
} catch (err) {
console.log(err)
}
}
}

i am getting only one document from the collection of firebase for different id

const { id } = useParams();
const [detailData, setDetailData] = useState({});
useEffect(() => {
const q = query(collection(db, "movie"));
onSnapshot(q, (querySnapshot) => {
querySnapshot.docs.map((doc) => {
if (doc.exists) {
setDetailData(doc.data());
} else {
console.log("no doc");
}
})
.catch((error) => {
console.log("Error:", error);`enter code here`
});
});
}, [id]);
how can i pass id of document in this code tried lots of tutorials and official documentation also .
QuerySnapshot.docs returns an array of QuerydocumentSnapshot which has an id field:
https://firebase.google.com/docs/reference/node/firebase.firestore.QueryDocumentSnapshot
This is located outside of the doc.data field. Doc.data contains the data in your DB object.

How to have a try catch wait for response before continuing?

I'm fetching some data from firestore and everything works, but there is one big flaw I'm trying to figure out…
I can only console log the data once I press on the “Read Data” button twice. Any help on how I can make it wait for it to have the response before showing it?
Bellow is my code, thanks in advance.
import firebase from 'firebase/compat/app'
import 'firebase/compat/firestore';
import { useState } from 'react';
export default function ReadToFirebase() {
const [data, setData] = useState([])
const readData = () => {
try {
firebase
.firestore()
.collection('users')
.onSnapshot(snapshot => {
let changes = snapshot.docChanges()
changes.forEach(change => {
let data = change.doc.data()
data = {
username: data.userName,
email: data.email
}
setData(oldArray => [...oldArray, data])
})
})
console.log('Data Read!')
console.log(data);
// return data
} catch (error) {
console.log(`Opps! Error querying data: \n\n ${error}`);
alert(error)
}
}
return (
<div className="p-4 grid place-items-center space-y-2">
<button onClick={readData} className="py-2 px-4 bg-gray-300 rounded-xl font-bold">Read Data</button>
<div className="">{}</div>
</div>
)
}
Your console.log statement is outside the onSnapshot function.
If you do something like this:
const readData = () => {
try {
firebase
.firestore()
.collection('users')
.onSnapshot(snapshot => {
let changes = snapshot.docChanges()
changes.forEach(change => {
let data = change.doc.data()
data = {
username: data.userName,
email: data.email
}
setData(oldArray => [...oldArray, data])
});
console.log('Data Read!')
console.log(data);
})
// return data
} catch (error) {
console.log(`Opps! Error querying data: \n\n ${error}`);
alert(error)
}
}
It'll work.
Two things to note here:
You can improve your code by using map instead of forEach:
const changedData = changes.map(change => {
let data = change.doc.data()
return {
username: data.userName,
email: data.email
}
});
setData(oldArray => [...oldArray, ...changedData])
In order for the try/catch to actually catch server error you should wrap your request in a promise. Something like:
const readData = () => {
new Promise((resolve, reject) => {
firebase.firestore().collection('users').onSnapshot(handleSnapshot(resolve, reject)());
})
.then(handleSuccessfulUsersFetch)
.catch(handleErrorInUsersFetch);
}
There are other ways you could achieve that (for instance with async/await). You can read more about error handling in JS in this nice article:
https://medium.com/walkme-engineering/javascript-error-handling-9fc1a2946119

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]);

fetching data and adding title to Json object

I would like to add title to my JSON object, the structure I wish to achieve is:
{
"posts": [
{
"title": "put title here",
"upvotes": 1234,
"score": 1000,
"num_comments": 100,
"created": "16.05.2019 12:12",
},
]
}
I was able to fetch data and put it into array of 26 elements, everything is fine but I wish to somehow add this "posts:" to be above whole rest, here is my code:
fetch("https://www.reddit.com/r/funny.json")
.then(resp => resp.json()
.then(async res => {
let posts = await res.data.children.map(el => {
let title = el.data.title;
let upvote = el.data.ups;
let score = el.data.score;
let comments = el.data.num_comments;
let created = el.data.created;
const allPosts = {title, upvote, score, comments, created}
postList.push(allPosts)
return postList
})
console.log(posts);
return posts
})
You might need to create the object like below
{propertyName:value}
const allPosts = {title:title,upvote: upvote,score: score,comments: comments, created:created}
postList.push(allPosts)
fetch("https://www.reddit.com/r/funny.json")
.then(resp => resp.json())
.then(async res => {
console.log(res);
let posts = await res.data.children.map(el => {
let title = el.data.title;
let upvote = el.data.ups;
let score = el.data.score;
let comments = el.data.num_comments;
let created = el.data.created;
const allPosts = { title, upvote, score, comments, created };
let postList = [];
postList.push(allPosts);
return postList;
});
console.log({"posts": posts});
return {"posts": posts};
});
You can try out the following code.
fetch("https://www.reddit.com/r/funny.json")
.then(resp => resp.json())
.then(res => ({
posts: res.data.children.map(el => ({
title: el.data.title,
upvote: el.data.upvote,
score: el.data.score,
comments: el.data.num_comments,
created: el.data.created
}))
}))
.then(posts => {
console.log(posts);
});
You can do it in this way:
fetch("https://www.reddit.com/r/funny.json")
.then(resp => resp.json()
.then(async res => {
let posts = await res.data.children.map(el => {
return {
title: el.data.title,
upvote: el.data.ups,
score: el.data.score,
comments: el.data.num_comments,
created: el.data.created
}
})
const postObject = { posts }
console.log(postObject);
return postObject
})
Map function return value, in this way you get an object with key (posts) and values (an object with details).

Categories

Resources