Searching through object in React using another object - javascript

Hi I have something like this that works great:
const handleFavour = () => {
const { developers } = state;
const results = developers.filter(list =>
list.uid.includes("authid")
);
setSearchResults(results);
console.log(results);
};
But now insead of "authid" if i wanted to search using another object or array that has many authids, how do I do this search? Couldn't find anything after searching for a very long time.

If you want to check if any element in an array matches any element in another array you could combine your current solution with .some.
const handleFavour = () => {
const allowedIds = ['id1', 'id2'];
const { developers } = state;
const results = developers.filter(list =>
list.uid.some(uid => allowedIds.includes(uid))
);
setSearchResults(results);
console.log(results);
};
Here .some will return true if any of the elements in list.uid are also present in allowedIds.
Based on your situation as described in the comments it looks like you want something like this:
const handleFavour = () => {
const users = [{ auth: 'id1' }, { auth: 'id2' }];
const { developers } = state;
const results = developers.filter(list =>
users.some(user => user.auth === list.uid)
);
setSearchResults(results);
console.log(results);
};
I initially thought list.uid was an array since you were using .includes, generally I'd recommend using the strict equality operator (===) to compare strings unless you actually want to allow partial matches, but I'm guessing in this case you don't.

Related

How to combine two object lists into one list by id in rxjs using observables?

I have two service calls that return with an observable list:
service.getAllUsers(): observable<User[]> where User looks like this:
User {
id: number,
username: string,
...
}
and the other service call:
service.getSomeData() : Observable<Data[]>
Data {
userId: number,
userSomeData1,
userSomeData2,
...
}
I want to merge these two lists into one Observable list, based on userId:
Observable<Result[]>
where Result should looks like this:
Result {
id,
username,
userSomeData1,
userSomeData2,
...
}
What I have done so far:
const result$ = this.userService.getAllUsers()
.pipe(switchMap(users => {
return this.userService.getSomeData()
.pipe(map(data => ({users, data})))
}))
But its not what I want.
Please help me, how can I achive this?
This is my solution, make the two api calls, run a map on the array, find the data for the respective user and finally merge using array destructuring!
const result$ = forkJoin([
this.userService.getAllUsers(),
this.userService.getSomeData(),
]).pipe(
map(([users, data]) => {
return users.map(user => {
let clonedData = user;
const foundData = data.find(x => x.userId === user.id);
if (foundData) {
clonedData = { ...clonedData, ...foundData };
}
return JSON.parse(JSON.stringify(clonedData));
});
})
);
Your code was very close, you just need to .map() each element in your users array to a new object with the corresponding data item appended:
const result$ = this.userService.getAllUsers().pipe(
switchMap(users => this.userService.getSomeData().pipe(
map(data => users.map(
u => ({...u, ...data.find(d => d.userId === u.id)})
))
})
));

useState array becomes 2 arrays in console.log?

Im trying to just use a empty array in a react/ts project like this.
const [companyChatrooms, setCompanyChatrooms]: any = useState([]);
I then use a useEffect to get it done when rendering component.
async function fetchMyChatRooms() {
const userCollection = await firestore.collection('user_in_chat')
const snapshot = await userCollection.where('user_id', '==', myIdNumber).where('chatroom_id', '==', companyChatrooms).get();
snapshot.forEach(doc => {
const roomID = doc.data().chatroom_id
setMyChatrooms([...myChatrooms, roomID])
});
}
fetchMyChatRooms()
}, [companyChatrooms, myIdNumber])
console.log(myChatrooms)```
However, my console.log shows 2 arrays with each value instead of 1 array holding both values.
How can i make sure both values are stored in same array?
[1]: https://i.stack.imgur.com/q0WPD.png <-- Check how the output looks.
I assume you have an array snapshot with more than 1 element and any iteration you are updating the state. This caused multiple re-render
I suggest you to update state after iterate entire array. Example:
const rooms = []
snapshot.forEach(doc => {
const roomID = doc.data().chatroom_id;
rooms.push(roomID);
});
setMyChatrooms(rooms)
you should set all of them in one time.
async function fetchMyChatRooms() {
const userCollection = await firestore.collection('user_in_chat')
const snapshot = await userCollection.where('user_id', '==', myIdNumber).where('chatroom_id', '==', companyChatrooms).get();
// here is the changing
const roomIDs = snapshot.map(doc => doc.data().chatroom_id);
setMyChatrooms(roomIDs )
//
fetchMyChatRooms()
}, [companyChatrooms, myIdNumber])
console.log(myChatrooms)

How to put a dynamic data from firestore in the function where() and also use the snap.size to count the total query to be passed in a graph?

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

Matching properties of objects in two different arrays

Background
I'm building a "Liked Stores" screen for an app I'm developing. This screen renders a flatlist which shows cards with each store a user has liked. The way I'm handling the process is that when a user likes a store, it creates a new doc in firestore which holds the id of the store and a "liked" boolean.
Problem
I've been trying to filter the totality of stores in my db and match the id's of the liked stores with the id's in the "liked" collection documents.
The data structure is:
user/uid/favorites, each doc here contains 2 fields. An id (matching to another id in the stores/ collection) and a boolean indicating that it has been liked. And then stores/'s docs contain lots of data but what matters here is a field which holds an id.
I've tried implementing the following solution, with two subscribers bringing each collection and a function which handles the filtering. But for some reason I don't quite understand I just can't get it to work.
Code is as follows:
const [stores, setStores] = useState([]);
useEffect(() => {
const subscriber = firebase.firestore()
.collection('stores/')
.onSnapshot(querySnapshot => {
const stores = [];
querySnapshot.forEach(documentSnapshot => {
stores.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setStores(stores);
});
return () => subscriber();
}, [])
const [liked, setLiked] = useState([]);
useEffect(() => {
const subscriber = firebase.firestore()
.collection('users/' + userId + '/favorites')
.onSnapshot(querySnapshot => {
const liked = [];
querySnapshot.forEach(documentSnapshot => {
liked.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setLiked(liked);
});
return () => subscriber();
}, [])
const usefulData = [];
const handleFilter = (obj) => {
for(let i = 0; i < stores.length; i++){
if(obj.id === liked[i].storeId) {
usefulData.push(obj)
}
}
}
stores.filter(handleFilter);
And then I'm passing usefulData as the data param to the FlatList.
When printing usefulData it simply returns an empty array.
Question
Can anyone provide a solution and an explanation as to why this is not working and how to compare two arrays of objects' values properly?
Well, figured it out in the most uncomfortable and least elegant possible way so any tips or better ideas will still be fondly accepted:
const likedIds = [];
liked.forEach(o => likedIds.push(o.id));
const storeIds = [];
stores.forEach(o => storeIds.push(o.id));
const intersection = storeIds.filter(element => likedIds.includes(element));
const [data, setData] = useState([]);
const finalArray = [];
useEffect(() => {
for (let i = 0; i < intersection.length; i++){
const fetchQuery = async () => {
const storeData = await firebase.firestore()
.collection('stores/')
.doc(intersection[i])
.get()
.then(documentSnapshot => {
if (documentSnapshot.exists) {
finalArray.push(documentSnapshot.data())
}
});
setData(finalArray)
}
fetchQuery()
}
}, [])
Without changing anything else from what is posed in the question, this is what I added in order to get this working. First I get all the Id's out of each array of objects, then filter them and store them in a new array. Then we can go ahead and build a new array of objects using a handy fetchQuery async function to get all the data from the original stores collection, just that this time it only brings up the stores with ids in the intersection array.
I am perfectly aware that this is the most bodged out and disgusting possible solution, but at the very least it works. Hope someone smarter than me can get some help out of this and make my solution much better!

Loop from multiple airtable bases in a Next JS page

I think this more of a general async/await loop question, but I'm trying to do it within the bounds of an Airtable API request and within getStaticProps of Next.js so I thought that is important to share.
What I want to do is create an array of base IDs like ["appBaseId01", "appBaseId02", "appBaseId03"] and output the contents of a page. I have it working with 1 base, but am failing at getting it for multiple.
Below is the code for one static base, if anyone can help me grok how I'd want to loop over these. My gut says that I need to await each uniquely and then pop them into an array, but I'm not sure.
const records = await airtable
.base("appBaseId01")("Case Overview Information")
.select()
.firstPage();
const details = records.map((detail) => {
return {
city: detail.get("City") || null,
name: detail.get("Name") || null,
state: detail.get("State") || null,
};
});
return {
props: {
details,
},
};
EDIT
I've gotten closer to emulating it, but haven't figured out how to loop the initial requests yet.
This yields me an array of arrays that I can at least work with, but it's janky and unsustainable.
export async function getStaticProps() {
const caseOneRecords = await setOverviewBase("appBaseId01")
.select({})
.firstPage();
const caseTwoRecords = await setOverviewBase("appBaseId02")
.select({})
.firstPage();
const cases = [];
cases.push(minifyOverviewRecords(caseOneRecords));
cases.push(minifyOverviewRecords(caseTwoRecords));
return {
props: {
cases,
},
};
}
setOverviewBase is a helper that establishes the Airtable connection and sets the table name.
const setOverviewBase = (baseId) =>
base.base(baseId)("Case Overview Information");
You can map the array of base IDs and await with Promise.all. Assuming you have getFirstPage and minifyOverviewRecords defined as below, you could do the following:
const getFirstPage = (baseId) =>
airtable
.base(baseId)("Case Overview Information")
.select({})
.firstPage();
const minifyOverviewRecords = (records) =>
records.map((detail) => {
return {
city: detail.get("City") || null,
name: detail.get("Name") || null,
state: detail.get("State") || null,
};
});
export async function getStaticProps() {
const cases = await Promise.all(
["appBaseId01", "appBaseId02", "appBaseId03"].map(async (baseId) => {
const firstPage = await getFirstPage(baseId);
return minifyOverviewRecords(firstPage);
})
);
return {
props: {
cases
}
};
}

Categories

Resources