Windowed Infinite Scroll
I was curious to know whether anyone has thoughts on “windowed” pagination—I’m not sure what people call this, but I’ll explain what I mean.
In most implementations of “infinite scroll” that I’ve seen, new items are loaded and appended to the end of a list indefinitely, but old items are never removed. This means that scrolling is not actually “infinite” in the sense that eventually, you load too many items onto the DOM and your page gets slow!
I would like the ability to “unload” items at the beginning of the list once the list reaches a certain limit, and then to reload those things if a user scrolls back up (or in the opposite direction).
I’m curious to know if this is possible to achieve right now, and if so, how!
12 Replies
First step I'd take would be a virtualized list view, still loading all data but only creating DOM elements for a window on the data. There are React libraries for this like https://github.com/bvaughn/react-virtualized
GitHub
GitHub - bvaughn/react-virtualized: React components for efficientl...
React components for efficiently rendering large lists and tabular data - GitHub - bvaughn/react-virtualized: React components for efficiently rendering large lists and tabular data
A less common case is actually unloading data from memory, not just the DOM. For that you'd need to make your own hook similar to usePaginatedQuery but with different behavior.
Good point, thank you! I don’t think a virtualized list would work for my use case, for a few different reasons (one being that the amount of data in memory could become too large), so I’m interested in that less common use case.
I’ll look into the pagination options a bit more to try and get a sense of the difficulty!
How much data are you talking about? I'd encourage you to try out a virtualized list but still load all of the data in memory. Thats what the Convex dashboard does (and my previous company, Asana did).
If you want to handle truly enormous lists, you'll also want to not keep the data for pages outside of the viewport on the client at all. To do this you'd want to write a hook similar to
usePaginatedQuery
that unsubscribes from old pages when they aren't in view. This should also be possible (we have an internal abstraction useQueries
https://docs.convex.dev/generated-api/react#usequeries that usePaginatedQuery
is based on that is general enough to do this kind of thing) but to my knowledge no one has tried this yet.react.js | Convex Developer Hub
These exports are not directly available in the convex package!
I guess I was initially imagining a version of the
usePaginatedQuery
hook where 1) the size of the "page" were limited by a max value, and 2) which also provided a loadMoreBackwards
function for loading the previous page. That seemed to me like it might be easier than using a virtual list, even assuming that keeping everything in memory is fine. But I'll think more about the tradeoffs of a virtual list option as well.
And thanks for pointing me to useQueries
!
I'm thinking about how to use the current API to implement some kind of "bi-directional pagination", and I'm having trouble seeing how one would go "back" a page if both
1. the Cursor
provided by a query is not usable by a different query (one which, say, sorts in reverse order), and
2. a paginate
query result provides only a single Cursor
corresponding to the position of (or after) the last item in the result
Am I missing something? Any chance someone could point me in the right direction?I see what you're getting at; if you don't hold onto the queries for those previous pages then you can't get them back
The design here is predicated on "we think you should hold onto those," but as it's built now what I think you're describing is at least convoluted, maybe not possible
@RJ it'd help me understand your goals (and help prioritize this) with a concrete example, similar to the "why not use a virtual list?" question from before
oh I hadn't caught up on the thread, I see
so they API you'd like is
when you say you want page size limited by a max value, it sounds like maybe you don't want these to be reactive? Are you ok with pages increasing and decreasing in size as contents change?
It'd be helpful to me to hear what the UI you want to build is; we definitely want common patterns to be easy and unusual ones to be possible, but do want to emphasize reactive queries.
Ah, no, I would want it to be reactive; my "max size" comment was misleading (disregard it). What I think I would like is exactly what you wrote in that code block, where
pageSize
indicates the initial page size, although the actual page size could change reactively. Basically, I think I'd want a query whose results are bookended by two cursors, and the ability to use the cursors to shift the results "window" either forwards or backwards. I realize that if this is to be reactive, it technically wouldn't mandate a hard limit on the size of the results set, but I think that would be fine.
As for the UI, imagine for example Apple Notes, but each note is rendered in full in a huge list, ordered chronologically. The goal would be to support infinite scrolling up and down throughout that entire history of notes, without needing to load them all into memory (which would, at a certain point, be prohibitively expensive, or at least a waste of the user's resources).
Virtualizing a list like that is also somewhat useless at a certain point, as you'd end up with a scrollbar that is so teeny tiny, it doesn't really tell you anything about the length of the list. And you run into trouble in that each note is of variable height, which in my experience and research is a difficult and finicky thing to support in a virtual list.
Imagine also a feature where you can "scroll" ("seek"?) to notes at a given point in history, like "March 1, 2022", while remaining in the context of this "infinite list".
It seems to me like this "windowed cursor-based pagination" would be the best fit for a feature set like this. I guess honestly you can fit a huge amount in memory, so maybe for a very long time you wouldn't need to avoid strategies that require that, but it seems so wasteful beyond a certain scale (both in terms of network bandwidth and memory usage), and most strategies that involve keeping all of that list data in memory also seem more complicated to implement (client-side, at least) than something like this "windowed cursor-based pagination" approach.A few thoughts:
First, thanks for the feedback! This use case totally makes sense. We haven't designed our pagination around double-ended pagination or "jumping" in the list and probably there are tweaks we can make to make this easier.
Given our existing
usePaginatedQuery
hook, you actually could create double-ended pagination! To do it, you'd want to create two separate query functions: one that uses .order("asc")
and one that uses .order("desc")
(or a single one with a parameter). Then you could call usePaginatedQuery
twice: once for the forward function once for the backwards one (filtering to start at the same point in the list). Then you can reverse the backwards list and concatenate them together to get a continuous middle segment. Obviously this loads all the data that you've viewed into memory.
It also is technically possible to write a paginator like usePaginatedQuery
that doesn't keep all the results in memory. The trick would be to maintain a data structure that stores the parameters for each page along with the query journals (https://docs.convex.dev/api/modules/browser#queryjournal). Then this paginator can make intelligent decisions of which of these parameters to pass to useQueries
based on the current scroll position. This is basically the same implementation as usePaginatedQuery
but only loading a subset of the pages at a time.
None of this creates the ability to "jump" to a given point in history at a time (only asking for the next page in two directions). You could maybe build "jumping" by not using .paginate(...)
at all and just passing date filters into the query (startDate
, endDate)
. That would be appropriate for a "timeline" view where each day is constant width.
Lmk if you want to hop on a call today to talk more about this! This stuff is complicated and different apps often need different pagination strategies.Module: browser | Convex Developer Hub
Tools for accessing Convex in the browser.
I'll write up more responses here, but I'd be happy to talk on a call today as well! I'm free between now and ~4:15pm Eastern time, and for an hour or so after ~5:15pm Eastern as well
Given our existing usePaginatedQuery hook, you actually could create double-ended pagination! To do it, you'd want to create two separate query functions: one that uses .order("asc") and one that uses .order("desc") (or a single one with a parameter). Then you could call usePaginatedQuery twice: once for the forward function once for the backwards one (filtering to start at the same point in the list). Then you can reverse the backwards list and concatenate them together to get a continuous middle segment. Obviously this loads all the data that you've viewed into memory.I'd thought about this approach, it makes sense (with the infinitely-growing list size caveat in mind)! I'll have to read more about how query journals work, and will look at the
usePaginatedQuery
source code more as well. I think I can't quite follow the vision for how that would work, yet
None of this creates the ability to "jump" to a given point in history at a time (only asking for the next page in two directions). You could maybe build "jumping" by not using .paginate(...) at all and just passing date filters into the query (startDate, endDate). That would be appropriate for a "timeline" view where each day is constant width.Good point that a date range filter would probably suffice for seeking/jumping to a point along a timeline. It would be really cool if it could be effected with
paginate
, but I'd have to think more about what an API would look like for that in the general case. Maybe the simplest would be if paginate
took an optional document ID, which is expected to be present in the results set, and which would (if present) then be the first item (the "anchor"?) in the results set?
So you'd do something like this:
🤷♂️UNPKG - convex
The CDN for convex
🙇♂️