React Firebase - retrieving photoURL from users collection to render in comments section - reactjs

I have a React app with Firebase backend. I have a users collection in the database which stores the photoURL of the user.
When a user comments on a post, instead of saving that photoURL each time into the comments collection I just want to render the photoURL from the users collection.
That way if the user updates their photoURL it is updated on all the comments plus there's no duplication of data.
On each comment I have saved the uid in the comment collection. I just can't quite figure out how to map through all the comments and take the uid and use that to get the photoURL from the users collection. Below is what I have so far.
I'm new to React so apologies if this is a simple fix.
const [comments, setComments] = useState([])
// This is used to get all the comments
useEffect(() => {
let unsubscribe
if (postId) {
unsubscribe = db.collection('posts').doc(postId).collection('comments').orderBy('timestamp', 'desc').onSnapshot((snapshot) => {
setComments(snapshot.docs.map((doc) => doc.data()))
})
}
return () => {
unsubscribe()
}
}, [postId])
// This is for adding the comment to the comments collection
const postComment = (event) => {
event.preventDefault()
db.collection('posts').doc(postId).collection('comments').add({
text: comment,
username: user.displayName,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
photoURL: user?.photoURL, // This does not save anything because it's taking form auth user and not user collection
uid: user?.uid
})
setComment('')
setCommentActive(true)
setCommentsArrow(!commentsArrow)
}
// This is a function to get the photoURL of any user based on the uid
async function getImageURL(uid) {
return (await db.collection("users").doc(uid).get()).data().photoURL
}
// Rendering the comments
return (
<div style={{ transition: "height 0.3s linear" }} className={commentActive ? null : "hidden"}>
<div className="post__comments">
{comments.map((comment) => (
<div className="post__comment">
<Avatar
className="post__avatar"
alt=""
src={getImageURL(comment.uid)} // Calls the getImageURL function and passes in the uid from the comment
/>
<p><strong>{comment.username}</strong> {comment.text}</p>
</div>
))}
</div>
</div>
)

Here are the 2 options that I see are viable:
I see you are storing the users' UID in the comment doc, then make a separate request to Firestore (users collection) and then fetch their photoURL from that document.
This is what I prefer and seems to be the most viable way but to do so you would have to store the user images in Firebase Storage or any storage service. There you can structure your directory like /images/<user_uid>. So if my UID in the comment is 'uid123' then you would look for the image in Firebase Storage.
A download URL from Firebase Storage looks like:
https://firebasestorage.googleapis.com/v0/b/<project_id>.appspot.com/o/users%2F<userId>.png?alt=media&token=token
Now it does has that token at the end so you may have to use the getDownloadURL method or make your bucket public from the Google Cloud Console. But the format of URL will remains consistent i.e. https://domain.tld/path/image.png
This means users can't just simply paste an image URL to update their photo but they'll have to upload the image and you'll have to store it somewhere.
If you don't want to use storage or the 2nd method above, then you would have to make additional calls to the database to get users' profile picture.
Edit:
Function to get user's image URL from UID:
async function getImageURL(uid) {
return (await db.collection("users").doc(uid).get()).data().photoURL
}
Then in the component below:
{comments.map((comment) => (
<div className="post__comment">
<Avatar
className="post__avatar"
alt=""
src={getImageURL(comment.uid)}
/>
<p><strong>{comment.username}</strong> {comment.text}</p>
</div>
))}

Related

No display of array on website even though data is displayed in console (Firebase & Next.js)

Im building a simple event-page where you can create events and buy tickets for those events. I have all the event ids a user has created stored in a firebase firestore doc to access it i need the current logged in users uid. (thats why im pulling the data client-side, no getStaticProps, ... possible) after i get the created events from the user i search with those ids in my event-collection in firestore to get the data i need.
It seems that i cant access a single array-field. array[0] e.g. is displayed as undefined even though in the next moment I get the data loged on the console for the whole array, where array[0] [1] ... is existent.
My basic idea:
get user & uid from fb
get data from firestore with the uid i got
display data with .map
Code:
Variable Declaration:
const auth = getAuth();
const [events, setEvents] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect to get logged in user:
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
getEvents(user.uid);
setIsLoading(false);
}
});
}, []);
get data from firestore:
const getEvents = async (uid) => {
const createdEvents = [];
await getDoc(doc(db, "users", uid)).then((currentUser) => {
currentUser.data().events.forEach(async (eventId) => {
await getDoc(doc(db, "events", eventId)).then((event) => {
createdEvents.push({ ...event.data(), id: event.id });
});
});
setEvents(createdEvents);
});
};
Display data on website:
{isLoading ? (
<span>loading events..</span>
) : (
events.map((event) => {
return <p key={event.id}>{console.log(event.title)}</p>;
})
)}
console outputs:
{isLoading ? null : console.log("event Array: ", events)} {/* data outputs in console */}
{isLoading ? null : console.log("single array element: ", events[0])} {/* is undefined */}
enter image description here
Note: the array renders twice, first time with no data because of the variable declaration (as expected) an second time with data, i just dont know why it wont display on the website.
I appreciate any help, thanks a lot!
I tried moving some variables and changing when the async function fires, nothing helps. Also i tried waiting for the data in the array with events && events.map ...

How do I structure a fetch request after an other?

I have a React blog and I am using Firebase as a back-end. I use createUserWithEmailAndPassword method and after authentication users are redirected to blog where they can start adding blogs.
I store their posts in a firestore collection "posts". No problem with that.
I also want a user object to be created after login with the user preferences. Let's say a specific theme each one has chosen.
I create a collection called "users" at firestore and where I will store each ones object {user: "random#hotmail.com, theme: "dark" ,isAdmin: false, etc} using addDoc method.
I want this object to be created once though and not every time a user logs in.
How do the check on that "users" collection if the user already exists?
I get the collection getDocs(userCollectionRef) and then I filter the data let's say by a property to see if there is that object there.
And if not I want to add the document using addDoc method.
this is the request:
useEffect(() => {
const createUserData = async () => {
const data = await getDocs(usersCollectionRef);
const docs = data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
const userData = docs.filter((doc) => doc.user === user.email);
if (userData.length === 0) {
await addDoc(usersCollectionRef, {
user: user.email,
isAdmin: false,
theme: "dark",
});
}
};
if (user) {
createUserData();
}
}, [user, usersCollectionRef]);
It seems like I am trying
to check and add to the collection at the same time and this is why it doesn't work.
Any ideas?
Should I have an in-between step where I store what I'm getting from the getDocs in a state or something and then do the second request?
Can anyone explain please?
I changed the if statement to a return like so and it worked.
return (
userData.length === 0 &&
(await addDoc(usersCollectionRef, {
user: user.email,
wordGoal: wordGoal,
}))
);
I guess I return the second promise after the first now that's why
I guess when the getDocs request happens because its asynchronous the data then are empty then the code proceeds to the if statement which at that point is true (we have no records) and so executes it.

How to create an approval post in Reactjs?

I need to approve "posts" before going online,
the first question is do I need to code on the backend, or I can do it only in the front-end in order to add the functionality?
so I have some code that creates a post, and I need to send data to the confirmation page before going to the main page.
here is the code for post creation:
const onSubmit = (data) => {
if (data.postText) {
const formData = postToFormData(data, file, createTags);
axios
.post(`/posts`, formData, {
headers: {
"Content-Type": "multipart/form-data",
accessToken: localStorage.getItem("accessToken"),
},
})
.then((res) => {
setTimeout(() => {
history.push("/approving");
}, 2000);
});
setMessage("posct created successfully!");
}
};
so as you see in the code above, we have a post method to post the data, and I need to send the data to the approving page where I need to see all the data and press "approve" or "reject"
i have 3 states in the main page to display the data:
#{value.fullName}
{value.postTitle}
{value.postText}
I would appreciate your help.
I think that you don't need to modify the backend. Only send a PUT request to update the post (if the backend accept this).
Regarding the other question, you don't need to redirect to another url to show new content in react. You can show different content depending in the state.
const Form = ({submitting, handleSubmit, formData, setFormData}) => {
return <form onSubmit={handleSubmit}>
<input
type="text"
required
value={formData.text}
onChange={(e) => setFormData({text: e.target.value})}
/>
<br />
<button type="submit">{submitting ? 'Sending...' : 'Save'}</button>
</form>
}
const Revision = ({formData}) => {
return <div>
<p>Your sucess message...</p>
<p>{formData.text }</p>
<button>Approve</button>
<button>Reject</button>
</div>
}
const App = () => {
const [submitting, setSubmitting] = React.useState(false)
const [submitted, setSubmitted] = React.useState(false)
const [formData, setFormData] = React.useState({text: ''})
const handleSubmit = (e) => {
e.preventDefault()
setSubmitting(true)
// Fake send post data
setTimeout(() => {
setSubmitted(true)
}, 1000)
}
const handleApprove = () => {
// Send PUT request to update your post.
}
const handleReject = () => {
// Send PUT request to update your post.
}
if (submitted) {
return <Revision
formData={formData}
handleApprove={handleApprove}
handleReject={handleReject}
/>
} else {
return <Form
submitting={submitting}
handleSubmit={handleSubmit}
formData={formData}
setFormData={setFormData}
/>
}
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="container"></div>
I don't quite understand what you means by "approve post before going online". But here the scenarios which I think I understood:-
a) CONFIRMATION FEATURE before submitting new post to backend (basically create a new set of data in the database) javascript
b) APPROVE or REJECT FEATURES after new post successfully submitted & created in the database
For SCENARIO A:-
you may create a component (like a popup or something) that shows user to choose whether they want to Confirm or Cancel creation of new post data.
Confirm - will triggered the onSubmit function and send the data to the backend to create a new post
Cancel - will reset every states to it's default value/state (basically emptying out every input placeholders)
For SCENARIO B:-
you must have some kind of variables in the database that can distinguish which post is approved & which is not. (like status or publish_status - can be set to either Yes or No or 1 or 0)
create a new function to handle just changing/update of status/publish_status var/value in the database of said/selected post. This can be done right after creation of new post (like what you want) or just at anytime you want (basically you have a working UI that handles a list of posts with just the id, title, status/publish_status & action buttons like edit, delete and/or publish, typically we display this in table - so if you hit action button publish, then it will triggered the update/put req to change/update the status/publish_status in the database. Just make sure to change your publish action button text to unpublish if the put req successful)

how do I display firestore data with React

I am trying to render my firestore data to my web app using React.
here is my code
I can see the document ID when I console.log(posts.id), however I cannot find the content of that document.
Here are the content of a document
The method for getting the data is .data, as in:
console.log(posts[0].data())
To render something for each post might be something like:
<p>
{posts.map(doc => {
const data = doc.data();
return (
<div>{data.title}</div>
)
})}
</p>
Personally, unless i need something other than the data from the document, i tend to do my calls to .data() before i even put it into react state. That way i don't need to do it on each render. e.g:
useEffect(() => {
database.posts.get().then(snapshot => {
setPosts(snapshot.docs.map(doc => doc.data());
});
});

Local Storage values not getting loaded in React App

I am new to React and following one of the video series available on Udemy (React for the rest of us by Brad Schiff). The author has provided a backend system for user login etc.
When the user logs in, the backend server sends 3 values in response to a successful login: the login token, the user name and the avatar url (a dummy image from gravatar.com). These values are then stored in local storage from where they can be displayed on the UI:
const response = await axios.post("http://localhost:8080/login", {username, password});
if (response.data) {
// if response contains token, set login flag to true
props.loginFlagHandler(true);
// save user token, username, avatar in localstorage
localStorage.setItem("username", response.data.username);
localStorage.setItem("loginToken", response.data.token);
localStorage.setItem("avatar", response.data.avatar); }
Now, when I log in and the UI updates, the values received from server are successfully stored in local storage of my browser, but they are not getting reflected on the UI. The avatar image doesn't load and the Welcome message that should display the username doesn't work as expected (see the 1st image below). The Welcome message should read "Hello <username>, your feed is empty"
However, once I hit refresh, both the Avatar Image and the User name get displayed as they should, on their relevant place. (refer 2nd image).
In both the cases, I am referring to the localStorage variable as below:
<img className="small-header-avatar" src={localStorage.getItem("avatar")} />
And:
<h2 className="text-center">
Hello <strong>{localStorage.getItem("username")}</strong>, your feed is empty.
</h2>
I think this should be a noob thing that I'm missing here. Nevertheless, thanks in advance:
In React, when a component mounts
states are load
UI prints with the initial states
Lifecycle hooks run (e.g. useEffect) and update states
DOM re-prints with updated states
if you don't use state in your component then you should follow the above workflow to update your UI. Though I don't know what your props.loginFlagHandler(true) does, I am giving an example below.
const [isDataStored, setIsDataStored] = useState(false)
useEffect(() => {
const fetchData = async () => {
const response = await axios.post("http://localhost:8080/login", {username, password});
if (response.data) {
// if response contains token, set login flag to true
props.loginFlagHandler(true);
// save user token, username, avatar in localstorage
localStorage.setItem("username", response.data.username);
localStorage.setItem("loginToken", response.data.token);
localStorage.setItem("avatar", response.data.avatar);
}
setIsDataStored(true)
}
}, [])
you can also try lazy loading your child component.
Well you should use state like this.
const [avatar, setAvatar] = useState("");
const [username, setUsername] = useState("");
useEffect(() => {
setUsername(localStorage.getItem("username"))
setAvatar(localStorage.getItem("avatar"))
})
................
<img className="small-header-avatar" src={avatar} />
And:
<h2 className="text-center">
Hello <strong>{username}</strong>, your feed is empty.
</h2>

Resources