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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions