- Great for Infinity Scroll: easily support multiple data sources in your infinity scroll.
- Retain order: your data is always in order, even when it comes from different sources.
- Service agnostic: work with any data service, whether REST, GraphQL, Algolia or another.
- Mix-and-match services: mix data services as you see fit, making one query from GraphQL and one from Algolia.
- Efficient: only fetch data when needed for that data source.
Used in production at https://wellpaid.io.
npm i combine-pagination --save
yarn add combine-pagination
If you already understand the problem, here are some quick examples for paginating across multiple data sets in different scenarios.
Paginate data from two generic data sets.
const combinedGetters =;const pageOne = await combinedGetters;const pageTwo = await combinedGetters;
Algolia - custom sorting
Paginate data from two distinct algolia queries where the Algolia Index is sorted by a custom field. Each query is using a different keyword.
const index =;const combinedGetters =;const pageOne = await combinedGetters;const pageTwo = await combinedGetters;
Algolia - default sorting
Paginate data from two distinct algolia queries where the Algolia Index is sorted using Algolia's default criteria.
This approach uses a custom
sort method that attempts to match Algolia's sorting algorithm.
sortAlgoliasort method used in this example is experimental. You might need to implement your own if using a custom ranking method.
;const index =;const combinedGetters =;
Suppose you have two data sets,
oldHats, and you want to combine them into one data set sorted by
const modernHats =name: "Baseball Cap"popularity: 95name: "Beanie"popularity: 50name: "Flat Cap"popularity: 20;const oldHats =name: "Top Hat"popularity: 85name: "Beret"popularity: 15name: "Bowler Hat"popularity: 9;
In this example, we’ll be paginating 2 results at a time. Let’s create a getter to paginate our data:
const getData = data;
Note, in reality you probably already have a data getting with pagination support to retrieve the data via a server.
Now let's get our data. Without using
combine-pagination, we might be tempted to just paginate each, sort and combine them. This is what NOT to do:
const modernHatsPage = ;const oldHatsPage = ;const hats = ...modernHatsPage ...oldHatsPage;
This will result in hats that looks like this
name: "Baseball Cap"popularity: 95name: "Top Hat"popularity: 85name: "Beanie"popularity: 50name: "Beret"popularity: 15;
This looks fine, until you query the second page, which will look like this
name: "Flat Cap"popularity: 20name: "Bowler Hat"popularity: 9;
If we combine these results, you’ll notice that now the results are out of order. Sure, we could re-sort our entire data set, but this has some problems:
- Reordering UI is confusing - if we’re rendering
hatsin a UI, such as an infinity scroll, it will cause the UI to reorder and confuse the user.
- Inefficient sort - re-sorting the entire data set on each pagination is highly inefficient.
- Unnecessary data request - depending on the order of the data, getting both data sets at once might be unnecessary, especially if a network request is involved.
Using a technique (currently) called Framed Range Intersection, we can conservatively hold back trailing data from the first page that we think might overlap with subsequent pages. In the example above, it would mean holding back "Beret" until the next page is retrieved.
combine-pagination implements this technique. Let's try again using the above data set:
;const combinedGetters =;
And query the first page:
const page = await combinedGetters;
name: "Baseball Cap"popularity: 95name: "Top Hat"popularity: 85name: "Beanie"popularity: 50;
As expected, we only received three results.
combine-pagination is only showing intersecting data, holding "Beret" back until it receives the next data set. Because of this, you can't define exactly how many results you want to receive. See Fuzzy Pagination.
The second time we run
getNext(), we get the next set of data, but this time in the correct order:
name: "Flat Cap"popularity: 20name: "Beret"popularity: 15name: "Bowler Hat"popularity: 9;
combine-pagination noticed that "Beret", which was held back from the first set of results, intersects "Flat Cap" and "Bowler Hat", so has inserted it and sorted the page.
That's it. Each time you call
getNext(), you'll retreive the next set of sorted data until the getters are exhausted.
- Using infinity scroll across multiple data sources.
- Paginating across multiple Algolia queries, such as one geo location query and one not.
- Paginating across different services.
Framed Range Intersecting
Intersecting ranges is a technique for finding values that overlap in two sets of data. For example:
- Intersection of [0, 3] & [2, 4] is [2, 3]
- Intersection of [-1, 34] & [0, 4] is [0, 4]
- Intersection of [0, 3] & [4, 4] is empty set
combine-pagination uses a technique called Framed Range Intersecting (name is WIP), a type of intersecting that determines the leading data set, and intersects the other data sets within that.
Unlike normal range intersecting:
- The first value is the first value of the leading data set.
- The last value is either the last value of the leading data set, or the the last value of the intersecting data set that finishes first.
- Values in multiple data sets are duplicated.
- Framed Intersection of [0, 3] & [2, 4] is [0, 2, 3]
- Framed Intersection of [-1, 34] & [0, 4] is [-1, 0, 4]
- Framed Intersection of [0, 3] & [4, 4] is [0, 3]
- Framed Intersection of [0, 3] & [2, 4] & [1, 2] is [0, 1, 2, 2]
Each time you execute
getNext(), you can't be sure how many results you're going to receive. We call this Fuzzy Pagination, which returns
0 - n results for any given page with page size
n. This technique is best suited for infinity scroll type use cases.
In normal pagination, you would receive
n results for each page, only receiving
0 - n results on the final page.
MIT © Chris Villa