EMIKETIC Logo

EMIKETIC Blog


EMIKETIC is a team of passionate programmers who fancy real-time and distributed application development. We write here about Meteor, React, modern JavaScript, Elixir, Phoenix and Ruby on Rails.


Server Side Pagination, using React, Redux and Ant Design

Server Side Pagination, using React, Redux and Ant Design

Let’s be straightforward: Fetching a quite long list of users (or other entity) from an API and displaying them in a list view is relatively not useful especially for large sets of data.

The main handicap of this idea would be an unlimited scroll down list which is definitely a bad design. Plus sinking the client with data.

Server side pagination seems useful to solve this matter. It transfers to the client a much smaller data to handle. So it provides the possibility to specify the number of data sub-sets to retrieve per page and for a given page number.

Although a substantial number of APIs do support serving data in this fashion, there are also a considerable number of them that don’t.

In our blog post, we are exposing two different solutions :

  • Server side pagination assuming an API that supports it.

  • Client side pagination without pagination, that is full data transfer.

At EMIKETIC, we use React with the Redux architecture combined with Ant Design. We will be showcasing this post with this stack along with a example loading users from a Restful API.

Let’s dive in!

As the Redux mantra prescribes it, all the application state is stored as a single object in a store. First of all, let’s define a sub-state we will call User ( refers to user component) in order to store rendered data and pagination states.

User component substate

const INDEX_PAGE_SIZE_DEFAULT = 50;
const INDEX_PAGE_SIZE_OPTIONS = [5, 10, 20, 30, 50, 100];

state = {
  data: null,
  meta: {
    page: 1,
    pageSize: INDEX_PAGE_SIZE_DEFAULT,
    pageSizeOptions: INDEX_PAGE_SIZE_OPTIONS,
    pageTotal: 1,
    total: 0,
  },
};

First Approach: Server-side pagination

This approach is used only when the API supports sending pagination parameters to receive a much smaller amount of data. Using Redux, we have to define action types, action creators and thunks to make it all happen.

Action Types and Action Creators to load users

// REQUEST LOADING USERS
const USER_INDEX_REQUEST = 'USER_INDEX_REQUEST';
const fetchIndexRequest = () => {
  return {
    type: USER_INDEX_REQUEST,
  };
};

// USERS RETREIVED WITH SUCCESS
const USER_INDEX_SUCCESS = 'USER_INDEX_SUCCESS';
const fetchIndexSuccess = payload => {
  return {
    type: USER_INDEX_SUCCESS,
    data: payload,
  };
};

// FAILED TO RETREIVE USERS
const USER_INDEX_FAILURE = 'USER_INDEX_FAILURE';
const fetchIndexFailure = () => {
  return {
    type: USER_INDEX_FAILURE,
  };
};

// THUNK ACTION CREATOR TO FETCH USERS
function $fetchIndex() {
  return (dispatch, getState) => {
    const { meta } = getState().User;

    dispatch(fetchIndexRequest());

    return fetch(
      `${endpoint}/users?${serializeQuery({
        per_page: meta.pageSize,
        page: meta.page - 1,
      })}`
    )
      .then(result =>
        dispatch(
          fetchIndexSuccess({
            data: result.users,
            meta: {
              page: 1 + result.start / result.limit,
              pageSize: result.limit,
              pageTotal: Math.ceil(result.total / result.limit),
              total: result.total,
            },
          })
        )
      )
      .catch(error => dispatch(fetchIndexFailure(error)));
  };
}

with :

function serializeQuery(query) {
  return Object.keys(query)
    .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`)
    .join('&');
}

Action Type and Action Creators for pagination

// SINGLE ACTION TYPE TO CHANGE META DATA
const USER_INDEX_META = 'USER_INDEX_META';

// ACTIONS CREATORS
function $pageSize(pageSize = INDEX_PAGE_SIZE_DEFAULT) {
  if (pageSize < 1) {
    pageSize = 10;
  }

  if (pageSize > 100) {
    pageSize = 100;
  }

  return {
    type: USER_INDEX_META,
    meta: {
      pageSize,
      page: 1,
    },
  };
}

function $page(page = 1) {
  return (dispatch, getState) => {
    const { meta } = getState()[substate];

    if (page < 1) {
      page = 1;
    }

    if (page > meta.pageTotal) {
      page = meta.pageTotal - 1;
    }

    dispatch({
      type: USER_INDEX_META,
      meta: {
        page,
      },
    });
  };
}

Reducer

function reducer(
  state = {
    data: null,
    meta: {
      page: 1,
      pageSize: INDEX_PAGE_SIZE_DEFAULT,
      pageSizeOptions: INDEX_PAGE_SIZE_OPTIONS,
      pageTotal: 1,
      total: 0,
    },
  },
  action
) {
  switch (action.type) {
    case USER_INDEX_META:
      return {
        ...state,
        meta: {
          ...state.meta,
          ...action.meta,
        },
      };
    case USER_INDEX_SUCCESS:
      return {
        ...state,
        data: action.data,
        meta: {
          ...state.meta,
          ...action.meta,
        },
      };
    default:
      return state;
  }
}

Wiring to the view

We used all defined actions in a react component called UserIndexView connected to the store thanks to react-redux npm module. In this component, we used the component Table from Ant Design which contains the configurable property Pagination. The properties of pagination responsible of changing the pagination state are:

  • onShowSizeChange : the action creator $pageSize is dispatched once the pageSize is changed.

  • onChange: the action creator $page is dispatched when the page number is changed.

  • pageSizeOptions: is an array that specifies the sizeChanger options. In our example we have pageSizeOptions = [5, 10, 20, 30, 50, 100].

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Table, Avatar, Icon, Input, Button } from 'antd';

import { $pageSize, $page, $fetchIndex } from './state';

// provide shared state and actions as props
const withStore = connect(
  (state) => ({
    data: state.User.data,
    meta: state.User.meta,
  }),
  (dispatch) => ({
    dispatch,
  }),
);

// provides shared state and actions as props
const Connector = (C) => withStore(C);

class UserIndexView extends Component {
  componentWillMount() {
    this.props.dispatch($fetchIndex())));
  }
// PAGINATION OPTIONS
  paginationOptions = {
    showSizeChanger: true,
    showQuickJumper: true,
    onShowSizeChange: (_, pageSize) => {
      this.props.dispatch($pageSize(pageSize));
      this.props.dispatch($fetchIndex())));
    },
    onChange: (page) => {
      this.props.dispatch($page(page));
      this.props.dispatch($fetchIndex())));
    },
    pageSizeOptions: this.props.meta.pageSizeOptions,
    total: this.props.meta.total,
    showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`,
  };

  render() {
    const pagination = {
      ...this.paginationOptions,
      total: this.props.meta.total,
      current: this.props.meta.page,
      pageSize: this.props.meta.pageSize,
    };

    return (
      <Table
        style={{ backgroundColor: 'white', flex: 1 }}
        dataSource={this.props.data}
        pagination={pagination}
      >
        <Table.Column
          title="Name"
          key="name"
          render={(record) => (<span>{record.name}</span>)}
        />
        <Table.Column
          title="Email"
          key="email"
          render={(record) => (<span>{record.email}</span>)}
        />
      </Table>
    );
  }
}
export default Connector(UserIndexView);

Second Approach: Client-side pagination

In this part, we are going to mention just the trick that differentiates the Client-side pagination from Server-side pagination. As the API does not supply the filtering parameters to send from the client and loads instead the whole data, we will need to load all the data and update pagination options based on our stored state and dispatch actions using Redux.

In the main component UserIndexView already defined, we definitely have no differences, just as we don’t in all actions creators. There are only two small differences in thunk action creator and reducer which will be mentioned in the two pieces of code:

Thunk action creator

...
return fetch(`${endpoint}/users`)
  .then((result) =>
    dispatch(fetchIndexSuccess({
      data: result.users,
      meta: {
        page: 1,
        pageSize: meta.pageSize,
        pageTotal: Math.ceil(result.total / meta.pageSize),
        total: result.total,
      },
    })))
...

Reducer

...
case USER_INDEX_SUCCESS:
  return {
    ...state,
    data,
    meta: {
      ...state.meta,
      ...action.meta,
      pageTotal: Math.ceil(data.length / state.meta.pageSize),
      total: data.length,
    },
  };
...

Conclusion

The purpose of this blog post is to highlight the importance of using pagination even if the restful API we are using does not provide it, and the important role that Redux plays in managing and organizing the whole mechanism.