Update value in object in nested array in React Firestore - javascript

I need to update value field in object at specific index which is in array in Firebase
I tried to grab through getState() array with all objects;
then get object index which I need;
then assign new value to object, in this case content;
then rewrite whole array (comments) to newArray (actualComments) as you can see below.
And this works how I want, but only for the first time. If I try to do it again, I get error TypeError: "content" is read-only.
export const editComment = (comment) => {
return (dispatch, getState, { getFirebase, getFirestore }) => {
const firestore = getFirestore();
let actualComments = getState().firestore.data.topics[comment.topicId].comments;
let numberArray = actualComments.findIndex(e => {return e.id === comment.commentId});
actualComments[numberArray].content = comment.editContent;
firestore.collection('topics').doc(comment.topicId).update({
comments: actualComments
}).then(() => {
dispatch({ type: 'EDIT_COMMENT' })
}).catch((error) => {
dispatch({ type: 'EDIT_COMMENT_ERROR', error})
})
}
}

My friend helped me with this, and now my updates in object at specifix index in Array works! Here is code, cheers
/////Grab through reference all comments Array in firestore
let actualComments = getState().firestore.data.topics[comment.topicId].comments;
////make container for array
let updatedComments = [];
//// copy array from reference to empty updatedComments array
actualComments.forEach(comment => {
updatedComments.push({
content: comment.content,
createdAt: comment.createdAt,
editDate: comment.editDate,
edited: comment.edited,
id: comment.id,
idTopic: comment.idTopic,
name: comment.name,
nameId: comment.nameId
})
})
//// grab index which i want to update
let numberArray = actualComments.findIndex(e => {return e.id === comment.commentId});
//// update in specific index array
updatedComments[numberArray].content = comment.editContent;
updatedComments[numberArray].editDate = new Date();
updatedComments[numberArray].edited = true;
//// replace updated array in firestore
firestore.collection('topics').doc(comment.topicId).update({
comments: updatedComments
}).then...

Related

How can I merge two arrays from Axios into one "datasource" array and pass it as a data into syncfusion SeriesDirective component?

the main problem is that when I console.log "datasource" array it first shows that array populated the way I want to, which is:
[
0:{ date: date, value: value },
1:{ date: date, value: value },
...
]
but then after the first log loads it suddenly changes to a empty array [];
Here is my code:
import React, {useState, useEffect} from 'react'
import { ChartComponent, LineSeries, ColumnSeries, SeriesDirective, SeriesCollectionDirective, Inject } from '#syncfusion/ej2-react-charts'
import axios from 'axios'
const StablesTVLchart = () => {
const [stables, setStables] = useState([])
const dates = new Array;
const totalCirculating = new Array;
const totalPegged = new Array;
const datasource = new Array;
useEffect(() => {
axios.get('https://stablecoins.llama.fi/stablecoincharts/all?stablecoin=1')
.then(res => {
setStables(res.data)
// Populate dates array and totalCirculating array /
for (var i = 0; i < stables.length; i++){
dates.push(parseFloat(stables[i].date))
totalCirculating.push(data[i].totalCirculatingUSD)
}
// Populate totalPegged array /
for (var y = 0; y < totalCirculating.length; y++){
totalPegged.push(totalCirculating[y].peggedUSD)
}
// Populate datasource array with date and totalPegged /
for (var e = 0; e < datadate.length; e++){
datasource.push({ date: dates[e], value: totalPegged[e] })
}
})
.catch(err => {
console.log(err)
})
}, []);
console.log(datasource);
const primaryxAxis = {visible: false }
const primaryyAxis = { labelFormat: '${value}K', visible: false }
const palette = ["skyblue"]
return (
<div className="w-full">
<ChartComponent id="charts" primaryXAxis={primaryxAxis} primaryYAxis={primaryyAxis} palettes= {palette}>
<Inject services={[ColumnSeries, LineSeries]} />
<SeriesCollectionDirective>
<SeriesDirective dataSource={datasource} xName='date' yName='value' name='TVL'/>
</SeriesCollectionDirective>
</ChartComponent>
</div>
)
}
export default StablesTVLchart
I recon that its possible I need to somehow make this datasource array into a state array?
If anyone has any clues or an idea on how to do this, I'd be very grateful.
As it appears from the code you provided that you are using state to store the data, but immediately trying to use it, I will remind you that React may batch multiple setState() calls into a single update for performance.
Also for improvement of the code readability, you can use something like:
useEffect(() => {
axios.get('https://stablecoins.llama.fi/stablecoincharts/all?stablecoin=1')
.then(res => {
const stables = res.data;
const dates = stables.map(item => parseFloat(item.date));
const totalCirculating = stables.map(item => item.totalCirculatingUSD);
const totalPegged = totalCirculating.map(item => item.peggedUSD);
const datasource = totalPegged.map((value, index) => ({ date: dates[index], value }));
setStables(datasource);
})
.catch(err => {
console.log(err)
});
}, []);
The api returns a single array. You store the api's response in a state you don't use. You then store parts of the data of original response in 4 arrays by mutating them (using push). You try to use one of the mutated arrays (datasource) in the view, but as soon as the view is re-rendered (because of setStables(res.data)) the array is re-created with an empty array. In addition, updating a variable/constant asynchronously, and mutating the arrays doesn't cause a re-render, and even if it would react won't detect the change, and won't change the view.
Solution:
Use Array.map() to create a new array based on the api's response array
Store result of the map into the state, and use that state in your view
Example:
const StablesTVLchart = () => {
const [datasource, setDatasource] = useState([])
useEffect(() => {
axios.get('https://stablecoins.llama.fi/stablecoincharts/all?stablecoin=1')
.then(({ data }) => {
setDatasource(data.map(({ date, totalCirculatingUSD }) => ({
date: parseFloat(date),
value: totalCirculatingUSD.peggedUSD
})))
})
.catch(err => {
console.log(err)
})
}, []);
console.log(datasource);
}

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)})
))
})
));

How to get data from 2 collection in firebase at a time?(similar to aggregate lookup in MongoDB) [duplicate]

I have a Cloud Firestore DB with the following structure:
users
[uid]
name: "Test User"
posts
[id]
content: "Just some test post."
timestamp: (Dec. 22, 2017)
uid: [uid]
There is more data present in the actual DB, the above just illustrates the collection/document/field structure.
I have a view in my web app where I'm displaying posts and would like to display the name of the user who posted. I'm using the below query to fetch the posts:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
const postDocs = docSnaps.docs;
for (let i in postDocs) {
loadedPosts[postDocs[i].id] = postDocs[i].data();
}
});
// Render loadedPosts later
What I want to do is query the user object by the uid stored in the post's uid field, and add the user's name field into the corresponding loadedPosts object. If I was only loading one post at a time this would be no problem, just wait for the query to come back with an object and in the .then() function make another query to the user document, and so on.
However because I'm getting multiple post documents at once, I'm having a hard time figuring out how to map the correct user to the correct post after calling .get() on each post's user/[uid] document due to the asynchronous way they return.
Can anyone think of an elegant solution to this issue?
It seems fairly simple to me:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
db.collection('users').child(doc.data().uid).get().then((userDoc) => {
loadedPosts[doc.id].userName = userDoc.data().name;
});
})
});
If you want to prevent loading a user multiple times, you can cache the user data client side. In that case I'd recommend factoring the user-loading code into a helper function. But it'll be a variation of the above.
I would do 1 user doc call and the needed posts call.
let users = {} ;
let loadedPosts = {};
db.collection('users').get().then((results) => {
results.forEach((doc) => {
users[doc.id] = doc.data();
});
posts = db.collection('posts').orderBy('timestamp', 'desc').limit(3);
posts.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
loadedPosts[doc.id].userName = users[doc.data().uid].name;
});
});
After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.
Might not be an optimum solution but here its solve your problem.
combineLatest(
this.firestore.collection('Collection1').snapshotChanges(),
this.firestore.collection('Collection2').snapshotChanges(),
//In collection 2 we have document with reference id of collection 1
)
.pipe(
take(1),
).subscribe(
([dataFromCollection1, dataFromCollection2]) => {
this.dataofCollection1 = dataFromCollection1.map((data) => {
return {
id: data.payload.doc.id,
...data.payload.doc.data() as {},
}
as IdataFromCollection1;
});
this.dataofCollection2 = dataFromCollection2.map((data2) => {
return {
id: data2.payload.doc.id,
...data2.payload.doc.data() as {},
}
as IdataFromCollection2;
});
console.log(this.dataofCollection2, 'all feeess');
const mergeDataFromCollection =
this.dataofCollection1.map(itm => ({
payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
...itm
}))
console.log(mergeDataFromCollection, 'all data');
},
my solution as below.
Concept: You know user id you want to get information, in your posts list, you can request user document and save it as promise in your post item. after promise resolve then you get user information.
Note: i do not test below code, but it is simplify version of my code.
let posts: Observable<{}[]>; // you can display in HTML directly with | async tag
this.posts = this.listenPosts()
.map( posts => {
posts.forEach( post => {
post.promise = this.getUserDoc( post.uid )
.then( (doc: DocumentSnapshot) => {
post.userName = doc.data().name;
});
}); // end forEach
return posts;
});
// normally, i keep in provider
listenPosts(): Observable<any> {
let fsPath = 'posts';
return this.afDb.collection( fsPath ).valueChanges();
}
// to get the document according the user uid
getUserDoc( uid: string ): Promise<any> {
let fsPath = 'users/' + uid;
return this.afDb.doc( fsPath ).ref.get();
}
Note: afDb: AngularFirestore it is initialize in constructor (by angularFire lib)
If you want to join observables instead of promises, use combineLatest. Here is an example joining a user document to a post document:
getPosts(): Observable<Post[]> {
let data: any;
return this.afs.collection<Post>('posts').valueChanges().pipe(
switchMap((r: any[]) => {
data = r;
const docs = r.map(
(d: any) => this.afs.doc<any>(`users/${d.user}`).valueChanges()
);
return combineLatest(docs).pipe(
map((arr: any) => arr.reduce((acc: any, cur: any) => [acc].concat(cur)))
);
}),
map((d: any) => {
let i = 0;
return d.map(
(doc: any) => {
const t = { ...data[i], user: doc };
++i;
return t;
}
);
})
);
}
This example joins each document in a collection, but you could simplify this if you wanted to just join one single document to another.
This assumes your post document has a user variable with the userId of the document.
J

updating and existing object in firestore database?

In the following example I am updating 2 things. The first thing is an existing array in the db, and the second thing is an existing object. The array pushing is working as it should, but the object has an issue. I mean is there an equivalent for array union to object?
// Doc model current state before the incoming update.
data
{items :[]}
{lists : {}}
const addObjToClientsArr = async () => {
const docRef = doc(db, 'data', _authContext.currentUser.uid);
const payload = selected;
// Array
await updateDoc(docRef, { items: arrayUnion(payload) });
// Obeject
const payload2 = {[context.id]: [
{ a : 1 },
]}
updateDoc(docRef, {
lists: {
payload2
});
};
It sounds like you want to update a field inside a nested object, which you can do with:
const path = `lists.${context.id}.a`;
await updateDoc(docRef, {
[path]: 1
})

Cloud Firestore: Query two collection [duplicate]

I have a Cloud Firestore DB with the following structure:
users
[uid]
name: "Test User"
posts
[id]
content: "Just some test post."
timestamp: (Dec. 22, 2017)
uid: [uid]
There is more data present in the actual DB, the above just illustrates the collection/document/field structure.
I have a view in my web app where I'm displaying posts and would like to display the name of the user who posted. I'm using the below query to fetch the posts:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
const postDocs = docSnaps.docs;
for (let i in postDocs) {
loadedPosts[postDocs[i].id] = postDocs[i].data();
}
});
// Render loadedPosts later
What I want to do is query the user object by the uid stored in the post's uid field, and add the user's name field into the corresponding loadedPosts object. If I was only loading one post at a time this would be no problem, just wait for the query to come back with an object and in the .then() function make another query to the user document, and so on.
However because I'm getting multiple post documents at once, I'm having a hard time figuring out how to map the correct user to the correct post after calling .get() on each post's user/[uid] document due to the asynchronous way they return.
Can anyone think of an elegant solution to this issue?
It seems fairly simple to me:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
db.collection('users').child(doc.data().uid).get().then((userDoc) => {
loadedPosts[doc.id].userName = userDoc.data().name;
});
})
});
If you want to prevent loading a user multiple times, you can cache the user data client side. In that case I'd recommend factoring the user-loading code into a helper function. But it'll be a variation of the above.
I would do 1 user doc call and the needed posts call.
let users = {} ;
let loadedPosts = {};
db.collection('users').get().then((results) => {
results.forEach((doc) => {
users[doc.id] = doc.data();
});
posts = db.collection('posts').orderBy('timestamp', 'desc').limit(3);
posts.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
loadedPosts[doc.id].userName = users[doc.data().uid].name;
});
});
After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.
Might not be an optimum solution but here its solve your problem.
combineLatest(
this.firestore.collection('Collection1').snapshotChanges(),
this.firestore.collection('Collection2').snapshotChanges(),
//In collection 2 we have document with reference id of collection 1
)
.pipe(
take(1),
).subscribe(
([dataFromCollection1, dataFromCollection2]) => {
this.dataofCollection1 = dataFromCollection1.map((data) => {
return {
id: data.payload.doc.id,
...data.payload.doc.data() as {},
}
as IdataFromCollection1;
});
this.dataofCollection2 = dataFromCollection2.map((data2) => {
return {
id: data2.payload.doc.id,
...data2.payload.doc.data() as {},
}
as IdataFromCollection2;
});
console.log(this.dataofCollection2, 'all feeess');
const mergeDataFromCollection =
this.dataofCollection1.map(itm => ({
payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
...itm
}))
console.log(mergeDataFromCollection, 'all data');
},
my solution as below.
Concept: You know user id you want to get information, in your posts list, you can request user document and save it as promise in your post item. after promise resolve then you get user information.
Note: i do not test below code, but it is simplify version of my code.
let posts: Observable<{}[]>; // you can display in HTML directly with | async tag
this.posts = this.listenPosts()
.map( posts => {
posts.forEach( post => {
post.promise = this.getUserDoc( post.uid )
.then( (doc: DocumentSnapshot) => {
post.userName = doc.data().name;
});
}); // end forEach
return posts;
});
// normally, i keep in provider
listenPosts(): Observable<any> {
let fsPath = 'posts';
return this.afDb.collection( fsPath ).valueChanges();
}
// to get the document according the user uid
getUserDoc( uid: string ): Promise<any> {
let fsPath = 'users/' + uid;
return this.afDb.doc( fsPath ).ref.get();
}
Note: afDb: AngularFirestore it is initialize in constructor (by angularFire lib)
If you want to join observables instead of promises, use combineLatest. Here is an example joining a user document to a post document:
getPosts(): Observable<Post[]> {
let data: any;
return this.afs.collection<Post>('posts').valueChanges().pipe(
switchMap((r: any[]) => {
data = r;
const docs = r.map(
(d: any) => this.afs.doc<any>(`users/${d.user}`).valueChanges()
);
return combineLatest(docs).pipe(
map((arr: any) => arr.reduce((acc: any, cur: any) => [acc].concat(cur)))
);
}),
map((d: any) => {
let i = 0;
return d.map(
(doc: any) => {
const t = { ...data[i], user: doc };
++i;
return t;
}
);
})
);
}
This example joins each document in a collection, but you could simplify this if you wanted to just join one single document to another.
This assumes your post document has a user variable with the userId of the document.
J

Categories

Resources