1

Getting frustrated to solve this since I am no JS expert.

I am using Firestore as a database and VuexFire to bind the data to VueX state, like so.

 getLeads: firestoreAction(async ({
        bindFirestoreRef
    }) => {
        // return the promise returned by `bindFirestoreRef`
        return bindFirestoreRef('leads', db.collection('leads').orderBy('updated.date', 'desc').limit(30))
    }),
It gets the first 30 results and then i want to implement an infinite scroll feature to run a function every time the scroll reaches the bottom and fetch more data and bind to the same state. In Firestore pagination require passing a query cursor of the last fetched document as a reference

Below from firebase document, with vanilla JS

var first = db.collection("cities")
        .orderBy("population")
        .limit(25);

return first.get().then(function (documentSnapshots) {
  // Get the last visible document
  var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
  console.log("last", lastVisible);

  // Construct a new query starting at this document,
  // get the next 25 cities.
  var next = db.collection("cities")
          .orderBy("population")
          .startAfter(lastVisible)
          .limit(25);
});
since I use VuexFire to bind the data to state, I dont see an option to get the snapshot of the last document fetched by VuexFire (lastVisible from the above code), in order to pass it to the next query.

Any help will be highly appreciated.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Jenos
  • 485
  • 1
  • 4
  • 15

3 Answers3

4

Lets say I have a collection of Customer records and i am displaying the first 5 ordered by last updated.

The query is

getLeads: firestoreAction(({ commit, bindFirestoreRef
}) => {
    bindFirestoreRef('Leads', db.collection('leads')
   .orderBy('updated.date', 'desc').limit(5)).then(documents => {
        commit('POPULATE_TESTLEADS', documents);
        commit('LAST_DOC', documents[documents.length - 1]);
    });

}),

I am saving both the results and the lastdoc in the state, looping and showing the names, like so:

Nakheel
Emaar Group
Yapi Kredi Inc
Cairo Amman Bank
Arab Jordan Investment Bank LLC

I then call again with the last doc as query cursor and expect the next 5 docs to return from firebase, like so

moreLeadLeads: firestoreAction(({ state, bindFirestoreRef
}) => {
    bindFirestoreRef('testLeads', db.collection('leads')
    .orderBy('updated.date', 'desc')
   .startAfter(state.lastDoc).limit(5))
}),

But I get the same 5 results as above from firestore. What am I doing wrong? :(

Jenos
  • 485
  • 1
  • 4
  • 15
3

Internally VueFire and VuexFire use a serializer function that maps each Document returned by RTDB or Firestore into the data objects that are bound to the final component or Vuex store state.

The default serializer is implemented by the function createSnapshot that is part of the vuefire-core library:

/**
 * @param {firebase.firestore.DocumentSnapshot} doc
 * @return {DocumentData}
 */
export function createSnapshot (doc) {
  // defaults everything to false, so no need to set
  return Object.defineProperty(doc.data(), 'id', {
    value: doc.id
  })
}

As you can see it returns only doc.data() (with id added) and discards the doc object. However when implementing Firestore pagination via query.startAfter(doc) we need the original doc object. The good news is that VueFire and VuexFire APIs allow us to replace the serializer with our own that can preserve the doc object like so:

const serialize = (doc: firestore.DocumentSnapshot) => {
  const data = doc.data();
  Object.defineProperty(data, 'id', { value: doc.id });
  Object.defineProperty(data, '_doc', { value: doc });
  return data;
}

We can configure our new VuexFire serializer either globally via plugin options or per binding via binding options.

// Globally defined Vue.use(firestorePlugin, { serialize });

// OR per binding bindFirebaseRef('todos', db.ref('todos'), { serialize } )

For VuexFire, we can now get access to the first document as state.todos[0]._doc or last document state.todos[state.todos.length-1]._doc and use them to implement pagination queries for collections or "get next" & "get previous" queries that bind single documents (essential when your base query has multi-field sorting).

NOTE: Because _doc and id are non-enumerable properties, they won't appear on component or store objects in Vue DevTools.

Tony O'Hagan
  • 21,638
  • 3
  • 67
  • 78
  • I am having the same issue. This almost got me there but I'm still unclear on how to get the paginated query results to bind to the original state data. I was not able to add a comment before so I created a new question here https://stackoverflow.com/questions/62639778/paginating-firestore-data-when-using-vuex-and-appending-new-data-to-the-state – ak22 Jun 30 '20 at 01:23
  • I've added some possible solutions for extending this to implement pagination. https://stackoverflow.com/a/62650214/365261 – Tony O'Hagan Jun 30 '20 at 04:19
1

From the VueFire documentation on binding data and using it, the $bind method (which I assume your bindFirestoreRef wraps) returns a promise with the result (as well as binding it to this). From that documentation:

this.$bind('documents', documents.where('creator', '==', this.id)).then(documents => {
  // documents will point to the same property declared in data:
  // this.documents === documents
})

So you should be able to do the same, and then get the document from the array with something like:

bindFirestoreRef('leads', db.collection('leads').orderBy('updated.date', 'desc').limit(30)).then(documents => {
  this.lastDoc = documents[documents.length - 1];
})
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • 1
    Thanks. But it deoesnt seem to work. Actually passing last document data to firestore cant be used as a query cursor. I should be passing a document reference, which is the parent of the document $bind returns :( – Jenos Nov 20 '19 at 17:31
  • 1
    Can you show me the docs of the method that takes a document reference to initialize a cursor? In the clients that I regularly work with ([JavaScript](https://firebase.google.com/docs/reference/js/firebase.firestore.Query.html#startat), ReactNative, [Android](https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/Query.html#startAt(com.google.firebase.firestore.DocumentSnapshot)), Swift, Flutter) it's a document *snapshot* as that contains the actual data that the cursor needs. – Frank van Puffelen Nov 20 '19 at 18:11
  • 1
    https://firebase.google.com/docs/firestore/query-data/query-cursors Its under Paginate a Query section – Jenos Nov 21 '19 at 12:30
  • 1
    document snapshot is what the cursor needs, however, as in my original question, documents from $bind in VuexFire retunrs the actual data of the document and not the document snapshot. – Jenos Nov 21 '19 at 12:32
  • 1
    You can also pass an array of values, instead of a document snapshot. – Frank van Puffelen Nov 21 '19 at 14:04
  • 1
    I have no clue why its not working. Lemme see if i can put up some code somewhere for you to refer. – Jenos Nov 21 '19 at 16:01