Pagination in InertiaJS: Letting Rails Drive the List
Skip custom data hooks. Use Rails to paginate, render an Inertia response, and let the client router swap components without full reloads.
06 Jul 2026, 18:30 UTC

Problem: Pagination that stays crawlable but feels like an SPA
You have a Rails CRUD list and you need pagination. You want search engines to see every page of content, but you also want clicking “Next” to update the URL and swap the list without a full browser reload. InertiaJS handles this by keeping your Rails controller as the source of truth: the server paginates the records, renders the initial HTML for crawlers, and then answers subsequent client-side requests with a lightweight JSON payload that Inertia uses to swap components.
How Inertia requests actually work
Inertia is not a data-fetching library with custom hooks. It is a response protocol. On the first visit to /posts, Rails returns a normal HTML document containing the rendered list. When a user clicks a pagination link, Inertia’s client router sends a GET request with an X-Inertia header. Rails detects that header and returns JSON instead of HTML. The JSON contains the next component name, its props, and the new URL. Inertia then swaps the page component and updates the address bar through the History API. The URL changes, but the browser does not perform a full page reload.
Rails controller as the pagination engine
Because the server owns the data, pagination logic stays in Rails. Using the inertia_rails gem, you render an Inertia response rather than plain JSON or a template. The example below uses Kaminari, though Pagy or WillPaginate work similarly.
class PostsController < ApplicationController
def index
posts = Post.order(created_at: :desc)
.page(params[:page])
.per(10)
render inertia: {
component: "Posts/Index",
props: {
posts: posts.as_json(only: [:id, :title, :published_at]),
pagination: {
current_page: posts.current_page,
total_pages: posts.total_pages,
next_page: posts.next_page,
prev_page: posts.prev_page
}
}
}
end
end
Run this in your Rails controller. The page param arrives from the query string just like a classic Rails paginated action. No separate API endpoint is required.
Client-side navigation with the Inertia router
On the client, you do not call a fetch hook. You use Inertia’s Link component or the router helper to visit the next URL. Both approaches trigger the same protocol: an XHR request with the X-Inertia header, followed by a component swap.
Here is a React page component that renders the list and pagination controls.
import { Link } from "@inertiajs/react"
export default function Index({ posts, pagination }) {
return (
<div>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<nav aria-label="Pagination">
{pagination.prev_page ? (
<Link
href={`/posts?page=${pagination.prev_page}`}
preserveScroll
>
Previous
</Link>
) : null}
<span>
Page {pagination.current_page} of {pagination.total_pages}
</span>
{pagination.next_page ? (
<Link
href={`/posts?page=${pagination.next_page}`}
preserveScroll
>
Next
</Link>
) : null}
</nav>
</div>
)
}
The preserveScroll option keeps the browser scroll position after the swap, which matters on long lists. If you need to preserve component state instead, Inertia also offers preserveState, but use it deliberately: it can mask bugs when stale local state hides fresh server props.
Verifying the behavior
Open your browser’s Network tab. The first load to /posts should show a request of type document. Clicking “Next” should show a new request with the X-Inertia header set to true; the response body is JSON containing component and props. View the page source on the first load: the <ul> and list items should be present in the raw HTML, confirming that crawlers receive the content without executing JavaScript. In your Rails log, you should see the page parameter arrive on each request.
Trade-offs and limitations
- Initial payload size. The first HTML response includes the full Rails layout and the serialized props for the first page. On very slow connections this is heavier than a bare JSON API response, though it is the same cost as any classic Rails page.
- No client-side cache. Inertia re-requests from the server on every navigation. If you need offline caching or optimistic UI, you must build that yourself; the server remains the source of truth.
- State resets. Component state is discarded on navigation unless you use
preserveState. Open modals, unsaved form fields, or scroll-dependent UI must be managed explicitly. - SSR is optional. Standard Inertia already gives you server-rendered HTML from Rails. If you additionally want to render your React or Vue components on the server (true SSR), that requires the separate
@inertiajs/serverpackage and extra configuration.
Next steps
Start by adding inertia_rails to your Rails 7 application and rendering an Inertia response from your index action. Build a page component that accepts posts and pagination as props, then wire the controls with Link. Verify the X-Inertia header in the Network tab and confirm the initial HTML contains the list markup. If the first payload feels too large, reduce the per page count or defer secondary associations rather than abandoning the server-driven model.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.