skip to content

An Inertia invoices table jumps to the top and clears its search box on each page change; which visit options fix this?

level: middleimportance: should knowfreq 44%

answer

  1. scroll resets by default on every visit
  2. preserveScroll keeps the position
  3. preserveState keeps the component instance
  4. 'errors' or a callback for conditional
  5. scroll-region for overflow containers

basics

~20 s

Inertia resets scroll and remounts the page component on each GET visit. preserveScroll keeps the scroll position, preserveState keeps local state such as the search input, replace avoids a history entry per page, and a scrollable container needs a scroll-region attribute.

solid answer

~50 s

By default a GET visit creates a fresh page component instance, so local state such as the search input's value and focus is lost, and Inertia scrolls the document, plus any element marked `scroll-region`, back to the top. For the table, pass `preserveScroll` so the position stays and `preserveState` so the same component instance receives the new props: `router.get('/invoices', { search, page }, { preserveState: true, preserveScroll: true, replace: true })`. On a pagination `<Link>` the same props apply. `replace` swaps the current history entry instead of adding one per keystroke or page. Both preserve options also accept `'errors'` or a callback receiving the new page, to keep state only in some outcomes. If the table scrolls inside an `overflow-y-auto` container rather than the document, add the `scroll-region` attribute to that element, or Inertia neither resets nor restores it.

code

jsx · 23 lines
jsx
import { Link, router } from '@inertiajs/react'
import { useState } from 'react'

export default function Invoices({ invoices, filters }) {
  const [search, setSearch] = useState(filters.search ?? '')

  function onSearch(value) {
    setSearch(value)
    router.get('/invoices', { search: value }, {
      preserveState: true, preserveScroll: true, replace: true,
    })
  }

  return (
    <main scroll-region="" className="overflow-y-auto">
      <input value={search} onChange={(e) => onSearch(e.target.value)} />
      <InvoiceTable rows={invoices.data} />
      {invoices.links.map((link) => link.url && (
        <Link key={link.label} href={link.url} preserveScroll preserveState>{link.label}</Link>
      ))}
    </main>
  )
}

go deeper

for a junior

Recall that preserveScroll keeps the scroll position and preserveState keeps local component state across a visit.

for a middle

Explain the defaults per method, the 'errors' and callback forms, replace for search-as-you-type, and the scroll-region attribute.

for a senior

Diagnose stale preserved state and container scrolling in persistent layouts, and apply the options only where they match user expectations.

for a principal

Define navigation conventions for list pages, such as URL-driven filters and scroll rules, so every table behaves the same.

## The two defaults behind the symptom An invoices page with a search box and a paginated table shows two problems on every page change, and each comes from a default: 1. **Scroll reset.** Like a browser loading a new document, Inertia scrolls the document to the top after a visit and resets every element marked as a scroll region. On forward and back navigation it restores the positions it saved. 2. **Fresh component.** A `GET` visit to the same page creates a new instance of the page component, so local state such as an input's value, focus and open dropdowns starts over, even though the component name has not changed. ## The options | Option | Default for GET visits | What `true` does | |---|---|---| | `preserveScroll` | `false` | keeps the document and scroll-region positions | | `preserveState` | `false` | keeps the page component instance, so local state survives and only props change | | `replace` | `false` | replaces the current history entry instead of pushing one | The preserve options accept more than booleans: - `'errors'` preserves only when the new page has validation errors. - A callback receives the new page, e.g. `preserveScroll: (page) => page.props.invoices.current_page > 1`. Defaults differ by method: `router.post`, `put`, `patch`, `delete` and `router.reload` set `preserveState: true`, `reload` also sets `preserveScroll: true`, and a `<Link>` with a non-GET method defaults `preserveState` to `true`. A visit to the current URL sets `replace` automatically. ## Applying them to the table - **Pagination links**: `<Link href={link.url} preserveScroll preserveState>` so the table stays under the user's eyes and the search box keeps focus and text. - **Search as you type**: debounce the input and call `router.get('/invoices', { search }, { preserveState: true, preserveScroll: true, replace: true })`; `replace` stops each keystroke creating a Back step. - **Only the rows**: add `only: ['invoices']` so the filters and other props are not recomputed; that option belongs to partial reloads. ## When the table scrolls inside a container Many admin layouts scroll a `<main class="overflow-y-auto">` element, not the document. Inertia only manages the document and elements carrying a `scroll-region` attribute: - Without the attribute, Inertia neither resets nor restores the container. A container inside a persistent layout keeps its old position when you open another page, and Back does not restore it. - With `<main scroll-region="" className="overflow-y-auto">`, Inertia saves the region's position in history, resets it on normal visits, restores it on Back, and leaves it alone when `preserveScroll` is set. ## Pitfalls - `preserveState: true` means the component is not remounted, so state initialised from props, such as `useState(props.filters.search)`, does not update when props change. Derive from props, or reset explicitly. - Preserving state after a successful create can leave a filled-in form on screen; `'errors'` keeps state only when validation failed. - `preserveScroll` on links to a *different* page usually feels wrong; users expect a new page to start at the top. - Rapid history updates, such as one per keystroke, can hit browser limits on `pushState` and `replaceState` calls, so debounce search. ## Links versus router calls | Situation | Use | |---|---| | A clickable element the user sees, such as pagination | `<Link preserveScroll preserveState>` | | A visit triggered by code, such as a debounced search | `router.get(url, data, options)` | | Refreshing the current page's data | `router.reload({ only: [...] })`, which preserves state and scroll by default | Both accept the same visit options, so the choice is about the trigger, not the behaviour. ## A checklist for the invoices page 1. Mark the scrolling container with `scroll-region` if the document itself does not scroll. 2. Put `preserveScroll` and `preserveState` on pagination links and filter visits, not on navigation to other pages. 3. Use `replace` for keystroke-driven visits so Back returns to the previous page, not the previous letter. 4. Keep the search value derived from the URL or props, so a shared link opens with the same filter applied.

  • Why can preserveState make a filter input show stale text?
    With `preserveState: true` the page component is not remounted, so state that was initialised once from props keeps its old value when new props arrive. If another action changes `filters.search` on the server, the input still shows the previous text. Derive the value from props, or sync it when the prop changes, when you rely on preserved state.
  • What does the scroll-region attribute change for a scrollable container?
    Inertia's scroll handling covers the document plus every element with `scroll-region`. Marking the container lets Inertia save its position into history, reset it on normal visits, restore it on Back and Forward, and skip the reset when `preserveScroll` is set. Without it, the container's position is left entirely to the DOM.

saying these in an interview costs you the question

  • preserveScroll also keeps the search input's text.
  • preserveState keeps state by caching the previous page's props.
  • Inertia restores scroll for any overflow container automatically.
  • replace: true prevents the server request.
  • GET visits preserve state by default.