Best practice for creating tables with pagination using React and WithTracker


#1

Hi guys, I’ve tried searching the forums and have run into a few helpful suggestions concerning building a custom table with subscription/pagination in React with WithTracker

eg: Meteor 1.5 publication/method pagination

However I keep seeming to run into the issue that in order to have access to current skip count, limit, query etc I’d need to resort to Session or some other global store. In Blaze I managed this easily enough with various React Vars inside a Tracker.autorun call, things don’t seem to be woking out quite as simply with withTracker.

Has anyone come up with a workable solution? Thanks in advance. (Am happy to share code but I’m not sure my current pattern is the right one.)


#2

You should still be able to get this all working with ReactiveVar's. I like using Griddle in Meteor+React apps, so here are a few quick code snippets from a recent project - hope this helps!

TransactionsContainer.js

import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { ReactiveVar } from 'meteor/reactive-var';

import { carbonOffsetTransactionsCollection } from 'meteor/l2:common';
import { TransactionsTable } from './TransactionsTable';
import { getTransactionsCount } from '../../../api/carbon-offsets/methods';

const tableConfig = new ReactiveVar({
  pageProperties: {
    currentPage: 1,
    pageSize: 10,
    recordCount: 0,
  },
  sortProperties: [
    { id: 'timestamp', sortAscending: false },
  ],
  filter: null,
});

export const TransactionsContainer = withTracker(() => {
  const config = tableConfig.get();

  getTransactionsCount.call((error, count) => {
    if (config.pageProperties.recordCount !== count) {
      config.pageProperties.recordCount = count;
      tableConfig.set(config);
    }
  });

  const sort = {
    [config.sortProperties[0].id]:
      config.sortProperties[0].sortAscending ? 1 : -1,
  };

  const transactionsHandle = Meteor.subscribe('carbonOffsetTransactions', {
    filter: config.filter,
    limit: config.pageProperties.pageSize,
    skip: config.pageProperties.currentPage < 0
      ? 0
      : (config.pageProperties.currentPage - 1) * config.pageProperties.pageSize,
    sort,
  });

  return {
    transactionsReady: transactionsHandle.ready(),
    transactions: carbonOffsetTransactionsCollection.find({}, { sort }).fetch(),
    tableConfig,
  };
})(TransactionsTable);

TransactionsTable.js

import React from 'react';
import PropTypes from 'prop-types';
import Griddle, { RowDefinition, ColumnDefinition } from 'griddle-react';

import { TablePagination } from '../table/TablePagination';
import { NextButton } from '../table/NextButton';
import { PreviousButton } from '../table/PreviousButton';
import { Filter } from '../table/Filter';
import { DateCell } from '../table/DateCell';

export const TransactionsTable = ({ transactions, tableConfig }) => {
  const config = tableConfig.get();
  return (
    <Griddle
      data={transactions}
      styleConfig={{
        classNames: {
          Table: 'table',
        },
      }}
      components={{
        Filter,
        Pagination: TablePagination,
        NextButton,
        PreviousButton,
      }}
      pageProperties={{ ...config.pageProperties }}
      sortProperties={[...config.sortProperties]}
      enableSettings={false}
      events={{
        onPrevious() {
          config.pageProperties.currentPage -= 1;
          tableConfig.set(config);
        },
        onNext() {
          config.pageProperties.currentPage += 1;
          tableConfig.set(config);
        },
        onGetPage(pageNumber) {
          config.pageProperties.currentPage = pageNumber;
          tableConfig.set(config);
        },
        onSort({ id }) {
          if (config.sortProperties[0].id === id) {
            config.sortProperties[0].sortAscending =
              !config.sortProperties[0].sortAscending;
          } else {
            config.sortProperties[0] = {
              id,
              sortAscending: true,
            };
          }
          config.pageProperties.currentPage = 1;
          tableConfig.set(config);
        },
        onFilter(filterText) {
          config.filter = filterText;
          config.pageProperties.currentPage = 1;
          tableConfig.set(config);
        },
      }}
    >
      <RowDefinition>
        <ColumnDefinition
          id="timestamp"
          title="Timestamp"
          type="date"
          customComponent={DateCell}
        />
        <ColumnDefinition id="transactionType" title="Type" />
        <ColumnDefinition id="carbonOffsetAmount" title="Purchse Amount ($)" />
        <ColumnDefinition id="partnerId" title="Partner ID" />
        <ColumnDefinition id="customerEmailHash" title="Customer Email Hash" />
      </RowDefinition>
    </Griddle>
  );
};

TransactionsTable.propTypes = {
  transactions: PropTypes.array,
  tableConfig: PropTypes.object.isRequired,
};

TransactionsTable.defaultProps = {
  transactions: [],
};

#3

Thanks, that’s incredibly helpful! I did look into Griddle but there was no mention anywhere of managing subscriptions so was happy enough rolling my own which is fine for my purposes.

It seems like a key to your code being reactive is storing your reactiveVar outside of your components?


#4

Absolutely - if you created your ReactieVar inside withTracker for example, it would be re-created each time your withTracker function runs.


#5

Perfect, thank you, so essentially a ReactiveVar mounted at that stage could replace the need for using Session.

Are there any problems instantiating ReactiveVar outside any of the lifecycles?

Thanks again :+1:


#6

No problems at all - the ReactiveVar in this case lives outside of React’s component lifecycle completely. You’re passing it into a component that needs it as a prop, which is all that React needs to know to use it properly. In essence, this is actually somewhat similar to how something like Redux works. You’re using the ReactiveVar like a Redux store, but to keep things simpler you’re passing that store into the component that needs it. Redux passes the store around as a prop as well, but usually only as far as a container component, at which point the needed state is extracted and passed further down into presentational components.

So, long story short - you shouldn’t have any problems with this approach, and while it’s not as cleanly separated as something like Redux would be (and can get quickly out of hand in terms of maintenance for a larger application), it’s a much simpler approach to work with.


#7

Fantastic, thanks for the advice! Yes have previously used Mobx and it works really well, just wanted to avoid using it as a crutch. Cheers