react_pagination_banner

React on Rails Tutorial: Part 3 – Pagination

   Back to list

Some of you requested to write an episode about React pagination, so here it is! This is the third episode of the “React on Rails” series, which covers integrating a Ruby on Rails back-end and a ReactJS front-end. The first two parts can be found here:

React on Rails Tutorial: Part 1
React on Rails Tutorial: Part 2

In this article we’ll add simple pagination, similar to the will_paginate gem. You’ll be able to paginate the events based on the amount of events on a page.

What’s more, this small component is designed to be flexible and can be reused in any part of your application. It only requires: a page, the total number of pages, and an onClick event function in parameters. Let’s get started!

Prepare

The source code from the previous parts can be found here. To download it, run:

$ git clone https://github.com/nopio/react_example
$ git reset --hard 8a6fa34

First, edit the seed.rb file, which will create a lot of events!

1.upto(5_000) do |i|
  Event.create(name: "Event #{i}",
               description: "It's sample event with number #{i}",
               event_date: Date.today + rand(3).months,
               place: "Random place number #{i}")
end

Next, run:

$ rake db:seed

Add core methods

Now we need to add methods which are responsible for React pagination to the Event class. To do so, edit the event.rb file and add:

class Event < ApplicationRecord
  validates :name, :place, :description, :event_date, presence: true

  class << self
    def per_page
      10
    end

    def pages(per_page = self.per_page)
      pages = count / per_page.to_f
      pages += 1 if pages % 1 > 0
      pages.to_i
    end

    def paginate(page: 1, per_page: self.per_page)
      page = page.to_i
      per_page = per_page.to_i

      offset = (page - 1) * per_page
      limit(per_page).offset(offset)
    end
  end
end

The first method – per_page – defines the number of records which are displayed on a page.
The second method returns the amount of pages based on the number of records. It has a default parameter set to the predefined number of pages from the per_page method.
The last method returns records based on the page number and the number of records per page. It can be run like this:
Event.paginate -> returns 10 events from the first page
Event.paginate(page: 3) -> returns 10 events from the third page
Event.paginate(page: 3, per_page: 30) -> returns 30 events from the third page

We need to modify and use the newly added methods in the EventsController. The index method will now return a JSON with a page number, the total number of pages, and all of the events from the paginate function. We will also add in the page function here. Don’t forget about paginating events in the search method:

module Api
  class EventsController < ApplicationController
    before_action :set_event, only: [:update, :destroy]
    
    def index
      render json: {
        events: Event.paginate(page: page)
                     .order(sort_by + ' ' + order),
        page: page,
        pages: Event.pages
      }
    end    
    ...      
    def search
      query = params[:query]
      events = Event.where('name LIKE ? OR place LIKE ? OR description LIKE ?',
                           "%#{query}%", "%#{query}%", "%#{query}%")
                    .paginate(page: page)
      render json: events
    end    
    ...      
    private    
    ...      
    def page
      params[:page] || 1
    end
  end
end

React Pagination

Let’s add the Pagination component. Add the pagination.js.jsx file under the javascripts/components:

var Pagination = React.createClass({
  render: function() {
    return(
      <div className="text-center">
        <ul className="pagination">
          <li className="active"><a onClick={this.props.handleChangePage.bind(null, 1)}>1</a></li>
          <li><a onClick={this.props.handleChangePage.bind(null, 2)}>2</a></li>
          <li><a onClick={this.props.handleChangePage.bind(null, 3)}>3</a></li>
          <li><a onClick={this.props.handleChangePage.bind(null, 4)}>4</a></li>
          <li><a onClick={this.props.handleChangePage.bind(null, 5)}>5</a></li>
        </ul>
      </div>
    );
  }
});

It simply renders Bootstrap’s pagination with additional an onClick event – handleChangePage which is passed from a parent.
Now let’s modify the EventApplication component. We need to add the handleChangePage function, add pages and page attributes to the getInitialState, update their state in both ajax responses, and render the Pagination component inside the render function:

var EventApplication = React.createClass({
  getInitialState: function() {
    return { events: [],
             sort: "name",
             order: "asc",
             page: 1,
             pages: 0 };
  },
  ...
  getDataFromApi: function() {
    ...
    success: function(data) {
      self.setState({ events: data.events, pages: parseInt(data.pages), page: parseInt(data.page) });
    },
    ...
  },
  ...
  handleSortColumn: function(name, order) {
    ...
    success: function(data) {
      this.setState({ events: data.events, sort: name, order: order });
    }.bind(this),
    ...
  },
  handleChangePage: function(page) {
    console.log(page);
  },
  render: function() {
    ...
      <div className="col-md-12">
        <EventTable ... />
        <Pagination page={this.state.page}
                    pages={this.state.pages}
                    handleChangePage={this.handleChangePage} />
      </div>
    ...
  }
});

Now everything is ready. We can check if our React pagination is displaying on the main page and that the events are limited to 10 per page. Also, we can check the JavaScript console, to see if by clicking on a page number, it logs it in the console.

react_rails_tutorial_screen1

Real pagination

When everything is working the way we want, we can start to rewrite the Pagination component to display real pagination and change pages.

var Pagination = React.createClass({
  paginationElement: function(number) {
    return (
      <li key={'page' + number}
          className={number == this.props.page ? 'active' : ''}>
        <a onClick={this.props.handleChangePage.bind(null, number)}>{number}</a>
      </li>
    )
  },
  render: function() {
    var self = this;
    var page = this.props.page;
    var last_page = this.props.pages;
    var page_links = [];
    var max_elements = 2;
    var pages = [1];

    for (var i = page - max_elements; i <= page + max_elements; i++) {
      if (!pages.includes(i))
        pages.push(i);
    }
    if (!pages.includes(last_page))
      pages.push(last_page);

    pages.forEach(function(i) {
      if (i > 0 && i <= last_page)
        page_links.push(self.paginationElement(i));
    });

    return(
      <div className="text-center">
        <ul className="pagination">
          {page_links}
        </ul>
      </div>
    );
  }
});

The paginationElement function renders a <li> element with valid pagination elements. If a number passed from a parameter is equal to a page number from props, it’s marked as active. Also we add the handleChangePage function with a dynamic parameter equal to the number. What does the render function do? It creates an array called page_links and returns the pagination elements from the paginationElements function.

Next  we need to change the EventApplication component. Add a page parameter to the getDataFromApi function, pass it in the ajax request and change the handleChangePage function to call the getDataFromApi function instead of logging a page number in the console:

var EventApplication = React.createClass({
  ...
  componentDidMount: function() {
    this.getDataFromApi(this.state.page);
  },
  getDataFromApi: function(page) {
    ...
    $.ajax({
      url: '/api/events',
      data: { page: page },
      ...
    });
  },
  ...
  handleChangePage: function(page) {
    this.getDataFromApi(page);
  },
  ...
});

Let’s check if the code we’ve just added works.

react_rails_tutorial_screen2

Yeah, it definitely works!

Code cleanups

When everything works as expected, we can remove code which is not needed.
Modify the NewForm form component. In the success response, we don’t need to pass  data in the handleAdd function:

var NewForm = React.createClass({
  ...
  handleAdd: function(e) {
    ...
    success: function(data) {
      self.props.handleAdd();
      self.setState(self.getInitialState());
    },
    ...
  },
  ...
});

Let’s change the EventTable, and remove the passed parameters in the handleDeleteRecord functions:

var EventTable = React.createClass({
  handleDeleteRecord: function() {
    this.props.handleDeleteRecord();
  },
  ...
});

Now refactor the Event component, and remove unneeded parameters in the handleDeleteRecord function:

var Event = React.createClass({
  ...
  handleDelete: function(e) {
    ...
      success: function(data) {
        this.props.handleDeleteRecord();
      }.bind(this),
    ...
  },
  ...
});

The last thing we can change is the EventApplication component. Modify the handleAdd and handleDeleteRecord functions – remove the unneeded passed parameters and change their body to only call the getDataFromApi function with a page number:

var EventApplication = React.createClass({
  ...
  handleAdd: function() {
    this.getDataFromApi(this.state.page);
  },
  handleDeleteRecord: function() {
    this.getDataFromApi(this.state.page);
  },
  ...
});

Conclusion

We hope that you liked this episode of the series “React On Rails”. In this part we covered a simple React pagination using pure JavaScript without any extra libraries. You can find the full source code here. If you like our blog and the content we publish there, feel free to subscribe to our newsletter!

8 thoughts on “React on Rails Tutorial: Part 3 – Pagination”

  1. Hello Piotr
    I get this error and nothing is displayed :
    ActionController::RoutingError (No route matches [GET] “/assets/components/sort_column.js-fe269aba988f1838e1962426dced8967da3186e127045aecdc45bf7608561912.jsxsort_column.js.jsxsort_column.js.jsxsort_column.js.jsxsort_column.self.js”):

    any clue?

    Thank you for your tutorials and effort.

  2. Very helpful tutorial, thank you! One caveat: The sorting and pagination are not connected. If you sort, then change page, the sorting is reset. And vice versa. It’s confusing.

Leave a Reply

Your email address will not be published. Required fields are marked *

Send this to a friend