react firestore sub collection - reactjs

How can I get a list of cars for a customer
clients:
w21rffa3:
name: Johny
phone: 123123
cars:
fn1jnr12:
brand: AUDi
model: a6
number: 24f1
dsdasgf122:
brand: AUDi
model: a3
number: 62s14
My code
const ref = firestore().collection('clients');
const [clientsList, setClientsList] = useState([]);
useEffect(() => {
return ref.onSnapshot(clientsSnapshot => {
const clients = [];
const cars = [];
clientsSnapshot.forEach(client => {
const carsRef = ref.doc(client.id).collection('cars').onSnapshot(carsSnapshot => {
carsSnapshot.forEach(car => {
if (car.data().brand.length > 0) {
const {
brand,
model,
number
} = car.data();
cars.push({
id: car.id,
brand,
model,
number,
});
}
});
//Good result`
console.log('After forEach: ', cars);
});
//Bad result
console.log('After snapshot: ', cars);
const {
name,
phone
} = client.data();
clients.push({
id: client.id,
name,
phone,
cars: cars,
});
});
setClientsList(clients);
});
}, []);
cars list for customers

The error you facing is due to misuse/misunderstanding of how async/callback-based functions works. As I said in my comment - good result and bad result - bad result scripts are executed before good result due to onSnapshot is async, and you pass a callback function to it, which will be executed when data is available from firebase, so a bit "later" than the rest of the code.
Now about what can be done. The code is a bit tricky and I didnt really test it, so if anything - please, let me know.
const [clientsList, setClientsList] = useState([]);
useEffect(() => {
let carsUnsubscribeFns = [];
const clientUnsubscribeFn = ref.onSnapshot((clientsSnapshot) => {
// Reset everything and stop previously created listeners for Cars
setClientsList([]);
carsUnsubscribeFns.forEach((x) => x());
carsUnsubscribeFns = [];
clientsSnapshot.forEach((c) => {
const { name, phone } = c.data();
const client = { id: c.id, name, phone };
// In case you dont want to use optional chaining,
// remove the const client = ... line above
// and uncomment the line below
// but optional chaining is prefered anyway
// const client = { id: c.id, name, phone, cars: [] };
const carsUnsubscribeFn = ref
.doc(client.id)
.collection("cars")
.onSnapshot((carsSnapshot) => {
// Mutate the Client object directly
client.cars = carsSnapshot.docs
.map((x) => ({ id: x.id, ...x.data() }))
.filter((x) => x.brand?.length > 0);
// mark ClientsList as updated to force a rerender
// due to we mutated one of the entities inside
setClientsList((curr) => [...curr]);
});
carsUnsubscribeFns.push(carsUnsubscribeFn);
setClientsList((curr) => {
curr.push(client);
return [...curr];
});
});
// clean-up function returned from hook to stop all the listeners
return () => {
[clientUnsubscribeFn, ...carsUnsubscribeFns].forEach((x) => x());
};
});
}, []);

Related

Update an array relation belongs to many with Strapi controller

I use Strapi V4. I have a link collection and I want to update likes.
How update the relation array ? When I put new data old value are replace by the new one.
Example :
likes : [1]
if I update another time
likes:[2].
BUT I want this likes : [1,2]
I try this but It d'oesn't work. Thans for your replay
'use strict';
/**
* link controller
*/
const { createCoreController } = require('#strapi/strapi').factories;
module.exports = createCoreController('api::link.link', ({ strapi }) => ({
// Method 2: Wrapping a core action (leaves core logic in place)
async find(ctx) {
const { data, meta } = await super.find(ctx);
const linkId = data.map((link) => link.id);
const allPosts = await strapi.entityService.findMany('api::link.link', {
fields: ["id"],
filters: { id: { $in: linkId } },
populate: {
likes: { count: true },
},
});
data.forEach(link => {
link.likes = allPosts.find(({ id }) => id === link.id)?.likes?.count || 0;
});
//update value with new array => need to be fix
await strapi.entityService.update("api::link.link", {
likes: [...allPosts.likes.map(({ id }) => id), ...likes],
});
return { data, meta };
},
}));
This part need to be fix. Can you help me ? Thanks
//update value with new array => need to be fix
await strapi.entityService.update("api::link.link", {
likes: [...allPosts.likes.map(({ id }) => id), ...likes],
});

React Firestore - Retrieve all documents in an Array of document IDs

I am trying to retrieve an all documents of which document ID is within the given array. I have no issues retrieving the list of documentIDs and storing in the array. However, I am not sure on how to retrieve the documents of which ids are in the array. Please help Thanks!
portfolio doc:
docId{
docId: docId,
...
}
const getData = (bookmarkIds) => {
console.log("this is " )
console.log(bookmarkIds)
console.log("entering" )
const portfolioQuery = database.portfolioRef.where("docId", 'in', bookmarkIds);
portfolioQuery.onSnapshot((snapshot) => {
console.log(snapshot.docs)
if (snapshot.docs.length !== 0) {
var id = 1;
const tempItem = [];
snapshot.docs.forEach((doc) => {
tempItem.push({
id: id,
intervieweeName: doc.data().intervieweeName,
intervieweeEmail: doc.data().intervieweeEmail,
projectTitle: doc.data().projectTitle,
portfolioTitle: doc.data().portfolioTitle,
dateCreated: doc.data().dateCreated,
fbId: doc.id
})
id++;
})
setPortfolioData(tempItem)
}
})
}
useEffect(() => {
const getUserData = database.usersRef.where("email", "==", currentUser.email);
const bookmarkArray = [];
const unsub = getUserData.onSnapshot((snapshot) => {
snapshot.docs.forEach((doc) =>{
bookmarkArray.push(doc.data().bookmarkIds);
})
console.log(bookmarkArray);
getData(bookmarkArray)
})
return unsub;
}, [currentUser.email]);
Based on my current code above, I am receiving the following (Not getting any error just a blank return):
I realised I made a double array by doing
bookmarkArray.push(doc.data().bookmarkIds);
solved by doing
bookmarkArray = doc.data().bookmarkIds;

Getting Array to string conversion while using React with Cart

I use react-use-cart package in react.
Here is my function is:
const { items, cartTotal } = useCart();
const lists = items.map(item => item.id);
function handleSave(event) {
event.preventDefault();
const order = {
user_id: customer.id,
order_date: date,
amount: cartTotal,
product_list: items
};
saveOrder(order)
.then(() => {window.location.href = `/account/orders`})
.catch(error => {});
}
Items added to cart are in the following format:
items: [
{id:9,quantity:2,...},
{id:24,quantity:1,...},
]
How to assign this items to a const order into product_list?. When I put only items prints the message: "Array to string conversion". Maybe I'm doing something the wrong way.

How to get all elements from an atomFamily in recoil?

Im playing around with recoil for the first time and cant figure out how I can read all elements from an atomFamily. Let's say I have an app where a user can add meals:
export const meals = atomFamily({
key: "meals",
default: {}
});
And I can initialize a meal as follows:
const [meal, setMeal] = useRecoilState(meals("bananas"));
const bananas = setMeal({name: "bananas", price: 5});
How can I read all items which have been added to this atomFamily?
You have to track all ids of the atomFamily to get all members of the family.
Keep in mind that this is not really a list, more like a map.
Something like this should get you going.
// atomFamily
const meals = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
When creating a new objects inside the family you also have to update the mealIds atom.
I usually use a useRecoilCallback hook to sync this together
const createMeal = useRecoilCallback(({ set }) => (mealId, price) => {
set(mealIds, currVal => [...currVal, mealId]);
set(meals(mealId), {name: mealId, price});
}, []);
This way you can create a meal by calling:
createMeal("bananas", 5);
And get all ids via:
const ids = useRecoilValue(mealIds);
Instead of using useRecoilCallback you can abstract it with selectorFamily.
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set}, meal) => {
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
Further more, in case you would like to support reset you can use the following code:
// atomFamily
const mealsAtom = atomFamily({
key: "meals",
default: {}
});
const mealIds = atom({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if(meal instanceof DefaultValue) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
If you're using Typescript you can make it more elegant by using the following guard.
import { DefaultValue } from 'recoil';
export const guardRecoilDefaultValue = (
candidate: unknown
): candidate is DefaultValue => {
if (candidate instanceof DefaultValue) return true;
return false;
};
Using this guard with Typescript will look something like:
// atomFamily
const mealsAtom = atomFamily<IMeal, number>({
key: "meals",
default: {}
});
const mealIds = atom<number[]>({
key: "mealsIds",
default: []
});
// abstraction
const meals = selectorFamily<IMeal, number>({
key: "meals-access",
get: (id) => ({ get }) => {
const atom = get(mealsAtom(id));
return atom;
},
set: (id) => ({set, reset}, meal) => {
if (guardRecoilDefaultValue(meal)) {
// DefaultValue means reset context
reset(mealsAtom(id));
reset(mealIds (id));
return;
}
// from this line you got IMeal (not IMeal | DefaultValue)
set(mealsAtom(id), meal);
set(mealIds (id), prev => [...prev, meal.id)]);
}
});
You can use an atom to track the ids of each atom in the atomFamily. Then use a selectorFamily or a custom function to update the atom with the list of ids when a new atom is added or deleted from the atomFamily. Then, the atom with the list of ids can be used to extract each of the atoms by their id from the selectorFamily.
// File for managing state
//Atom Family
export const mealsAtom = atomFamily({
key: "meals",
default: {},
});
//Atom ids list
export const mealsIds = atom({
key: "mealsIds",
default: [],
});
This is how the selectorFamily looks like:
// File for managing state
export const mealsSelector = selectorFamily({
key: "mealsSelector",
get: (mealId) => ({get}) => {
return get(meals(mealId));
},
set: (mealId) => ({set, reset}, newMeal) => {
// if 'newMeal' is an instance of Default value,
// the 'set' method will delete the atom from the atomFamily.
if (newMeal instanceof DefaultValue) {
// reset method deletes the atom from atomFamily. Then update ids list.
reset(mealsAtom(mealId));
set(mealsIds, (prevValue) => prevValue.filter((id) => id !== mealId));
} else {
// creates the atom and update the ids list
set(mealsAtom(mealId), newMeal);
set(mealsIds, (prev) => [...prev, mealId]);
}
},
});
Now, how do you use all this?
Create a meal:
In this case i'm using current timestamp as the atom id with Math.random()
// Component to consume state
import {mealsSelector} from "your/path";
import {useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(Math.random()));
setMeal({
name: "banana",
price: 5,
});
Delete a meal:
// Component to consume state
import {mealsSelector} from "your/path";
import {DefaultValue, useSetRecoilState} from "recoil";
const setMeal = useSetRecoilState(mealsSelector(mealId));
setMeal(new DefaultValue());
Get all atoms from atomFamily:
Loop the list of ids and render Meals components that receive the id as props and use it to get the state for each atom.
// Component to consume state, parent of Meals component
import {mealsIds} from "your/path";
import {useRecoilValue} from "recoil";
const mealIdsList = useRecoilValue(mealsIds);
//Inside the return function:
return(
{mealIdsList.slice()
.map((mealId) => (
<MealComponent
key={mealId}
id={mealId}
/>
))}
);
// Meal component to consume state
import {mealsSelector} from "your/path";
import {useRecoilValue} from "recoil";
const meal = useRecoilValue(mealsSelector(props.id));
Then, you have a list of components for Meals, each with their own state from the atomFamily.
Here is how I have it working on my current project:
(For context this is a dynamic form created from an array of field option objects. The form values are submitted via a graphql mutation so we only want the minimal set of changes made. The form is therefore built up as the user edits fields)
import { atom, atomFamily, DefaultValue, selectorFamily } from 'recoil';
type PossibleFormValue = string | null | undefined;
export const fieldStateAtom = atomFamily<PossibleFormValue, string>({
key: 'fieldState',
default: undefined,
});
export const fieldIdsAtom = atom<string[]>({
key: 'fieldIds',
default: [],
});
export const fieldStateSelector = selectorFamily<PossibleFormValue, string>({
key: 'fieldStateSelector',
get: (fieldId) => ({ get }) => get(fieldStateAtom(fieldId)),
set: (fieldId) => ({ set, get }, fieldValue) => {
set(fieldStateAtom(fieldId), fieldValue);
const fieldIds = get(fieldIdsAtom);
if (!fieldIds.includes(fieldId)) {
set(fieldIdsAtom, (prev) => [...prev, fieldId]);
}
},
});
export const formStateSelector = selectorFamily<
Record<string, PossibleFormValue>,
string[]
>({
key: 'formStateSelector',
get: (fieldIds) => ({ get }) => {
return fieldIds.reduce<Record<string, PossibleFormValue>>(
(result, fieldId) => {
const fieldValue = get(fieldStateAtom(fieldId));
return {
...result,
[fieldId]: fieldValue,
};
},
{},
);
},
set: (fieldIds) => ({ get, set, reset }, newValue) => {
if (newValue instanceof DefaultValue) {
reset(fieldIdsAtom);
const fieldIds = get(fieldIdsAtom);
fieldIds.forEach((fieldId) => reset(fieldStateAtom(fieldId)));
} else {
set(fieldIdsAtom, Object.keys(newValue));
fieldIds.forEach((fieldId) => {
set(fieldStateAtom(fieldId), newValue[fieldId]);
});
}
},
});
The atoms are selectors are used in 3 places in the app:
In the field component:
...
const localValue = useRecoilValue(fieldStateAtom(fieldId));
const setFieldValue = useSetRecoilState(fieldStateSelector(fieldId));
...
In the save-handling component (although this could be simpler in a form with an explicit submit button):
...
const fieldIds = useRecoilValue(fieldIdsAtom);
const formState = useRecoilValue(formStateSelector(fieldIds));
...
And in another component that handles form actions, including form reset:
...
const resetFormState = useResetRecoilState(formStateSelector([]));
...
const handleDiscard = React.useCallback(() => {
...
resetFormState();
...
}, [..., resetFormState]);

I get undefined from state

I have problem, in the code below I'm trying to console.log only name of the playLists from state with console.log(this.state.playLists.name); I get undefined.
getPlaylist() {
spotifyApi.getUserPlaylists().then((response) => {
if (response.items) {
const items = response.items;
console.log(items);
const playListsId = items.map((obj) => {
const playList = {
name: obj.name,
id: obj.id,
};
return playList;
});
console.log(playListsId);
this.setState({
playLists: playListsId,
});
}
console.log(this.state.playLists.name);
});
}
Since this.state.playLists is an array you cannot get name on it. you have to get the name of all elements in an array.
Try this in place of console.log(this.state.playLists.name);
this.state.platLists.map(playList => console.log(playList.name));

Resources