0

I implemented the following to display a paginated query (this was suggested by Tony O'Hagan in this post: How to get the last document from a VueFire query):

bindUsers: firestoreAction(({ bindFirestoreRef }) => {
  return bindFirestoreRef('users', 
      Firebase.firestore().collection('users').limit(8), { serialize })
}),
bindMoreUsers: firestoreAction(context => {
  return context.bindFirestoreRef('users', Firebase.firestore().collection('users').startAfter(context.state.users[context.state.users.length - 1]._doc).limit(8), { serialize })
})

When the user scrolls to the end of the page, I call bindMoreUsers which updates the state.users to the next set of 8 documents. I need to be able to append to the state.users as opposed to overwrite the original set of 8 documents. How can I do this?

ak22
  • 158
  • 2
  • 14
  • HI @ak22 - Are you needing the realtime updates? If you are then your likely best bet off the top of my head is to use `startAt` the document of your first query and rather than do a hard coded `limit(8)` do a limit with offset such as `limit(users.length + offset)` this way you keep your existing users list and tack on the additional `offset` amount, in your case `8`. If you don't need the realtime, then just list the array and keep your `doc.id` for whatever UI edit updates to save back to Firestore. – JoeManFoo Jun 30 '20 at 05:47
  • Thank you. I will try this out and let you know. – ak22 Jul 01 '20 at 02:29
  • So, this is basically binding first 8 documents then 16, then 24 and so forth. So, doesn't that accrue 8+16+24 reads? – ak22 Jul 01 '20 at 23:13
  • Hi @ak22 - it will accrue additional reads. You can use some analytics to help you make the decision on whether or not this is the best fit for you. For example, do the initial read for 24 items but only display 8 and then have analytics tied to your user's behavior by tracking their consumption for more items. If there is significant usage for more pagination then weigh impact of UX/UI for listing more results initially to reduce reads. – JoeManFoo Jul 02 '20 at 06:05
  • Ok, thanks. It's strange that there is no good solution for this. Seems like a common use case right? Let's say you want to scroll through a list of members. Now that I think about it I don't think real time updates are necessary as I am only displaying the member name, location and a couple of other statistical items that are not likely to change very much. So, I could possibly do direct queries as opposed to binding queries in the store. Thanks for your responses. – ak22 Jul 02 '20 at 16:52

1 Answers1

1

Confession: I've not yet implemented pagination on my current app but here's how I'd approach it.

In my previous answer I explained how to keep references to the Firestore doc objects inside each element of the state array that is bound by VuexFire or VueFire. In Solution #1 below we use these doc objects to implement Firestore's recommended cursor based pagination of a query result sets using startAfter(doc) query condition instead of the slower more expensive offset clause.

Keep in mind that since we're using Vuexfire/Vuefire we're saying that we wish to subscribe to live changes to our query so our bound query will define precisely what ends up in our bound array.

Solution #1. Paging forward/backward loads and displays a horizontal slice of the full dataset (our bound array maintains the same size = page size). This is not what you requested but might be a preferred solution given the Cons of other solutions.

  • Pros: Server: For large datasets, this pagination query will execute with least cost and delay.
  • Pros: Client: Maintains a small in memory footprint and will render fastest.
  • Cons: Pagination will likely not feel like scrolling. UI will likely just have buttons to go fwd/backward.
  • Page Forward: Get the doc object from the last element of our state array and apply a startAfter(doc) condition to our updated view query that binds our array to the next page.
  • Page Backward: Bit Harder! Get the doc object from the first element of our bound state array. Run our page query with startAfter(doc), limit (1), offset(pagesize-1) and reverse sort order. The result is the starting doc (pageDoc) of the previous page. Now use startAfter(pageDoc) and forward sort order and limit(pageSize) to rebind the state array (same query as Page Forward but with doc = pageDoc).

NOTE: In the general case, I'd argue that we can't just keep the pageDoc values from previous pages (to avoid our reverse query) since we're treating this as a 'live' update filtered list so the number of items still remaining from previous pages could have radically changed since we scrolled down. Your specific application might not expect this rate of change so perhaps keeping past pageDoc values would be smarter.

Solution #2. Paging forward, extends the size of the query result and bound array.

  • Pros: UX feels like normal scrolling since our array grows.

  • Pros: Don't need to use serializer trick since we're not using startAfter() or endBefore()

  • Cons: Server: You're reloading from Firestore the entire array up to the new page every time you rebind to a new page and then getting live updates for growing array. All those doc reads could get pricey!

  • Cons: Client: Rendering may get slower as you page forward - though shadow DOM may fix this. UI might flicker as you reload each time so more UI magic tricks needed (delay rendering until array is fully updated).

  • Pros: Might work well if we're using an infinite scrolling feature. I'd have to test it.

  • Page Forward: Add pageSize to our query limit and rebind - which will re-query Firestore and reload everything.

  • Page Backward: Subtract pageSize from our query limit and rebind/reload (or not!). May also need to update our scroll position.

Solution #3. Hybrid of Solution #1 and #2. We could elect to use live Vuexfire/Vuefire binding for just a slice of our query/collection (like solution #1) and use a computed function to concat it with an array containing the pages of data we've already loaded.

  • Pros: Reduces the Firestore query cost and query delay but now with a smooth scrolling look and feel so can use Infinite scrolling UI. Hand me a Koolaid!
  • Cons: We'll have to try to keep track of which part of our array is displayed and make that part bound and so live updated.
  • Page Forward/Backward: Same deal as Solution #1 for binding the current page of data, except we now have to copy the previous page of data into our non-live array of data and code a small computed function to concat() the two arrays and then bind the UI list to this computed array.

Solution #3a We can cheat and not actually keep the invisible earlier pages of data. Instead we just replace each page with a div (or similar) of the same height ;) so our scrolling looks we've scrolled down the same distance. As we scroll back we'll need to remove our sneaky previous page div and replace it with the newly bound data. If you're using infinite scrolling, to make the scrolling UX nice and smooth you will need to preload an additional page ahead or behind so it's already loaded well before you scroll to the page break. Some infinite scroll APIs don't support this.

Solution #1 & #3 probably needs a Cookbook PR to VueFire or a nice MIT'd / NPM library. Any takers?

Tony O'Hagan
  • 21,638
  • 3
  • 67
  • 78
  • @tony-ohagan, great write-up and great work on the previous answer post. For `Solution #3a` I think a simple `a` (anchor) would work for the scroll work which would open easy scroll-to functionality. I don't think these things ought to live within the VueFire et al. packages (unless used as examples) but a NPM makes sense as well. – JoeManFoo Jun 30 '20 at 05:37
  • Thanks for the detailed answer. I think option 3 is worth pursuing in my case but this is the part I'n not clear on: "We'll have to try to keep track of which part of our array is displayed and make that bound and so live updated". Actually a side question I have is (probably because I don't understand the internals of Vuexfire), if a previously bound document is bound again, does this incur another read? So, lets say items 1-10 are in view and are currently bound. Then user scrolls and now 2-11 are bound. Scrolls back, 1-10 are bound again. Does item 1 incur another read? – ak22 Jul 01 '20 at 02:28
  • No that would kill the performance and blow up the cost as you're re-querying every time you rebind. Solution depends on who you plan to implement paging .. using **scrolling** or **next/prev page buttons**. If you're using scrolling then you're using a gadget like [Quasar's Infinite Scroll](https://quasar.dev/vue-components/infinite-scroll) that triggers loading of the next page. So if pageSize = 10, 2nd page would be 11..20. – Tony O'Hagan Jul 02 '20 at 00:04