March 14 2017

React - Pagination Example with Logic like Google

This is an example of how to implement pagination in React and ES6 / JavaScript with logic like Google's search results.

Project is available on GitHub at https://github.com/cornflourblue/react-pagination-example.

Here it is in action: (See on CodePen at http://codepen.io/cornflourblue/pen/oZZzLr/)

The React code on CodePen is slightly different to the code on GitHub, on CodePen all the React components are in same file / window because CodePen only has a single window for JS, the GitHub repo is structured like a real application with the React components in separate JSX files, this is the better way to structure your React project. Also the example on CodePen transpiles the ES6 into JS in the browser which is only good for demos and not production applications.

Even though the example is in React with ES6, the pagination logic inside the Pagination component is pure javascript and can easily be ported to other JS frameworks like Angular. The first version of the code I posted was actually in Angular 1 and later an updated version in Angular 2, if you're interested in those examples you can see them at Angular 1 Pagination Example and Angular 2 Pagination Example


Google's Pagination Logic

The logic in Google's pagination is as follows:

  • there are 10 page links shown at any time (e.g. 1 2 3 4 5 6 7 8 9 10) unless there are less than 10 total pages
  • the active link (current page) is in the 6th position, except for when the active link is below 6 or less than 4 from the last position


Here's what it looks like for each page if there are 15 total pages:

[1] 2 3 4 5 6 7 8 9 10
1 [2] 3 4 5 6 7 8 9 10
1 2 [3] 4 5 6 7 8 9 10
1 2 3 [4] 5 6 7 8 9 10
1 2 3 4 [5] 6 7 8 9 10
1 2 3 4 5 [6] 7 8 9 10
2 3 4 5 6 [7] 8 9 10 11
3 4 5 6 7 [8] 9 10 11 12
4 5 6 7 8 [9] 10 11 12 13
5 6 7 8 9 [10] 11 12 13 14
6 7 8 9 10 [11] 12 13 14 15
6 7 8 9 10 11 [12] 13 14 15
6 7 8 9 10 11 12 [13] 14 15
6 7 8 9 10 11 12 13 [14] 15
6 7 8 9 10 11 12 13 14 [15] 


Running the React Pagination Example Locally

The example uses webpack to transpile / compile the ES6 code and build the project, webpack is an npm package that runs on Node.

  1. Install NodeJS and NPM from https://nodejs.org/en/download/.
     
  2. Download the React project source code from GitHub at  https://github.com/cornflourblue/react-pagination-example
     
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
     
  4. Start the application by running npm start from the command line in the project root folder, this will start the webpack dev server which serves the example from port 8080 on your local machine.
     
  5. Browse to the url http://localhost:8080 to test the React pagination example


React Pagination Component

The React pagination component is a self contained reusable component that can be used to enable paging through any list of items. As properties it takes the array of items to be paged and an onChangePage() callback function to notify the parent component when the page is changed. It also has an optional property to set the initial page displayed which defaults to the first page.

When the paged items array is updated after initial rendering (e.g. after an async http request to a web api) the pagination component resets the page in the react componentDidUpdate() lifecycle function, causing the pagination component to re-render and notify the parent component via the onChangePage() callback;

I've structured the code according to the recommended best practices on the Airbnb React/JSX Style Guide.

import React, { PropTypes } from 'react';

const propTypes = {
    items: PropTypes.array.isRequired,
    onChangePage: PropTypes.func.isRequired,
    initialPage: PropTypes.number
}

const defaultProps = {
    initialPage: 1
}

class Pagination extends React.Component {
    constructor(props) {
        super(props);
        this.state = { pager: {} };
    }

    componentWillMount() {
        // set page if items array isn't empty
        if (this.props.items && this.props.items.length) {
            this.setPage(this.props.initialPage);
        }
    }

    componentDidUpdate(prevProps, prevState) {
        // reset page if items array has changed
        if (this.props.items !== prevProps.items) {
            this.setPage(this.props.initialPage);
        }
    }

    setPage(page) {
        var items = this.props.items;
        var pager = this.state.pager;

        if (page < 1 || page > pager.totalPages) {
            return;
        }

        // get new pager object for specified page
        pager = this.getPager(items.length, page);

        // get new page of items from items array
        var pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1);

        // update state
        this.setState({ pager: pager });

        // call change page function in parent component
        this.props.onChangePage(pageOfItems);
    }

    getPager(totalItems, currentPage, pageSize) {
        // default to first page
        currentPage = currentPage || 1;

        // default page size is 10
        pageSize = pageSize || 10;

        // calculate total pages
        var totalPages = Math.ceil(totalItems / pageSize);

        var startPage, endPage;
        if (totalPages <= 10) {
            // less than 10 total pages so show all
            startPage = 1;
            endPage = totalPages;
        } else {
            // more than 10 total pages so calculate start and end pages
            if (currentPage <= 6) {
                startPage = 1;
                endPage = 10;
            } else if (currentPage + 4 >= totalPages) {
                startPage = totalPages - 9;
                endPage = totalPages;
            } else {
                startPage = currentPage - 5;
                endPage = currentPage + 4;
            }
        }

        // calculate start and end item indexes
        var startIndex = (currentPage - 1) * pageSize;
        var endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);

        // create an array of pages to ng-repeat in the pager control
        var pages = _.range(startPage, endPage + 1);

        // return object with all pager properties required by the view
        return {
            totalItems: totalItems,
            currentPage: currentPage,
            pageSize: pageSize,
            totalPages: totalPages,
            startPage: startPage,
            endPage: endPage,
            startIndex: startIndex,
            endIndex: endIndex,
            pages: pages
        };
    }

    render() {
        var pager = this.state.pager;

        if (!pager.pages || pager.pages.length <= 1) {
            // don't display pager if there is only 1 page
            return null;
        }

        return (
            <ul className="pagination">
                <li className={pager.currentPage === 1 ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(1)}>First</a>
                </li>
                <li className={pager.currentPage === 1 ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(pager.currentPage - 1)}>Previous</a>
                </li>
                {pager.pages.map((page, index) =>
                    <li key={index} className={pager.currentPage === page ? 'active' : ''}>
                        <a onClick={() => this.setPage(page)}>{page}</a>
                    </li>
                )}
                <li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(pager.currentPage + 1)}>Next</a>
                </li>
                <li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(pager.totalPages)}>Last</a>
                </li>
            </ul>
        );
    }
}

Pagination.propTypes = propTypes;
Pagination.defaultProps = defaultProps;
export default Pagination;


React App Component that uses the Pagination Component

An example react component that uses the above pagination component to paginate a list of 150 example items that are generated using underscore.js.

import React from 'react';
import Pagination from './Pagination';

class App extends React.Component {
    constructor() {
        super();

        // an example array of items to be paged
        var exampleItems = _.range(1, 151).map(i => { return { id: i, name: 'Item ' + i }; });

        this.state = {
            exampleItems: exampleItems,
            pageOfItems: []
        };

        // bind function in constructor instead of render (https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md)
        this.onChangePage = this.onChangePage.bind(this);
    }

    onChangePage(pageOfItems) {
        // update state with new page of items
        this.setState({ pageOfItems: pageOfItems });
    }

    render() {
        return (
            <div>
                <div className="container">
                    <div className="text-center">
                        <h1>React - Pagination Example with logic like Google</h1>
                        {this.state.pageOfItems.map(item =>
                            <div key={item.id}>{item.name}</div>
                        )}
                        <Pagination items={this.state.exampleItems} onChangePage={this.onChangePage} />
                    </div>
                </div>
                <hr />
                <div className="credits text-center">
                    <p>
                        <a href="http://jasonwatmore.com" target="_top">JasonWatmore.com</a>
                    </p>
                </div>
            </div>
        );
    }
}

export default App;


Web Developer Sydney

Feel free to drop me a line if you're looking for a React developer or web developer in Sydney Australia, I also provide remote contracting services for clients outside Sydney.


Sponsored by