Skip to content

Easy SPA Pagination

Pagination without reload is easy to add.

Starting point

Lets pretend that we're already able to see a list of posts.

# app/controllers/posts_controller.rb

def index
  @posts = Post.all
end
# app/views/posts/index.json.props

json.rightNav do
  ...
end

json.posts do
  json.list do
    json.array! @posts do |post|
      json.id post.id
      json.body post.body
      json.editPostPath edit_post_path(post)
    end
  end
end
# app/views/posts/index.js

import React from 'react'
import PostList from './PostList'
import RightNav from './RightNav'

export default PostIndex = ({
  posts,
  rightNav
}) => {
  return (
    <>
      <RightNav {...rightNav}>
      <PostList items={posts}>
    </>
  )
}

Add gems

Lets also add Kaminari.

bundle install kaminari

Add pagination

The changes here are almost same with the .erb counterpart. We're using path_to_next_page and path_to_prev_page which come with Kaminari.

Info

Some helpers like paginate output HTML instead of JSON, but we can still use more primitives methods.

# app/controllers/posts_controller.rb

def index
  @posts = Post.all
+   .page(params[:page_num])
+   .per(10)
+   .order(created_at: :desc)
end
# app/views/posts/index.json.props

json.rightNav do
  ...
end

json.posts do
  json.list do
    json.array! @posts do |post|
      json.id post.id
      json.body post.body
      json.editPostPath edit_post_path(post)
    end
  end
+
+ json.pathToNextPage path_to_next_page(@posts)
+ json.pathToPrevPage path_to_prev_page(@posts)
end
# app/views/posts/index.js
import React from 'react'
import PostList from './PostList'
import RightNav from './RightNav'

export default PostIndex = ({
  posts,
  rightNav
+  pathToNextPage,
+  pathToPrevPage
}) => {
  return (
    <>
      <PostList items={posts}>
+     <a
+       href={pathToNextPage}
+     >
+       Next Page
+     </a>
+     <a
+       href={pathToPrevPage}
+     >
+       Prev Page
+     </a>
    </>
  )
}

Smooth navigation

The above adds pagination, but each click on Next Page is a new page load.

Lets navigate without a reload. In this example, we're using data-sg-remote, which would set the current page's state to the response without changing the URL.

index.js

# app/views/posts/index.js
import React from 'react'
import PostList from './PostList'
import RightNav from './RightNav'

export default PostIndex = ({
  posts,
  rightNav,
  pathToNextPage,
  pathToPrevPage
}) => {
  return (
    <>
      <PostList items={posts}>
      <a
        href={pathToNextPage}
+       data-sg-remote
      >
        Next Page
      </a>
      <a
        href={pathToPrevPage}
+       data-sg-remote
      >
        Prev Page
      </a>
    </>
  )
}

Optimize!

Lets skip data.rightNav when navigating and dig for data.posts. For the user, only the posts lists change, but the rightNav stays the same.

Info

In effect, this achieves the same functionality as Turbo Frames, but Superglue leans more on Unobtrusive Javascript and a simple props_at for better ergonomics.

index.json.props

Recall how digging for content works. We'll add a props_at that digs for the json.posts while skipping other content on that page.

# app/views/posts/index.json.props

json.rightNav do
  ...
end

json.posts do
  json.list do
    json.array! @posts do |post|
      json.id post.id
      json.body post.body
      json.editPostPath edit_post_path(post)
    end
  end

- json.pathToNextPage path_to_next_page(@posts)
+ json.pathToNextPage path_to_next_page(@posts, props_at: 'data.posts')
- json.pathToPrevPage path_to_prev_page(@posts)
+ json.pathToPrevPage path_to_prev_page(@posts, props_at: 'data.posts')
end