I have this data from firestore and I wanted to retrieve it dynamically with a where() but this is the error I'm getting:
TypeError: vaccines is not a function
The user collection:
[![enter image description here][1]][1]
Below are the codes:
const Vaccine = () => {
const [vaccines, setVaccines] = useState([]);
useEffect(() => {
const unsubscribe = firestore
.collection("vaccines")
.onSnapshot((snapshot) => {
const arr = [];
snapshot.forEach((doc) =>
arr.push({
...doc.data(),
id: doc.id,
})
);
setVaccines(arr);
});
return () => {
unsubscribe();
};
}, []);
Preface
As highlighted in the comments on the original question, this query structure is not advised as it requires read access to sensitive user data under /users that includes private medical data.
DO NOT USE THIS CODE IN A PRODUCTION/COMMERICAL ENVIRONMENT. Failure to heed this warning will lead to someone suing you for breaches of privacy regulations.
It is only suitable for a school project (although I would a fail a student for such a security hole) or proof of concept using mocked data. The code included below is provided for education purposes, to solve your specific query and to show strategies of handling dynamic queries in React.
From a performance standpoint, in the worst case scenario (a cache miss), you will be billed one read, for every user with at least one dose of any vaccine, on every refresh, for every viewing user. Even though your code doesn't use the contents of any user document, your code must download all of this data too because the Client SDKs do not support the select() operator.
For better security and performance, perform this logic server-side (e.g. Cloud Function, a script on your own computer, etc) and save the results to a single document that can be reused by all users. This will allow you to properly tighten access to /users. It also significantly simplifies the code you need to display the graphs and live statistics on the client-side.
useEffect
As stated by the React documentation on the Rules of hooks:
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
The documentation further elaborates that React relies on the order in which Hooks are called, which means that you can't have hook definitions behind conditional logic where their order and quantity changes between renders. If your hooks rely on some conditional logic, it must be defined inside of the hook's declaration.
As an example, if you have an effect that relies on other data, with this logic:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
if (userProfile) {
useEffect(() => {
// get user post list and store in userPosts
}, [userProfile]);
}
you need to instead use:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
useEffect(() => {
if (!userProfile) {
// not ready yet/signed out
setUserPosts(null);
return;
}
// get user post list and store in userPosts
}, [userProfile]);
Similarly, for arrays:
someArray && someArray.forEach((entry) => {
useEffect(() => {
// do something with entry to define the effect
}, /* variable change hooks */);
});
should instead be:
useEffect(() => {
if (!someArray) {
// not ready yet
return;
}
const cleanupFunctions = [];
someArray.forEach((entry) => {
// do something with entry to define an effect
cleanupFunctions.push(() => {
// clean up the effect
});
});
// return function to cleanup the effects created here
return () => {
cleanupFunctions.forEach(cleanup => cleanup());
}
}, /* variable change hooks */);
Because this looks a lot like lifecycle management, you are actually better off replacing it with nested components rather than using hooks, like so:
return (
<> // tip: React.Fragment shorthand (used for multiple top-level elements)
{
someArray && someArray
.map(entry => {
return <Entry key={entry.key} data={entry.data} />
})
}
</>
);
Adapting to your code
Note: The code here doesn't use onSnapshot for the statistics because it would cause a rerender every time a new user is added to the database.
const getVaccineStats = (vaccineName) => {
const baseQuery = firestore
.collection("users")
.where("doses.selectedVaccine", "==", vaccine);
const oneDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.get()
.then(querySnapshot => querySnapshot.size);
const twoDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.get()
.then(querySnapshot => querySnapshot.size);
return Promise.all([oneDoseQueryPromise, twoDoseQueryPromise])
.then(([oneDoseCount, twoDoseCount]) => ({ // tip: used "destructuring syntax" instead of `results[0]` and `results[1]`
withOneDose: oneDoseCount,
withTwoDoses: twoDoseCount
}));
};
const Vaccine = () => {
const [vaccines, setVaccines] = useState();
const [vaccineStatsArr, setVaccineStatsArr] = useState([]);
// Purpose: Collect vaccine definitions and store in `vaccines`
useEffect(() => {
return firestore // tip: you can return the unsubscribe function from `onSnapshot` directly
.collection("vaccines")
.onSnapshot({ // tip: using the Observer-like syntax, allows you to handle errors
next: (querySnapshot) => {
const vaccineData = []; // tip: renamed `arr` to indicate what the data contains
querySnapshot.forEach((doc) =>
vaccineData.push({
...doc.data(),
id: doc.id,
});
);
setVaccines(vaccineData);
}),
error: (err) => {
// TODO: Handle database errors (e.g. no permission, no connection)
}
});
}, []);
// Purpose: For each vaccine definition, fetch relevant statistics
// and store in `vaccineStatsArr`
useEffect(() => {
if (!vaccines || vaccines.length === 0) {
return; // no definitions ready, exit early
}
const getVaccineStatsPromises = vaccines
.map(({ vaccine }) => [vaccine, getVaccineStats(vaccine)]);
// tip: used "destructuring syntax" on above line
// (same as `.map(vaccineInfo => [vaccineInfo.vaccine, getVaccineStats(vaccineInfo.vaccine)]);`)
let unsubscribed = false;
Promise.all(getVaccineStatsPromises)
.then(newVaccineStatsArr => {
if (unsubscribed) return; // unsubscribed? do nothing
setVaccineStatsArr(newVaccineStatsArr);
})
.catch(err => {
if (unsubscribed) return; // unsubscribed? do nothing
// TODO: handle errors
});
return () => unsubscribed = true;
}, [vaccines]);
if (!vaccines) // not ready? hide element
return null;
if (vaccines.length === 0) // no vaccines found? show error
return (<span class="error">No vaccines found in database</span>);
if (vaccineStatsArr.length === 0) // no stats yet? show loading message
return (<span>Loading statistics...</span>);
return (<> // tip: React.Fragment shorthand
{
vaccineStatsArr.map(([name, stats]) => {
// this is an example component, find something suitable
// the `key` property is required
return (<BarGraph
key={name}
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>);
});
}
</>);
};
export default Vaccine;
Live Statistics
If you want your graphs to be updated live, you need "zip together" the two snapshot listeners into one, similar to the rxjs combineLatest operator. Here is an example implementation of this:
const onVaccineStatsSnapshot => (vaccine, observerOrSnapshotCallback, errorCallback = undefined) => {
const observer = typeof observerOrCallback === 'function'
? { next: observerOrSnapshotCallback, error: errorCallback }
: observerOrSnapshotCallback;
let latestWithOneDose,
latestWithTwoDoses,
oneDoseReady = false,
twoDosesReady = false;
const fireNext = () => {
// don't actually fire event until both counts have come in
if (oneDoseReady && twoDosesReady) {
observer.next({
withOneDose: latestWithOneDose,
withTwoDoses: latestWithTwoDoses
});
}
};
const fireError = observer.error || (err) => console.error(err);
const oneDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.onSnapshot({
next: (querySnapshot) => {
latestWithOneDose = querySnapshot.size;
oneDoseReady = true;
fireNext();
},
error: fireError
});
const twoDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.onSnapshot({
next: (querySnapshot) => {
latestWithTwoDoses = querySnapshot.size;
twoDosesReady = true;
fireNext();
},
error: fireError
});
return () => {
oneDoseUnsubscribe();
twoDoseUnsubscribe();
};
}
You could rewrite the above function to make use of useState, but this would unnecessarily cause components to rerender when they don't need to.
Usage (direct):
const unsubscribe = onVaccineStatsSnapshot(vaccineName, {
next: (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
},
error: (err) => {
// TODO: error handling
}
);
or
const unsubscribe = onVaccineStatsSnapshot(vaccineName, (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
});
Usage (as a component):
const VaccineStatsGraph = (vaccineName) => {
const [stats, setStats] = useState(null);
useEffect(() => onVaccineStatsSnapshot(vaccineName, {
next: (newStats) => setStats(newStats),
error: (err) => {
// TODO: Handle errors
}
}, [vaccineName]);
if (!stats)
return (<span>Loading graph for {vaccineName}...</span>);
return (
<BarGraph
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>
);
}
vaccines is an array and not a function. You are trying to run a map on vaccines. Try refactoring your code to this:
vaccines &&
vaccines.map((v, index) => {
// ...
})
Also do check: How to call an async function inside a UseEffect() in React?
here is the code, that works for you:
function DatafromFB() {
const[users, setUsers] = useState({});
useEffect(()=>{
const fetchVaccine = async () => {
try {
const docs = await db.collection("vaccines").get();;
docs.forEach((doc) => {
doc.data().vaccineDetails
.forEach(vaccineData=>{
fetchUsers(vaccineData.vaccine)
})
})
} catch (error) {
console.log("error", error);
}
}
const fetchUsers = async (vaccine)=>{
try {
const docs = await db.collection("users")
.where("doses.selectedVaccine", "==", vaccine).get();
docs.forEach(doc=>{
console.log(doc.data())
setUsers(doc.data());
})
}catch(error){
console.log("error", error);
}
}
fetchVaccine();
},[])
return (
<div>
<h1>{users?.doses?.selectedVaccine}</h1>
</div>
)
}
export default DatafromFB
what is ${index.vaccine} I think it must be v.vaccine
also setSize(snap.size); will set set size commonly not vaccine specific
I'm trying to check if certain field in a collection has a value.
I have a collection of users and that collection has a documents by their uid than that document has a field bio.
I want to check if that field is empty or not.
This is where i'm at at the moment...
const docRef = db.collection('users').doc(auth.user.uid).get()
docRef.then((querySnapshot) => {
if(querySnapshot.docs[0].data() != ''){
//there is value in field
}
})
By doing db.collection('users').doc(auth.user.uid).get() you define a Promise that resolves with a DocumentSnapshot (and not a QuerySnapshot). A DocumentSnapshot does not have any docs property.
So you need to do as follows:
const docRef = db.collection('users').doc(auth.user.uid).get();
docRef.then((docSnapshot) => {
if (docSnapshot.exists) {
if (docSnapshot.data().bio === '') {
// Bio is empty
}
} else {
// docSnapshot.data() will be undefined in this case
console.log("No such document!");
}
}).catch((error) => {
console.log("Error getting document:", error);
});
Note that from a naming convention perspective, it would make more sense to write:
const docRef = db.collection('users').doc(auth.user.uid); // This is a DocumentReference
docRef.get().then((doc) => {...});
I am developing the backend of an application using Node JS, Sequelize and Postgres database.
When the course is registered, the user must inform which organizations, companies and teachers will be linked to it.
The organization IDs are passed through an array to the backend, I am trying to do a check to make sure that the passed IDs exist.
What I've done so far is this:
const { organizations } = req.body;
const organizationsArray = organizations.map(async (organization) => {
const organizationExists = await Organization.findByPk(organization);
if (!organizationExists) {
return res
.status(400)
.json({ error: `Organization ${organization} does not exists!` });
}
return {
course_id: id,
organization_id: organization,
};
});
await CoursesOrganizations.bulkCreate(organizationsArray);
This link has the complete controller code, I believe it will facilitate understanding.
When !OrganizationExists is true, I am getting the return that the organization does not exist. The problem is when the organization exists, I am getting the following message error.
The Array.map() is returning an array of promises that you can resolve to an array using Promise.all(). Inside the map you should use throw new Error() to break out of the map - this error will be raised by Promise.all() and you can then catch it and return an error to the client (or swallow it, etc).
This is a corrected version of your pattern, resolving the Promise results.
const { organizations } = req.body;
try {
// use Promise.all to resolve the promises returned by the async callback function
const organizationsArray = await Promise.all(
// this will return an array of promises
organizations.map(async (organization) => {
const organizationExists = await Organization.findByPk(organization, {
attributes: ['id'], // we only need the ID
raw: true, // don't need Instances
});
if (!organizationExists) {
// don't send response inside the map, throw an Error to break out
throw new Error(`Organization ${organization} does not exists!`);
}
// it does exist so return/resolve the value for the promise
return {
course_id: id,
organization_id: organization,
};
})
);
// if we get here there were no errors, create the records
await CoursesOrganizations.bulkCreate(organizationsArray);
// return a success to the client
return res.json({ success: true });
} catch (err) {
// there was an error, return it to the client
return res.status(400).json({ error: err.message });
}
This is a refactored version that will be a bit faster by fetching all the Organizations in one query and then doing the checks/creating the Course inserts.
const { Op } = Sequelize;
const { organizations } = req.body;
try {
// get all Organization matches for the IDs
const organizationsArray = await Organization.findAll({
attributes: ['id'], // we only need the ID
where: {
id: {
[Op.in]: organizations, // WHERE id IN (organizations)
}
},
raw: true, // no need to create Instances
});
// create an array of the IDs we found
const foundIds = organizationsArray.map((org) => org.id);
// check to see if any of the IDs are missing from the results
if (foundIds.length !== organizations.length) {
// Use Array.reduce() to figure out which IDs are missing from the results
const missingIds = organizations.reduce((missingIds, orgId) => {
if (!foundIds.includes(orgId)){
missingIds.push(orgId);
}
return missingIds;
}, []); // initialized to empty array
throw new Error(`Unable to find Organization for: ${missingIds.join(', ')}`);
}
// now create an array of courses to create using the foundIds
const courses = foundIds.map((orgId) => {
return {
course_id: id,
organization_id: orgId,
};
});
// if we get here there were no errors, create the records
await CoursesOrganizations.bulkCreate(courses);
// return a success to the client
return res.json({ success: true });
} catch (err) {
// there was an error, return it to the client
return res.status(400).json({ error: err.message });
}
If you have an array of Ids and you want to check if they exist you should you use the (in) operator, this makes it so that you are hitting the DB only once and getting all the records in one hit (instead of getting them one by one in a loop), after you get these records you can check their lengths to determine if they all exist or not.
const { Op } = require("sequelize");
let foundOrgs = await Organization.findAll({
where: {
id: {
[Op.in]: organizationsArray,
}
}
});
I am using Firebase authentication to store users. I have two types of users: Manager and Employee. I am storing the manager's UID in Firestore employee along with the employee's UID. The structure is shown below.
Firestore structure
Company
|
> Document's ID
|
> mng_uid: Manager's UID
> emp_uid: Employee's UID
Now I want to perform a query like "Retrieve employees' info which is under the specific manager." To do that I tried to run the below code.
module.exports = {
get_users: async (mng_uid, emp_uid) => {
return await db.collection("Company").where("manager_uid", "==", mng_uid).get().then(snaps => {
if (!snaps.empty) {
let resp = {};
let i = 0;
snaps.forEach(async (snap) => {
resp[i] = await admin.auth().getUser(emp_uid).then(userRecord => {
return userRecord;
}).catch(err => {
return err;
});
i++;
});
return resp;
}
else return "Oops! Not found.";
}).catch(() => {
return "Error in retrieving employees.";
});
}
}
Above code returns {}. I tried to debug by returning data from specific lines. I got to know that the issue is in retrieving the user's info using firebase auth function which I used in forEach loop. But it is not returning any error.
Thank you.
There are several points to be corrected in your code:
You use async/await with then() which is not recommended. Only use one of these approaches.
If I understand correctly your goal ("Retrieve employees' info which is under the specific manager"), you do not need to pass a emp_uid parameter to your function, but for each snap you need to read the value of the emp_uid field with snap.data().emp_uid
Finally, you need to use Promise.all() to execute all the asynchronous getUser() method calls in parallel.
So the following should do the trick:
module.exports = {
get_users: async (mng_uid) => {
try {
const snaps = await db
.collection('Company')
.where('manager_uid', '==', mng_uid)
.get();
if (!snaps.empty) {
const promises = [];
snaps.forEach(snap => {
promises.push(admin.auth().getUser(snap.data().emp_uid));
});
return Promise.all(promises); //This will return an Array of UserRecords
} else return 'Oops! Not found.';
} catch (error) {
//...
}
},
};
I am creating a Stripe payment system, and am having difficulty making this integrated code functional. The code was pulled from here. What I am trying to do is create a database reference to the specific user ID (uid) of the user, since the createStripeCharge waits for a write event to occur to something like /stripe_customers/XCXaweADowefj/charges/QC123XYZ. I'm not sure, but perhaps my issue is that I am incorrectly referencing the customer. If you have any ideas or feedback about how I can make this function correctly I greatly appreciate it!
The Trigger (this causes the Cloud Function to work):
payByStripe(amount,email,userId, token): firebase.database.Reference {
return firebase.database().ref(`/stripe_customers/${userId}/charges/`).push({
amount:amount,
email:email,
token:token
});
}
Cloud Function:
exports.createStripeCharge = functions.database.ref('/stripe_customers/{userId}/charges/{id}').onWrite(event => {
const val = event.data.val();
if (val === null || val.id || val.error) return null;
return admin.database().ref(`/stripe_customers/${event.params.userId}/customer_id`).once('value').then(snapshot => {
return snapshot.val();
}).then(customer => {
const amount = val.amount;
const idempotency_key = event.params.id;
let charge = {amount, currency, customer};
if (val.source !== null) charge.source = val.source;
return stripe.charges.create(charge, {idempotency_key});
}).then(response => {
return event.data.adminRef.set(response);
}, error => {
return event.data.adminRef.child('error').set(userFacingMessage(error)).then(() => {
return reportError(error, {user: event.params.userId});
});
}
);
});
Currently, the database is creating the stripe customers, but the charge doesn't go through, with error message "You have passed a blank string for 'customer'. You should remove the 'customer' parameter from your request or supply a non-blank value."
Update is I've been iterating on this. Tried the following:
firebase.database().ref(`/stripe_customers/${userId}/charges/`).push({
amount:amount,
email:email,
token:token,
customer: userId
});
&&
firebase.database().ref(`/stripe_customers/${userId}/charges/`).push({
amount:amount,
email:email,
token:token,
customer_id:{
customer: userId
}
});
And still getting "You have passed a blank string for 'customer'. You should remove the 'customer' parameter from your request or supply a non-blank value."