> For the complete documentation index, see [llms.txt](https://jho406.gitbook.io/breezy/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://jho406.gitbook.io/breezy/docs/recipes/spa-pagination.md).

# SPA (Single Page Application) Pagination

In this recipe, we'll be adding pagination that works without reloading the page.

## Starting point

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

\=== "`posts_controller.rb`" \`\`\`ruby # app/controllers/posts\_controller.rb

````
def index
  @posts = Post.all
end
```
````

\=== "`index.json.props`" !!! info In this example, we have a `sleep` that we will optimize for later

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

json.header do
  json.name "bob"
  sleep 2
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
```
````

\=== "`index.js`" !!! info Let's assume `Header` is a simple component that exist.

````
```js
// app/views/posts/index.js

import React from 'react'
import {useContent} from '@thoughtbot/superglue'
import Header from './Header'

export default PostIndex = () => {
  const {
    posts,
    header
  } = useContent()

  return (
    <>
      <Header {...header}/>
      <div>
        {
          posts.list.map(({id, body}) => (
            <p key={id}>{body}</p>
          ))
        }
      </div>
      <PostList items={posts}>
    </>
  )
}

```
````

## Add gems

Lets also add Kaminari to your gem file

```terminal
gem 'kaminari'
```

and `bundle`

## 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, both methods return `nil` if there are no subsequent pages.

!!! info Some [helpers](https://github.com/kaminari/kaminari#the-paginate-helper-method) like `paginate` output HTML instead of JSON, but we can still use more primitives methods.

\=== "`posts_controller.rb`" \`\`\`diff # app/controllers/posts\_controller.rb

````
def index
  @posts = Post.all
+   .page(params[:page_num])
+   .per(10)
+   .order(created_at: :desc)
end
```
````

\=== "`index.json.props`" \`\`\`diff # app/views/posts/index.json.props

````
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
```
````

\=== "`index.js`"

````
```diff
// app/views/posts/index.js

import React from 'react'
import {useContent} from '@thoughtbot/superglue'
import Header from './Header'

export default PostIndex = () => {
  const {
    posts,
    header,
+   pathToNextPage,
+   pathToPrevPage
  } = useContent()

  return (
    <>
      <Header {...header}/>
      <div>
        {
          posts.list.map(({id, body}) => (
            <p key={id}>{body}</p>
          ))
        }
      </div>
+     {pathToPrevPage && <a href={pathToPrevPage}>Prev Page</a>}
+     {pathToNextPage && <a href={pathToNextPage}>Next 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 the [UJS](/breezy/docs/ujs.md) helper `data-sg-visit`, which would set the current page's state to the response without changing the URL.

**`index.js`**

```diff
// app/views/posts/index.js

import React from 'react'
import {useContent} from '@thoughtbot/superglue'
import PostList from './PostList'
import Header from './Header'

export default PostIndex = () => {
  const {
    posts,
    header,
    pathToNextPage,
    pathToPrevPage
  } = useContent()

  return (
    <>
      <Header {...header}/>
      <div>
        {
          posts.list.map(({id, body}) => (
            <p key={id}>{body}</p>
          ))
        }
      </div>
-     {pathToPrevPage && <a href={pathToPrevPage}>Prev Page</a>}
+     {pathToPrevPage && <a href={pathToPrevPage} data-sg-visit>Prev Page</a>}
-     {pathToNextPage && <a href={pathToNextPage}>Next Page</a>}
+     {pathToNextPage && <a href={pathToNextPage} data-sg-visit>Next Page</a>}
    </>
  )
}
```

## Optimize!

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

!!! info In effect, this achieves the same functionality as [Turbo Frames](https://turbo.hotwired.dev/handbook/frames), but Superglue leans more on Unobtrusive Javascript for better ergonomics.

**`index.json.props`**

Recall how [digging](/breezy/docs/tutorial.md#digging-for-content) for content works. We'll add a `props_at` that digs for the `json.posts` while skipping other content on that page.

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

json.header 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
```

\- \[:octicons-arrow-right-24: Interested in infinite-scroll?]\(./infinite-scroll.md) for \`visit\`
