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
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 :
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.