(Re: Pagination, You're doing it wrong) - Fixing Duplicate/Missing Records in Infinite Scrolling + Pagination by Hacking the Kaminari Gem

Using pagination with JS to create an infinite scrolling page is common,

(Check out this RailsCast episode if you don't know how:
http://railscasts.com/episodes/114-endless-page-revised?view=comments
)

 But it gives rise to the problem of loading duplicate or missing records.
 (See: https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong)


So, how do you solve this?
---
I solved the problem by creating a CustomRelation model that builds on Kaminari's paginate helper.

In this example, I am creating an infinite scrolling page of 'posts', akin to Facebook. With the Kaminari gem, its 'paginate' helper accepts ActiveRecord::Relation, which should respond to these 3 methods (this, I refer to the  Kir Shatrov's post, see bottom):
  • current_page - returns current page number
  • total_pages - returns total number of pages
  • all - returns an array of rows for the current page
So, instead of using Kaminari's default Relation, I created a CustomRelation model, like so:

 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
class CustomRelation
    def initialize(user_id, page, last_id)
      @user_id = user_id
      page = page || 1
      @page = page.to_i
      @limit = 20
      @last_id = last_id || 99999
    end
  
    def current_page
      @page
    end
  
    def total_pages
      total = @posts.length
      (total.to_f / @limit).ceil
    end
  
    def limit_value
      @limit
    end

    def next_page
        @page < total_pages
    end
  
    def per_page
        @posts ||= Post.where("user_id = ?", @user_id)
                       .where("posts.id < ?", @last_id)
                       .order(created_at: :desc).limit(@limit)
       
    end
  
  
  end

Assuming that you followed RailsCast's method in writing the JS function, you will need to change a few things for the script:
  • find the id of the last post being displayed currently
    • the example below called #last() on the class '.post' which finds the last post, then it gets the id of the post, e.g. post_123'; and finally extracts the number part of the id, which is the actual post id.
  • when calling the url of 'next page', add the params  'last_id' = to the id of the last post being displayed
    • Note: If you did follow RailsCast which used the Pagination gem, you will have to change the method of getting the next page url. In Kaminari, you can do that by finding the link(i.e. <a>) that has [rel = next]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$(function() {

  if ($('.pagination').length) {
    $(window).scroll(function() {
      var last_id = $('.post').last().attr('id').match(/[0-9]+/)
      var url = $('.pagination a[rel=next]').attr('href')
      if (url && $(window).scrollTop() > $(document).height() - $(window).height() - 1500) {
        $('.pagination').text("Loading...")
        $.getScript(url  + '&last_id=' + last_id)
      };
    });
    $(window).scroll()
  }

})

Then, in the controller, instead of calling #page (i.e. Kaminari's helper method), create an instance of CustomRelation, passing in an additional 'last_id' params:

1
2
3
4
5
6
7
8
class PostsController < ApplicationController

  def show
      @feed = CustomRelation.new(params[:id], params[:page] , params[:last_id])
 #    @feed = Post.where('id = ?', params[:id]).page(params[:page]).per(20) <- default kaminari helper method
  end

end


Finally, I changed my views to generate partials using @feed.per_pages as the collection instead of @feed, since in CustomRelation, #per_pages fetches the records:

1
2
3
4
<div id="feed_container">
    <%= render partial: "posts/post", collection: @feed.per_page, as: :post %>
</div>
<div class="digg_pagination"><%= paginate @feed %></div>

With this all done, when the page hits the bottom and fetches more posts, the script will now access the url of next_page along with the params last_id, which then invokes the CustomRelation and finds the next 20 records that comes after 'last_id'.

Problem solved.


Reference(s):
http://iempire.ru/2015/11/08/kaminari-custom-query/

Comments

Popular posts from this blog

I18n Country & City Select Fields - Reconstructing Carmen-Rails from Scratch

Sending an Email Confirmation Link with the Right Locale (with Devise)