Post

Rails 7 pagination with Kaminari tutorial

Intro : bloated data

Displaying bulk of data in a single page is not a good practice because it slows down our rails application and will decrease the performance of the database server.

Displaying long lists has a significant performance impact on both front-end and back-end:

  • First, the database reads a lot of data from disk.

  • Second, the result of the query is converted to Ruby objects, which increases memory allocation.

  • Third, large responses take longer to send to the user’s browser.

  • As a result, displaying long lists can cause the browser to freeze.

Adding pagination

The simplest and most common way to solve this problem is to add pagination to our rails applications in order to limit the number of records we want to show in a single page. Going further, we can specify the required number of records per page and customise how the pagination will be displayed.

Pagination divides data into equal parts (pages). On the first visit, the user only gets a limited number of items (page size). The user can see more items as they page forward thus sending a new HTTP request and a new database query.

Offset-based pagination is the simplest and the most common way to paginate over records. It uses the SQL LIMIT and OFFSET clauses to extract a certain piece from a table.

Example database query:

SELECT * FROM records LIMIT [Number to Limit By] OFFSET [Number of rows to skip];
SELECT * FROM posts LIMIT 20 OFFSET 10;

Rails answer

There are several popular pagination libraries used by the Rails community that you can use. In this post, we’ll look at how to add pagination with Kaminari gem. I suggest using this gem to paginate our blog posts so that we can manage each page.

What we want is an easy way to show only a certain number of posts per page and let the user view them page by page. The gem itself is well tested and its integration is quite easy, so there is no need to create any specs to test pagination. Also it provides handy helper methods to implement pagination on ActiveRecord queries and customisation helpers.

Demo : create a new Rails app

So let’s start a tutorial from scratch.

Tool I will use in this tutorial :

1
2
3
ruby -v  # 3.3.0
bundle -v  # 2.4.10
node -v # 20.9.0

Then let’s go at the root of your usual workspace, and start to build a fresh new Rails app :

1
2
3
4
5
mkdir pagination && cd pagination  
echo "source 'https://rubygems.org'" > Gemfile  
echo "gem 'rails', '7.1.3'" >> Gemfile  
bundle install  
bundle exec rails new . --force

Notice here that I don’t use the --minimal flag - see options here. It would remove Hotwire, Stimulus, and so on - the collaboration between the browser and the server would be then different. I want to stick to plain old Rails (which now includes Hotwire) in this tutorial.

Create database

Let’s create an “Article”

1
bin/rails g model Article title:string body:text --no-test-framework --no-timestamps

Then create the database :

1
bin/rails db:create db:migrate

Seed data

Pagination means a lot of data to handle, so let’s create it manually this time - we could also use the faker gem next time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# inside db/seed.rb file
articles = Article.create([
  { title: "Apple", body:"Lorem ipsum" },
  { title: "Watermelon", body:"Lorem ipsum" },
  { title: "Orange", body:"Lorem ipsum" },
  { title: "Pear", body:"Lorem ipsum" },
  { title: "Cherry", body:"Lorem ipsum" },
  { title: "Strawberry", body:"Lorem ipsum" },
  { title: "Nectarine", body:"Lorem ipsum" },
  { title: "Grape", body:"Lorem ipsum" },
  { title: "Mango", body:"Lorem ipsum" },
  { title: "Blueberry", body:"Lorem ipsum" },
  { title: "Pomegranate", body:"Lorem ipsum" },
  { title: "Plum", body:"Lorem ipsum" },
  { title: "Banana", body:"Lorem ipsum" },
  { title: "Raspberry", body:"Lorem ipsum" },
  { title: "Mandarin", body:"Lorem ipsum" },
  { title: "Jackfruit", body:"Lorem ipsum" },
  { title: "Papaya", body:"Lorem ipsum" },
  { title: "Kiwi", body:"Lorem ipsum" },
  { title: "Pineapple", body:"Lorem ipsum" },
  { title: "Lime", body:"Lorem ipsum" },
  { title: "Lemon", body:"Lorem ipsum" },
  { title: "Apricot", body:"Lorem ipsum" },
  { title: "Grapefruit", body:"Lorem ipsum" },
  { title: "Melon", body:"Lorem ipsum" },
  { title: "Coconut", body:"Lorem ipsum" },
  { title: "Avocado", body:"Lorem ipsum" },
  { title: "Peach", body:"Lorem ipsum" },
  { title: "Anise", body:"Lorem ipsum" },
  { title: "Basil", body:"Lorem ipsum" },
  { title: "Caraway", body:"Lorem ipsum" },
  { title: "Coriander", body:"Lorem ipsum" },
  { title: "Chamomile", body:"Lorem ipsum" },
  { title: "Daikon", body:"Lorem ipsum" },
  { title: "Dill", body:"Lorem ipsum" },
  { title: "Fennel", body:"Lorem ipsum" },
  { title: "Lavender", body:"Lorem ipsum" },
  { title: "Cymbopogon", body:"Lorem ipsum" },
  { title: "Lemongrass", body:"Lorem ipsum" },
  { title: "Marjoram", body:"Lorem ipsum" },
  { title: "Oregano", body:"Lorem ipsum" },
  { title: "Parsley", body:"Lorem ipsum" },
  { title: "Rosemary", body:"Lorem ipsum" },
  { title: "Thyme", body:"Lorem ipsum" },
  { title: "Alfalfa", body:"Lorem ipsum" },
  { title: "Azuki", body:"Lorem ipsum" },
  { title: "Sprouts", body:"Lorem ipsum" },
  { title: "Black beans", body:"Lorem ipsum" },
  { title: "Black-eyed", body:"Lorem ipsum" },
  { title: "Borlottibean", body:"Lorem ipsum" },
  { title: "Broadbeans", body:"Lorem ipsum" },
  { title: "Chickpeas", body:"Lorem ipsum" },
  { title: "Garbanzos", body:"Lorem ipsum" },
  { title: "Kidney", body:"Lorem ipsum" },
  { title: "Lentils", body:"Lorem ipsum" },
  { title: "Limabeans", body:"Lorem ipsum" },
  { title: "Butterbeans", body:"Lorem ipsum" },
  { title: "Mungbeans", body:"Lorem ipsum" },
  { title: "Navybeans", body:"Lorem ipsum" },
  { title: "Peanuts", body:"Lorem ipsum" },
  { title: "Pintobeans", body:"Lorem ipsum" },
  { title: "Runnerbeans", body:"Lorem ipsum" },
  { title: "Splitpeas", body:"Lorem ipsum" },
  { title: "Soybeans", body:"Lorem ipsum" },
  { title: "Peas", body:"Lorem ipsum" },
])

Then launch in the console :

1
bin/rails db:seed

The bare minimum files

So we need a controller, a view, and a route to see what is happening.

The controller is generated as follow :

1
bin/rails g controller Articles index --no-test-framework

That fetches all required articles:

1
2
3
4
5
6
7
class ArticlesController < ApplicationController

  def index
    @articles = Article.all
  end

end

Then the route :

1
2
3
4
Rails.application.routes.draw do
  get "articles/index"
  root "articles#index"
end

And the view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# inside views\articles\index.html.erb

<h1>Articles</h1>
<% @articles.each do |article| %>
  <p style="margin-top: 2rem;">
    <strong>Title:</strong>
    <%= article.title %>
  </p>

  <p>
    <strong>Content:</strong>
    <%= article.body %>
  </p>
<% end %>

Result without pagination

As stated in the beginning of the article, there are too much data in the view layer - and for our dear user, too.

Launch your local web server :

1
bin/rails server

And open your browser at localhost:3000

All articles on one page
All articles on one page

Add Kaminari

Open your Gemfile, and add :

1
gem "kaminari"

And open your command line.

1
$> bundle install

After running bundle we can generate a configuration file for Kaminari.

## Configure Kaminari

1
2
bin/rails g kaminari:config
# => create  config/initializers/kaminari_config.rb

The path to find the file is config/initializers. Let’s look inside:

1
2
3
4
5
6
7
8
9
10
11
# inside config\initializers\kaminari_config.rb
Kaminari.configure do |config|
  # config.default_per_page = 25
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
end

Add some pagination

These are the defaults. For example, default_per_page sets up how many results will be in each page. We can change that to required number and uncomment to apply changes.

1
2
3
4
# inside config\initializers\kaminari_config.rb
Kaminari.configure do |config|
  config.default_per_page = 5
# ...

Don’t forget to restart your Rails server when you change this config.

Let’s move into the controller:

1
2
3
4
5
6
class ArticlesController < ApplicationController
  # ...
  def index
    @articles = Article.order(created_at: :desc).page(params[:page])
  end
end

And the view :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div style="font-size: 2rem;">
  <%= paginate @articles %>
</div>

<h1>Articles</h1>

<% @articles.each do |article| %>
  <p style="margin-top: 2rem;">
    <strong>Title:</strong>
    <%= article.title %>
  </p>

  <p>
    <strong>Content:</strong>
    <%= article.body %>
  </p>
<% end %>

Notice I added some raw CSS styles to highlight concepts.

The result should be as follow :

Pagination
Pagination

Further possibilities

We only scratched the surface here, but at least you have all the skeleton needed to discover all possibilities of the kaminari gem.

Don’t try to build a pagination feature yourself before to read the whole documentation on Kaminari GitHub’s repository.

You may miss some nice helpers like page_entries_info, I let you search what it actually is :)

Conclusion

Pagination is a very common problem of web application. I guess it has to be reconsidered since the raise of Hotwire, but I hope this tutorial helped you to gain confidence with large dataset.

This post is licensed under CC BY 4.0 by the author.