JS Firestore 'load more' pagination ignoring startAfter() and loading old data - javascript

I'm implementing a pagination where the user clicks a button to load more items.
I'm following the docs at https://firebase.google.com/docs/firestore/query-data/query-cursors (paginate a query)
I'm passing in the last item id to the startAfter() function. This should make the next query return items that comes after the passed in item id.
What actually is happening, is that the previous items get loaded again.
So if the initial fetches looks like:
[
0: {id: "1", stuff: ...}
1: {id: "2", stuff: ...}
2: {id: "3", stuff: ...}
]
the id 3 will get passed into startAfter(), and should load
[
0: {id: "4", stuff: ...}
1: {id: "5", stuff: ...}
]
instead, it loads the initial items
[
0: {id: "1", stuff: ...}
1: {id: "2", stuff: ...}
]
So I end up displaying duplicate items when I press my 'load more' button.
initial fetch function:
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const fetchData = async () => {
const obj = [];
const firstFetch = await db
.collection('recipes')
.orderBy('createdAt', 'desc')
.limit(3)
.get();
const snapshot = await firstFetch;
snapshot.forEach((doc) => {
obj.push({ id: doc.id, ...doc.data() });
});
const allRecipes = {
userRecipes: obj,
};
setRecipes(allRecipes.userRecipes);
};
fetchData();
}, []);
The recipe variable logs:
[
0: {id: "1", stuff: ...}
1: {id: "2", stuff: ...}
2: {id: "3", stuff: ...}
]
The button to load more items:
<button
type="button"
onClick={() => showMore({ recipe: recipes[recipes.length - 1] })}
>
Show More
</button>
Which calls the showMore function:
const showMore = async ({ recipe }) => {
const data = await db
.collection('recipes')
.orderBy('createdAt', 'desc')
.startAfter(recipe)
.limit(2)
.get();
const moreData = data.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
setRecipes([...recipes, ...moreData]);
};
If I log the 'moreData' variable:
[
0: {id: "1", stuff: ...}
1: {id: "2", stuff: ...}
]
if I log the updated 'recipe' variable:
[
0: {id: "1", stuff: ...}
1: {id: "2", stuff: ...}
2: {id: "3", stuff: ...}
0: {id: "1", stuff: ...}
1: {id: "2", stuff: ...}
]
So to me, it seems like it's ignoring the startAfter() function, and loading everything from the start instead.
Any pointers on what am I doing wrong?
Edit:
The actual data in the db looks like this:
{
about: "Non magnam quasi tempore."
createdAt: t {seconds: 1596741068, nanoseconds: 531000000}
description: "Ea quibusdam odit quasi corporis et nostrum."
imageUrl: "http://lorempixel.com/640/480/food"
ingredients: [{…}]
likes: []
title: "Ella"
userId: "U8n09jb0ZENwrZhDeYrJUSikcM12"
userSaves: ["uM7A4r5O6ENiX3qTL4tGhDXYA3Z2"]
username: "foobar"
}
the ID is the name of the document. The ID gets appended to the document in the initial fetch function.
SOLUTION:
Looking at Frank van Puffelen's answer, I had to provide the complete snapshot (which includes all info about the item), not only the array with items in it.
By creating a new hook with useState, I could store the state of the last snapshot in it:
const last = snapshot.docs[snapshot.docs.length - 1]
setLastDocSnap(last)
in my fetchMore() function, I was able to se the state in the startAfter() function:
const data = await db
.collection('recipes')
.orderBy('createdAt')
.limit(limit)
.startAfter(lastDocSnap)
.get();

I am not able to reproduce the problem. I've simplified your code to:
ref.orderBy('id')
.limit(3)
.get()
.then(function(querySnapshot) {
console.log("Initial query: "+querySnapshot.docs.length+" results");
querySnapshot.forEach(function(doc) {
console.log(doc.data().id);
})
var last = querySnapshot.docs[querySnapshot.docs.length - 1];
ref.orderBy('id')
.startAfter(last)
.limit(3)
.get()
.then(function(querySnapshot) {
console.log("Second query: "+querySnapshot.docs.length+" results");
querySnapshot.forEach(function(doc) {
console.log(doc.data().id);
})
});
});
And then run it on a collection with 5 documents, with id values 1 through 5. It's output is:
Initial query: 3 results
1
2
3
Second query: 2 results
4
5
Which is as I'd expect it to be.
You can see the running code here: https://jsbin.com/qusijaw/edit?js,console
If my results don't help you get it working on your end, can you try to reproduce the problem in an environment like jsbin, so that we can take a look at it?

Related

Sequelize: Special methods don't update the database? - Problem updating instance

I've spent a decent chunk of the day confused about the behaviour of one of my methods that should create an instance of a Model, Company, that has a Contact.
I feel like it should be simple, but it's just not outputting the contact field as I'd expect, even though it is saving it to the database correctly:
await sequelize.transaction(async (t) => {
const company = await Company.create({ name: req.body.companyName }, { transaction: t });
const contact = await Contact.create({position: req.body.position}, { transaction: t });
await company.addContact(contact, {transaction: t}); // This method does exist
await company.save(); // Thought this might help in case `addContact` didn't update the db
console.log(company.dataValues); // {id:5, name: 'test'}
console.log(contact.dataValues); // {id: 7, position: 'Legend'}
const test = await Company.findOne({where: { name: req.body.companyName }, transaction: t});
console.log(test.dataValues); // {id: 5, name: 'test'}, no contact property
console.log(company.contacts); // is this not posssible instead?
//...
I was expecting this as the output:
{
id: 5,
name: 'test',
contacts: [{ id: 7, position: 'legend' }]
}
Really can't understand why it's not working. Any help would be really appreciated at this point!
** Associations:
Company.hasMany(Contact);
Contact.belongsTo(Company);

JavaScript Get Indexes based from the Selected Array of Object Id

Today, I'm trying to get the list of javascript index based from the selected data id that I have.
I'm following this guide from https://buefy.org/documentation/table/#checkable where it needs something like this: checkedRows: [data[1], data[3]] to able to check the specific row in the table.
What I need to do is to check the table based from my web API response.
I have this sample response data.
response.data.checkedRows // value is [{id: 1234}, {id: 83412}]
and I have the sample data from the table.
const data = [{
id: 1234,
name: 'Jojo'
},{
id: 43221,
name: 'Jeff'
},{
id: 83412,
name: 'Kacey'
}]
So basically, I need to have something, dynamically, like this: checkedRows: [data[0], data[2]] because it matches the data from the response.data.checkedRows
So far, I tried using forEach
let selectedIndex = [];
response.data.checkedRows.forEach((d) => {
this.data.forEach((e) => {
if (d.id=== e.id) {
// need the result to be dynamic depending on the response.data.checkedRows
this.checkedRows = [data[0], data[2]]
}
});
});
I'm stuck here because I'm not sure how can I get the index that matches the selected checkedRows from response.
Any help?
Map the response checkedRows, and in the callback, .find the matching object in the array:
const checkedRows = [{id: 1234}, {id: 83412}];
const data = [{
id: 1234,
name: 'Jojo'
},{
id: 43221,
name: 'Jeff'
},{
id: 83412,
name: 'Kacey'
}];
const objs = checkedRows.map(({ id }) => (
data.find(obj => obj.id === id)
));
console.log(objs);
If there are a lot of elements, you can use a Set of the IDs to find instead to decrease the computational complexity:
const checkedRows = [{id: 1234}, {id: 83412}];
const data = [{
id: 1234,
name: 'Jojo'
},{
id: 43221,
name: 'Jeff'
},{
id: 83412,
name: 'Kacey'
}];
const ids = new Set(checkedRows.map(({ id }) => id));
const objs = data.filter(obj => ids.has(obj.id));
console.log(objs);

TypeError: Cannot read property 'map' of undefined but state has been stored

I am trying to get a specific team data from the database and store it in a state. But when I map an array inside that data it returns an error. When I console log my state it returns the data below
createdAt: "2021-03-19T13:36:22.868Z"
gameEvent: "basketball"
players: Array(5)
0: {_id: "605ea59c5cdf492b48987107", name: "Jerry Ale", jerseyNumber: "12"}
1: {_id: "605ea59c5cdf492b48987108", name: "Judel Agur", jerseyNumber: "14"}
2: {_id: "605ea59c5cdf492b48987109", name: "qwe", jerseyNumber: "12"}
3: {_id: "605ea59c5cdf492b4898710a", name: "qwe", jerseyNumber: "12"}
4: {_id: "605ea59c5cdf492b4898710b", name: "qwe", jerseyNumber: "12"}
length: 5
__proto__: Array(0)
teamName: "Balilihan"
updatedAt: "2021-03-27T03:25:16.148Z"
__v: 0
_id: "6054a8d63fec5c24389624ac"
I have an useEffect to gather this;
useEffect(() => {
const getTeam = async () => {
try {
const { data } = await fetchContext.authAxios.get('get-all-teams');
setIsLoaded(true);
if (isLoaded === true) {
setCurrentTeam(data.find((team) => team._id === row._id));
}
} catch (err) {
console.log(err);
}
};
getTeam();
}, [fetchContext, row, isLoaded]);
and I map the players array in a new variable because I want a controlled inputs for my form because I am updating the data. I am using Formik by the way
let playersOfTeam = currentTeam.players.map((player, index) => [
{
name: player.name,
jerseyNumber: player.jerseyNumber,
},
]);
But when I just get a specific value like the teamName it returns the teamName and when I console log currentTeam.players it returns what I expected to get. I am confused why I get this kind of error
Your data is undefined when the component is first mounted. This is because useEffect runs after render.
So adding a null check is the solution. Personally I prefer optional chaining. Simply change to:
let playersOfTeam = currentTeam?.players?.map((player, index) => [
{
name: player.name,
jerseyNumber: player.jerseyNumber,
},
]);

Accessing object value inside array using forEach? [duplicate]

This question already has answers here:
How to find object in array by property in javascript?
(3 answers)
Closed 4 years ago.
Hi my first question here. I have an array of objects that looks like this:
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2},
{title: 'title3', id: 3}]
From this array I want to extract the dynamic value of title by id. Meaning if I run a function on this I'd like to receive 'title1' when id is specified as '1'. I've tried a number of things including a forEach loop that right now looks like this:
albums.forEach(function(key, index, arr) {
arr[index].title })
I read elsewhere on this site that for/forEach loops could be useful in extracting value from an object inside an array of objects. But I guess I don't fully understand how a forEach loop works in this regard, because I'm unable to extract any value from it. if inside the forEach loop I call a console.log (so likeconsole.log(arr[index].title) it nicely logs the value of the title property for each element in the array. But when I try to return (return arr[index].title) and call the function it comes up as undefined.
So in summary I have these two questions:
What is the best way to access a value inside an object inside an array of objects?
How does a forEach loop work exactly when trying to access an object inside an array of objects?
Thanks for any input, suggestions, feedback
Try this code:
const albums = [{ title: 'title1',id: 1}, {title: 'title2',id: 2},{title: 'title3',id: 3}]
var idToFind = 2;
albums.forEach((element) => {
if (element.id == idToFind) {
console.log(element.title)
}
})
(1) To find a value in an array, Array.prototype.find is almost always your best bet.
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2}, {title: 'title3', id: 3}]
const findAlbum = (id) => albums.find(album => album.id === id)
console.log(findAlbum(2)); //~> {title: 'title2', id: 2}
(2) forEach can be made to work, but it is not generally the best fit for this job. forEach is about running functions against elements. It does not intrinsically capture the results. So you could wrap it like this:
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2}, {title: 'title3', id: 3}]
const findAlbum = (id) => {
let found
albums.forEach(album => {
if (!found && album.id === id) {
found = album
}
})
return found
}
console.log(findAlbum(2)) //~> {title: 'title2', id: 2}
That code is clearly more complex. And it obscures the fact that what you want to do is to "find" something.
(It also has a more insidious problem. If you wanted to find something in a list of potentially false-y values, it will keep searching if the value is false-y. So if your were to alter this to try to find the first even number in [3, 5, 7, 0, 11, 14, 13, 17, 20], it would skip the false-y 0 and return 14.) You can fix this by splitting found into two variables, one to check whether we've found anything and another to store the found value, but that makes the code still more complex, especially when we have a find method available.)
(3) You could break this down into some reusable helpers. I won't write them here. But you could write generic reusabale where and equals functions so that the code looks more like this:
const findAlbum = (id) => albums.find(where('id', equals(id)))
That looks a lot more readable to me.
JavaScript has several ways to extract and manipulate objects in arrays. Here are some of the most common that pertain to your problem:
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2},
{title: 'title3', id: 3}];
// For each is for doing something that causes side effects
albums.forEach(({title}) => console.log(title))
// nothing is returned
// console output:
// title1
// title2
// title3
// Map is for transforming each object in the array into something else
const titles = albums.map(({title}) => title);
// => ['title1', 'title2', title3']
// If you need to find a specific one, use find
const albumWithId3 = albums.find(({id}) => id === 3);
// => {title: 'title3', id: 3}
// If you need to find a subset, use filter
const ablumsWithIdsLessThan3 = albums.filter(({id}) => id < 3)
// => [{title: 'title1', id: 1}, {title: 'title2', id:2}]
To directly answer your question, you probably want something like this:
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2},
{title: 'title3', id: 3}];
const getTitle = album => album ? album.title : undefined;
const idToAlbum = (myId, albums) => albums.find(({id}) => myId === id);
const idToTitle = (myId, albums) => getTitle(idToAlbum(myId, albums));
const title = idToTitle(1, albums);
console.log(title);
// => 'title1'
Or, if you prefer an imperative style:
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2},
{title: 'title3', id: 3}];
const idToTitle = (id, albums) => {
for (const album of albums) {
if (album.id === id) {
return album.title;
}
}
}
const title = idToTitle(1, albums);
console.log(title);
// => 'title1'
Try this:
function getTitleById(id)
{
var i;
for (i = 0; i < albums.length; i++) {
if(albums[i]['id'] == id)
return albums[i]['title'];
}
}
If you know the position of the object in the array, just access it with the position. I.e console.log(albums[4].title). But if you don't know the position you need to go over each and search for the object in question. Then the best way would be to write a search function that returns the object.
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2},
{title: 'title3', id: 3}];
console.log(serachArrayByTitle(albums,'title2'));
function serachArrayByTitle(arrayToSerach,titleToSearchFor){
for(var i=0;i<arrayToSerach.length;i++){
if(arrayToSerach[i].title===titleToSearchFor){
return arrayToSerach[i];
}
}
}
forEach() executes the given callback function for each element of the array. Read more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
For consistency, you should use forEach scoped, like this:
var Album = function(arr){
this.arr = arr;
this.get = function(id){
this.arr.forEach((element) => {
if (element.id == id) {
console.log(element.title)
}
})
}
}
a = new Album([{title: 'title1', id: 2}]);
a.get(2)
this should return 'title1'
You can use Array#find() to look for an array element that matches whatever criteria you want.
Then if there is a match return whatever you want from that element
const albums = [{title: 'title1', id: 1}, {title: 'title2', id: 2},
{title: 'title3', id: 3}]
const getTitleById = (id) => {
const o = albums.find(album => album.id === id);
return o && o.title
}
console.log(getTitleById(2))
console.log(getTitleById(3))

Applying a property to each element in several arrays, then returning a flat map with one array

I have a collection that looks like this
[
{
count: 123,
description: 'some description',
articles: [
{...}
]
},
{
count: 234,
description: 'some description',
articles: [
{...}
]
}
]
Each object in the collection has a collection of articles. What I need is to apply the description to each article object in the respective collection in each element of the primary collection. I also want to end up with a flat array containing only articles. Clearly I'm using mergeMap incorrectly, but I'm not sure how to do it.
I have tried this
json$.pipe(
// Filter out empty arrays
filter(section => section.count > 0),
// Apply the descriptions
map(section => section.articles.map(a => (Object.assign(a, section.sectionName)))),
mergeMap(x => x.articles)
).subscribe(x => console.log(x));
But the articles do not have the description property in them, and it's not a flat array of articles. I've tried a few things but I'm unsure how to proceed
You only need to concatMap the outer observable, after adjusting each article.
const { Observable } = Rx;
const { map, concatMap, filter } = Rx.operators;
const json$ = Observable.from([
{
count: 123,
description: 'some description 123',
articles: [
{id: 1},
{id: 2},
]
},
{
count: 234,
description: 'some description 234',
articles: [
{id: 3},
{id: 4},
]
}
]);
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => Observable.from(articles).map(a => ({...a, description}))
)
);
withDescription$.subscribe(console.log);
<script src="https://unpkg.com/#reactivex/rxjs#^5/dist/global/Rx.min.js"></script>
If you don't need any special behavior on the inner observable, you could simplify to:
const withDescription$ = json$.pipe(
filter(({count}) => count > 0),
concatMap(
({articles, description}) => articles.map(a => ({...a, description}))
),
);

Categories

Resources