A Way of Meteor DDP page-by-page Pagination


The Meteor document have discuss infinite-scroll pagination in detail.

Here I want to discuss a way of page-by-page pagination support sort, condition with limited data(page size).

  • Shortly, the idea is query the page with skip and limit, only fecth the ObjectID.
  • another query is the published cursor only include the data of that page which query based on the ObjectID.
  • And there are another query to find out the total count of the query.
  • Also there are a collection to store the total count of the query , the ids and the real page index.


Build query options based on params

export type PaginatingParams = {
  // page size
  size: number;
  // page index
  index: number;
  // sort field name
  sort?: string;
  // order
  order?: 'asc' | 'desc'

export function buildPaginatingOptions(paginatingParams: PaginatingParams, ops: any = {}) {
  ops = {
     fields: { '_id': 1 }

  if (paginatingParams.size > -1) {
    ops.skip = paginatingParams.index * paginatingParams.size;
    ops.limit = paginatingParams.size;

  if (paginatingParams.sort) {
    ops.sort = { [paginatingParams.sort]: paginatingParams.order == 'asc' ? 1 : -1 }

  return ops;

As you see, the buildPaginatingOptions function will create pagination options for mongodb query.


Meteor.publish('List.query.pub', function (args) {
const selector = {
      // some params here
    const ids = List.find(selector, buildPaginatingOptions(paginatingParams)).fetch().map(r => r._id.toString());
    // const ops = buildPaginatingOptions(paginatingParams);
    const total = List.find(selector).count();
    const selectorReal = {  _id: { $in: ids } };
    // records = CampaignSessionList.filterPrivateFields(records);

    let pagingSelector = {
        paramsHash: hash(args),

    PagingList.upsert(pagingSelector, {
        $set: {
            index: paginatingParams.index,
            updatedAt: now
        $setOnInsert: {
            createdAt: now,

    const pagingCursor = PagingList.find(pagingSelector);
    const recordsCursor = List.find(selector, selectorReal);

    return [pagingCursor, recordsCursor];

So there are three query, one for page ObjectID, one for page data will be used as publication, another is the total count.

The pagingSelector is the paging data we will use, we will discuss at later.

The PagingList

schema: {        
        // name
        name: { type: String, optional: false, index: true },
        // params hash
        paramsHash: { type: String, optional: false, index: true },
        // page index
        index: { type: Number, optional: false, index: false },
        // IDs
        ids: { type: Array, optional: false, index: false },
        'ids.$': { type: String, blackbox: true, optional: false, index: false },
        // total count
        total: { type: Number, optional: false, index: false }

PagingList store the paging data, we use name and params hash, to unified a publication, so we can use as many different pagination as we want at a time, and they will not disordered.

The Front-End

Here I use amazing DDP Client simpleddp and sift to perform ddp subscribe and filter the data.

const pubname = 'List.query.pub';
const subHandler = await ddp.subscribe(name, params);

const hashedParams = hash(params);

const { index, total , ids} = ddp.collection('PagingList').filter(sift({
  name: pubname,
  paramsHash: hashedParams

const coll = ddp.collection('List').filter(sift({


We have do lots of meteor based project, mostly, we want a page-per-page style pagination, we are satisfied with the result by the way I described above.

Total views.

© 2013 - 2023. All rights reserved.

Powered by Hydejack v6.6.1