Posting an array as part of a Schema with React and Axios - reactjs

I have the following Schema:
const SubmitDebtSchema = new Schema ({
balance: [{newBalance: Number, balanceDate: Date}],
});
What I want to do is make a post request from my React frontend using Axios, and save the 'newBalance' as a number pulled from state and balanceDate as today's date.
However, I can't figure out how to access the 'newBalance' and balanceDate.
If I try the following, it just posts an empty array to my database:
onSubmit = async (e) => {
e.preventDefault();
const dayCurrent = new Date().toLocaleString();
await axios.post("/api/submit/submitDebt", {
newBalance: this.state.balance,
balanceDate: dayCurrent,
})
this.props.history.push('/dashboard');
}
And similarly, if I try the following, it errors out:
onSubmit = async (e) => {
e.preventDefault();
const dayCurrent = new Date().toLocaleString();
await axios.post("/api/submit/submitDebt", {
balance.newBalance: this.state.balance,
balance.balanceDate: dayCurrent,
})
this.props.history.push('/dashboard');
}
So how do I access my Schema's newBalance and balanceDate, and append new entries rather than replace the original ones?
Any feedback would be appreciated!

Related

Fetch returns undefined the first time a user signs in

I sort of know why it happens, but not sure how to go on about solving it.
I have a React project that uses Cloud Firestore as database, and I have a simple login-page where you can sign in via your Google account. The first time you sign in a new document gets added to the "users" collection in Firebase.
After the document has been created it fetches that user data from Firebase and stores it in Redux.
const signInWithGoogle = async () => {
try {
const res = await signInWithPopup(auth, googleProvider);
const user = res.user;
const q = query(collection(db, "users"), where("uid", "==", user.uid));
const docs = await getDocs(q);
if(docs.docs.length === 0){
const firstName = user.displayName.split(' ')[0];
await addDoc(collection(db, "users"), {
uid: user.uid,
name: user.displayName,
firstName: firstName,
photoURL: user.photoURL,
authProvider: "google",
email: user.email,
})
dispatch(getUser(user))
}
} catch(err) {
console.error(err);
alert(err.message);
}
}
I also check whenever the user's auth state changes (here I also do another fetch and store it in Redux).
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
if(user){
dispatch(getUser(user))
} else {
console.log("user logout")
}
});
return unsubscribe;
}, []);
But when a new user signs in the first time, I get an error from the fetch:
export const getUser = createAsyncThunk("profile/getUser", async (user) => {
try {
const userQuery = query(
collection(db, "users"),
where("uid", "==", user?.uid)
);
const doc = await getDocs(userQuery);
const data = doc.docs[0].data();
return data;
} catch (err) {
console.error(err);
alert("An error occured while fetching user data");
}
});
"data" in above block is undefined for a small moment when the user signs in, so the alert in the try/catch block always goes off (it does manage to fetch the data after though).
This error only happens when it's a new user.
I understand that the fetch occurs before a document has been created in the "users" collection, but I'm not sure how to solve this. I've tried to add if/else to certain parts of the code (but just felt like I was grasping for straws).
I'm very much new to Firebase and still learning React, so every bit of help is really appreciated!
Problem is that your signInWithGoogle & useEffect both are running on user's auth status change. And, when its the new user, signInWithGoogle function makes aysnc call to create default doc, whereas useEffect runs to dispatch action, but at that moment user doesn't have any linked document. That is why you are getting undefined.
Ideally, you should remove one. You can merge the useEffect into signInWithGoogle to set the user details and dispatch as well.
const signInWithGoogle = async () => {
try {
const res = await signInWithPopup(auth, googleProvider);
const user = res.user;
const q = query(collection(db, "users"), where("uid", "==", user.uid));
const docs = await getDocs(q);
// create `doc` if its the new user
if(docs.docs.length === 0){
const firstName = user.displayName.split(' ')[0];
await addDoc(collection(db, "users"), {
uid: user.uid,
name: user.displayName,
firstName: firstName,
photoURL: user.photoURL,
authProvider: "google",
email: user.email,
})
}
// set user info and dispatch
setCurrentUser(user);
setLoading(false);
dispatch(getUser(user))
} catch(err) {
console.error(err);
alert(err.message);
}
}
Hope that answers your query.
Thanks

Firebase v9 signup a new user and store users in a users collection

I am able to sign up a new user, but how can I store that user in a collection in firestore? I am not sure with firebase v9 if the addDoc function is in the right place, but I don't know where else or how to code it.
export const useSignup = () => {
const [error, setError] = useState("");
const { dispatch } = useAuthContext();
const signup = (email: string, password: string, username: string) => {
setError("");
createUserWithEmailAndPassword(auth, email, password)
.then((res) => {
dispatch({ type: "LOGIN", payload: res.user });
const uid = res.user.uid;
const data = {
id: uid,
email,
username,
};
const ref = collection(db, "users");
addDoc(ref, {
data,
});
})
.catch((err) => {
setError(err.message);
});
};
return { error, signup };
};
To create a user-document for a newly signed-up user, you can do the following:
Access their bid from the user object (like you already did).
Create a document reference whose path ends in the above mid. Be aware that this document does not exist yet.
Use the setDoc method with above document reference, and user data to be stored as inputs.
It looks like the following in the form of code:
const uid = res.user.uid;
const data = {
id: uid,
email,
username,
};
const ref = collection(db, `users/${uid}`);
setDoc(ref, data)
.then(() => console.log("Created New User Document Successfully"))
.catch(() => console.log("Error"))
You must write the above code right after your dispatch call. Hope this helps!

Firebase v9.8.4 deleteDoc Not Deleting from db

I have a collection of Posts that are added to my db via this method:
const sendPost = async (event: any) => {
event.preventDefault();
if (loading) return;
setLoading(true);
const docRef = await addDoc(collection(db, "Posts"), {
id: tokenId,
postId: UID,
username: username,
profilePic: uri,
bio: bio,
likes: 0,
text: input,
timestamp: serverTimestamp(),
});
I have created a delete button with an onClick handler with the following code:
const handleDelete = async (e:any) => {
e.stopPropagation();
deleteDoc(doc(db, "Posts", post.postId));
console.log(post.postId);
}
The console.log in the above code matches the UID of the Document that im trying to delete, but nothing happens. Note: I added delete to the rules, and still no luck.
Does anybody know if I'm missing a step or can point me in the right direction??
Visual
Thanks!
You just don't know what is doc ID. Using addDoc(collection(db, "Posts"), data) function, you generate random document ID, and it is not an UID! To get that ID, you need to show how you get data from Firebase.
Using getDocs function, you can get doc ID like this:
async function getDocuments() {
const ref = collection(db, 'Posts')
const result = await getDocs(ref)
let data = []
if(result.exists()) {
result.foreach(docSnap => {
const doc = docSnap.data()
doc.docID = docSnap.id // here you getting real document ID
data.push(doc)
}
)
}
}
You have document ID above + Start Collection blue button, not in postID field!

Check setState has run before executing API call in React

I have a form with a text field and a form input which accepts multiple files. OnSubmit the files are sent to Firebase Storage, which sends back a URL for each file. These URLs are then stored in a 'photosURL' array in the form object, which is then posted to MongoDB.
The problem is, every time I post the form object data to Mongo, the photos array is empty, despite the console log showing it to be populated before I call the post-to-Mongo code. This leads me to think the post-to-Mongo code is using the form object value before it has been populated with the photo URLs.
The question is, how do I check that the photo array has been populated before I run the code to push the data to MongoDB? I'm already using a Promise.all to in theory wait for all the files to be sent and the URLs returned, but I can't work out why else the photoURLs array is empty every time data is sent to Mongo.
Here's the code:
const [form, setForm] = useState({
userId: '',
post: '',
createdAt: createdAt,
photoURLs: [],
})
const handleSubmit = (e) => {
e.preventDefault()
newPost ? postData(form) : ...
}
// SEND FILE TO FIREBASE AND GET BACK THE URL
async function handleUpload(file) {
const storageRef = useStorage.ref("PostImages");
const fileRef = storageRef.child(`${nanoid()}`);
return fileRef.put(file).then(() => {
return fileRef.getDownloadURL().then(function (url) {
photoArray.push(url);
setForm(prevState => ({ ...prevState, photos: photoArray }))
});
});
}
// POST FUNCTION
const postData = async (form) => {
setLoading(true)
let thisFileArray = fileInput.current.files;
const uploadTasks = [];
for (let i = 0; i < thisFileArray.length; i++) {
uploadTasks.push(handleUpload(thisFileArray[i]));
}
Promise.all(uploadTasks).then(() => {
axios.post('/api/posts', form)
.then(response => {
...
})
.catch(error => {
...
})
})
}
Can anyone see what's going wrong, please?
EDIT: This is a consolel log of the form object, called before the axios.post code (it's showing the photosURL as populated):
createdAt: 1630072305502
photos:
0: "https://firebasestorage.googleapis.com/..."
1: "https://firebasestorage.googleapis.com/..."
post: "sample text"
userId: "1iNGV..."
I think that you are running into a timing issue.
Don't forget that React state updates are asynchronous, as described here.
I suggest to pass your URLs directly instead of going through your component's state:
async function handleUpload(file) {
const storageRef = useStorage.ref("PostImages");
const fileRef = storageRef.child(`${nanoid()}`);
await fileRef.put(file);
const url = await fileRef.getDownloadURL();
return url; // Send back the download URL
}
const postData = async (form) => {
setLoading(true);
let thisFileArray = fileInput.current.files;
const uploadTasks = [];
for (let i = 0; i < thisFileArray.length; i++) {
uploadTasks.push(handleUpload(thisFileArray[i]));
}
const photos = await Promise.all(uploadTasks); // Get all URLs here
await axios.post('/api/posts', {...form, photos}); // Send URLs to your server
setLoading(false);
}
If I understood correct, You want to upload files first and when you get your urls array populated then only you want to call postData function?
If that's the case, then you can use useEffect to detect the urls change.
useEffect(() => {
// Here you call your postData function.
postData();
}, [form.photoURLs])
What this will do is, Whenever your form.photoURLs gets populated this useEffect will run and will make the request to the server with proper data.

Upload multiple files and query Firestore DB - ReactJS

I have been reading quite a lot posts like this one How to upload multiple files to Firebase? about this question before and I tried different options but can’t get my result.
I am using React Hook Form for an insanely huge form that now I have to add an input more (to upload files), and need help doing everything at once.
My code to update one file:
const onChangeFile = async (e: any) => {
const file = e.target.files[0];
const storageRef = app.storage().ref();
const fileRef = storageRef.child(file.name);
await fileRef.put(file);
setFileUrl(await fileRef.getDownloadURL());
};
const onSubmit = async (data: any) => {
try {
await db
.collection('listings')
.doc()
.set({ ...data, created: new Date(), uid: currentUser.uid, file: fileUrl });
reset();
} catch (error) {
}
};
So, as you can see, uploading only one file is quite straightforward, but a different story is when uploading many. Thing is, I would need more functions inside my onSubmit, which is itself an async funcion, so that limits the amount of things I can do inside of it.
Does anyone have an simple workaround?
If e.target.files has multiple files then you can push the upload operations to an array and then upload them using Promise.all():
const onChangeFile = async (e: any) => {
const storageRef = app.storage().ref();
const fileUploads = e.target.files.map(file => storageRef.child(file.name).put(file));
const fileURLReqs = (await Promise.all(fileUploads)).map(f => f.ref.getDownloadURL())
const fileUrls = await Promise.all(fileURLReqs)
// setFileUrl(await fileRef.getDownloadURL());
};
fileUrls will be an array of URLs of all the uploads images which onSubmit can be uploaded to Firestore if needed.
I'm not sure if you are adding a new document for each image but if you are, try this:
// in onSubmit
const listingsCol = db.collection("listings")
const files = [] // get that fileUrls array from your state
const updates = files.map(file => listingsCol.add({...data, file}))
await Promise.all(updates)
// this will add a new document for each new file uploaded
Thanks to #Dharmaraj, I have arrived to a simple working version of this popular question, I think he has given a very clear and straightforward solution for it and the credit is totally his.
Nonetheless, my implementation could have weaknesses even though its working (so far), so I would like to post it, you can use it or point out possible red flags.
const Form: React.FC = () => {
const [filesUrl, setFilesUrl] = useState<any[]>([]);
const { register, handleSubmit, reset } = useForm({ defaultValues });
const onChangeFile = async (e: any) => {
const storageRef = app.storage().ref();
const fileUploads = Array.from(e.target.files).map((file: any) => storageRef.child(file.name).put(file));
const fileURLReqs = (await Promise.all(fileUploads)).map((f: any) => f.ref.getDownloadURL());
const fileUrls = await Promise.all(fileURLReqs);
setFilesUrl(fileUrls);
};
const onSubmit = async (data: any) => {
try {
await db
.collection('collName')
.doc()
.set({ ...data, created: new Date(), uid: currentUser.uid, file: filesUrl });
reset();
} catch (error) {
console.log(error)
}
};

Resources