Inertia.js Pagination: Keeping Local State and Scroll Without a Client Store
Inertia.js Link components support preserveState and preserveScroll to keep local component state and scroll position across server-driven pagination visits, avoiding full reloads without a client-side store.
28 Apr 2026, 14:20 UTC

Server-driven pagination often feels like a step backward in a single-page application. You build a filtered list, click to page two, and the whole browser reloads—or the page component remounts and your local search input resets. If you reach for a client-side state library just to keep a filter box populated across pages, you are solving the wrong problem. Inertia.js offers a narrower, declarative fix: pass preserveState and preserveScroll to its Link component.
The remount problem
When you use a standard HTML anchor tag, the browser performs a full document request. Inertia.js intercepts those clicks and issues an XHR (an asynchronous HTTP request) with an X-Inertia header, expecting a JSON page object back. That avoids the full reload, but by default the JavaScript framework still unmounts and remounts the page component. Any local state—open dropdowns, unsent form values, active tab index—disappears.
Declarative state and scroll preservation
Inertia's Link component accepts two props that change this behavior without a custom store:
preserveState: keeps the current page component mounted instead of destroying and recreating it.preserveScroll: restores the window scroll offset after the new page content arrives.
Both apply only to Inertia visits initiated through Link or router.visit. A plain <a> tag ignores them and triggers a normal browser navigation.
A worked pagination example
Imagine a backend that returns a paginator object as an Inertia prop. The frontend renders a list and pagination controls. Without preservation, clicking "Next" resets a local search filter and jumps to the top of the page.
Here is an illustrative React example. It assumes your server sends a users prop shaped like a common paginator (with data, links, and metadata). Adapt the prop names to your backend:
import { Link } from '@inertiajs/react'
import { useState } from 'react'
export default function Index({ users }) {
const [filter, setFilter] = useState('')
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter locally..."
/>
<ul>
{users.data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<nav>
{users.links.map((link, i) => (
link.url ? (
<Link
key={i}
href={link.url}
preserveState
preserveScroll
>
<span dangerouslySetInnerHTML={{ __html: link.label }} />
</Link>
) : (
<span key={i} disabled>
<span dangerouslySetInnerHTML={{ __html: link.label }} />
</span>
)
))}
</nav>
</div>
)
}
With preserveState, the filter value survives because React does not unmount the component. With preserveScroll, the viewport stays near the pagination controls instead of snapping to the top.
Trade-offs and sharp edges
These props are useful, but they are not a universal replacement for state management.
Hard refreshes still reset everything
preserveState only guards against component remounting during an Inertia visit. A browser refresh destroys the JavaScript context entirely. If a user reloads the page, the local filter is gone. If that data matters, persist it to the URL query string or a backend session instead.
Prop shape mismatches can break preserved components
Because the component stays mounted, it may hold references to keys that no longer exist in the new server response. For example, if page one returns users.data[0].email and page two omits that field, code that assumed email would throw. When prop shapes vary across pages, either normalize the backend response or let the component remount by omitting preserveState.
Scroll behavior is adapter-sensitive
Default scroll management can differ between Inertia versions and between the React and Vue adapters. Browser back/forward navigation is especially prone to variation. Do not assume preserveScroll behaves identically in every environment; test it explicitly.
Overuse masks legitimate resets
Passing preserveState on every link makes the UI feel inconsistent. A "Create New" link probably should reset the page to a blank form. Reserve preservation for navigation where continuity is expected—typically pagination and tabbed filters.
How to verify it is working
Open your browser's developer tools and watch the Network tab while clicking a pagination link. You should see an XHR request carrying an X-Inertia header. The response should be a JSON page object, not an HTML document. If you see a full HTML response, the link was not handled as an Inertia visit—check that you are using the Inertia Link component and not a raw anchor tag.
To confirm scroll preservation, place the pagination controls low on a long list, click to the next page, and observe whether the viewport stays near the controls. To confirm state preservation, type into a local input before navigating; the text should remain after the new page arrives.
Because scroll behavior and adapter APIs can shift between releases, confirm the exact prop names and defaults against the Inertia.js documentation for your installed version before relying on this in production.
Bottom line
Use preserveState and preserveScroll on Inertia Link components when you want server-driven pagination to feel like a native SPA. Keep the scope narrow: apply them to pagination and similar continuous navigation, not to every link. Verify the X-Inertia header in devtools, test back/forward behavior in your target adapters, and fall back to URL-based state when data must survive a hard refresh.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.