As you've probably guessed by the title of my article, I still consider Ruby on Rails as a relevant technology that offers a lot of value, especially when combined with ReactJS as it's frontend counterpart. Here's how I approach the topic.
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.
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.
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!