Skip to content

goToPage() shortly after onLoaded lands on the last page instead of the target #60

@davi-pandektes

Description

@davi-pandektes

Is your feature request related to a problem? Please describe.

When we open a PDF and programmatically scroll to a specific page shortly after onLoaded fires (e.g. deep-linking to a chat citation or a highlighted reference), goToPage(target) intermittently lands at or near the last page of the document instead of the target.

Tracing through the source, two things compound:

  1. scrollToPage (PaginationContext-15f88187.js) calls setFocusedPage(u) synchronously before the early-return guard if (!m.current || !s) return, and also before the actual element.scrollTo(...). So focusedPage reports success even when the scroll was skipped or got browser-clamped.
  2. The scroll-ratio preservation effect in RPPages.js (const nt = X / we * Y; i.scrollTo({ top: Math.min(nt, Y) ... })) then amplifies an initial clamped scroll. If the first scrollTo was clamped to scrollHeight - clientHeight because the virtualizer hasn't measured rows yet, X / we ≈ 1, and every subsequent totalInnerDimensions growth pulls the scrollTop back to ~1.0 × newTotalHeight — i.e. the bottom of the document.

The initialPage > 1 code path already handles this correctly (it skips Pe and uses a debounced ne.height > 0 gate). But initialPage is only read on mount, which doesn't help applications where the target page is determined asynchronously (e.g. after text resolution or a user interaction).

Describe the solution you'd like

Any one of these would be enough:

  1. A public "virtualizer-ready" signal — e.g. an onLayoutReady?: () => void prop on RPProvider, or exporting useVirtualScrollContext().totalInnerDimensions — so consumers can defer imperative calls until layout is stable.
  2. Make goToPage internally queue the scroll if virtualScrollableElementRef / totalInnerDimensions.height aren't ready, and flush it once they are (mirroring the initialPage > 1 effect).
  3. Guard Pe against growth caused by initial measurement, e.g. skip the ratio preservation while lastMeasuredRowIndex < 0 — same early return already used for the initialPage > 1 branch.
  4. Move the setFocusedPage(u) call in scrollToPage below the if (!m.current || !s) return guard so focusedPage reflects the actual scroll state and can be used as a signal by consumers.

Describe alternatives you've considered

  • initialPage prop on RPProvider: works for true deep-link-on-mount, but doesn't cover cases where the target page is known only after async work (e.g. async reference resolution, user clicking a citation on an already-open viewer).
  • Retry loop around goToPage: doesn't help because Pe keeps re-applying the bad ratio as dimensions grow; retries just fight it.
  • Polling document.querySelector('[data-rp="pages"] > div').scrollHeight until it's stable for ~100ms, then calling goToPage: current workaround. Works but relies on an internal DOM selector and duplicates logic the viewer already has internally.

Additional context

  • Version: @react-pdf-kit/viewer 2.3.0.
  • Environment: React 19 / Next.js 16. Timing-sensitive, so reliably reproducible on cold opens of long PDFs (100+ pages).
  • Minimal repro:
    function ScrollToPage({ page }: { page: number }) {
      const { goToPage } = usePaginationContext()
      useEffect(() => { goToPage(page) }, [page])
      return null
    }
    
    <RPProvider src={url} onLoaded={() => {}}>
      <RPLayout toolbar={false}><RPPages /></RPLayout>
      <ScrollToPage page={50} />
    </RPProvider>
    Opening a 100+ page PDF with page={50} intermittently lands near the last page rather than page 50.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions