Skip to content

useViewerSetup

Scope: React viewer toolkit (@scaryterry/pdfium/react).

useViewerSetup() is the single orchestration hook for building PDF viewer UIs. It composes five lower-level hooks (usePageNavigation, useZoom, useFitZoom, useWheelZoom, usePageDimensions) into one unified result object with logically grouped state slices. The result plugs directly into <PDFToolbar> and <PDFDocumentView>.

import { useViewerSetup } from '@scaryterry/pdfium/react';
const viewer = useViewerSetup({ initialFitMode: 'page-width' });

useViewerSetup accepts an optional UseViewerSetupOptions object:

PropertyTypeDefaultDescription
initialScalenumber1Starting zoom level. Clamped to the range [0.25, 5] by the underlying useZoom hook.
initialFitModeFitModeundefinedInitial fit mode. Use 'page-width', 'page-height', or 'page-fit' to start in a live fit state that reapplies on resize. While active, it takes precedence over initialScale.
initialScrollMode'continuous' | 'single''continuous'Starting scroll mode. 'continuous' renders all pages in a scrollable container; 'single' renders one page at a time.

useViewerSetup returns a UseViewerSetupResult containing the current document plus five state groups:

FieldTypeDescription
documentWorkerPDFiumDocument | nullThe currently loaded document from PDFiumProvider, or null if no document is open.
FieldTypeDescription
pageIndexnumberZero-based index of the current page.
setPageIndex(index: number) => voidNavigate to a page by zero-based index. The value is clamped to valid bounds.
next() => voidNavigate to the next page. No-op if already on the last page.
prev() => voidNavigate to the previous page. No-op if already on the first page.
canNextbooleantrue when navigation forward is possible (i.e. not on the last page).
canPrevbooleantrue when navigation backward is possible (i.e. not on the first page).
pageCountnumberTotal number of pages in the document. 0 when no document is loaded.
FieldTypeDescription
scalenumberCurrent zoom scale factor (e.g. 1 = 100%, 2 = 200%).
setScale(scale: number) => voidSet the zoom level to an arbitrary value. Clamped to [0.25, 5]. Also clears any active fit mode.
zoomIn() => voidIncrease zoom by one step (default step: 0.25). Clears the active fit mode.
zoomOut() => voidDecrease zoom by one step (default step: 0.25). Clears the active fit mode.
reset() => voidReset zoom to initialScale. Clears the active fit mode.
canZoomInbooleantrue when the current scale is below the maximum (default: 5).
canZoomOutbooleantrue when the current scale is above the minimum (default: 0.25).

Note: All manual zoom actions (setScale, zoomIn, zoomOut, reset) automatically clear the active fit mode. This prevents the fit-mode auto-reapply from overriding the user’s explicit zoom choice.

FieldTypeDescription
fitWidth() => voidSet the zoom level so the current page fills the container width (minus 32px padding). Activates the 'page-width' fit mode.
fitPage() => voidSet the zoom level so the entire current page fits within the container. Activates the 'page-fit' fit mode.
fitScale(mode: FitMode) => numberCompute the scale value for a given fit mode without applying it. Useful for custom UI (e.g. displaying what the scale would be).
activeFitModeFitMode | nullThe currently active fit mode ('page-width' or 'page-fit'), or null if the user has manually zoomed since the last fit action.

FitMode is defined as:

type FitMode = 'page-width' | 'page-height' | 'page-fit';
FieldTypeDescription
scrollMode'continuous' | 'single'The current scroll mode.
setScrollMode(mode: 'continuous' | 'single') => voidSwitch between continuous and single-page scroll modes.
FieldTypeDescription
refRefObject<HTMLDivElement | null>React ref to attach to the scroll container element. Shared between useFitZoom (for ResizeObserver measurements), useWheelZoom (for wheel event listening), and PDFDocumentView (for scroll virtualisation).
dimensionsPageDimension[] | undefinedArray of { width, height } for every page in the document (in PDF points). undefined while loading.
zoomAnchorRefRefObject<ZoomAnchor | null>Ref used for cursor-anchored zoom coordination between useWheelZoom and useVisiblePages. Pass this to PDFDocumentView so that Ctrl/Cmd+wheel zoom keeps the point under the cursor stationary.

PDFToolbar accepts the entire UseViewerSetupResult object via its viewer prop. No mapping is required:

import { PDFToolbar, useViewerSetup } from '@scaryterry/pdfium/react';
function Viewer() {
const viewer = useViewerSetup();
return (
<PDFToolbar viewer={viewer}>
<PDFToolbar.Navigation>
{({ getPrevProps, getNextProps, pageNumber, pageCount }) => (
<>
<button {...getPrevProps()}>Prev</button>
<span>{pageNumber} / {pageCount}</span>
<button {...getNextProps()}>Next</button>
</>
)}
</PDFToolbar.Navigation>
<PDFToolbar.Zoom>
{({ getZoomInProps, getZoomOutProps, percentage }) => (
<>
<button {...getZoomOutProps()}>-</button>
<span>{percentage}%</span>
<button {...getZoomInProps()}>+</button>
</>
)}
</PDFToolbar.Zoom>
</PDFToolbar>
);
}

PDFDocumentView requires several props sourced from different groups in the viewer result. The following table shows the mapping:

PDFDocumentView propSourceDescription
containerRefviewer.container.refScroll container ref for layout measurements and wheel zoom.
scaleviewer.zoom.scaleCurrent zoom scale factor.
scrollModeviewer.scroll.scrollMode'continuous' or 'single'.
currentPageIndexviewer.navigation.pageIndexZero-based index of the current page.
onCurrentPageChangeviewer.navigation.setPageIndexCallback fired when the visible page changes during scroll.
zoomAnchorRefviewer.container.zoomAnchorRefCursor-anchored zoom coordination ref.
import { PDFDocumentView, useViewerSetup } from '@scaryterry/pdfium/react';
function Viewer() {
const viewer = useViewerSetup();
return (
<PDFDocumentView
containerRef={viewer.container.ref}
scale={viewer.zoom.scale}
scrollMode={viewer.scroll.scrollMode}
currentPageIndex={viewer.navigation.pageIndex}
onCurrentPageChange={viewer.navigation.setPageIndex}
zoomAnchorRef={viewer.container.zoomAnchorRef}
/>
);
}

The useViewerSetup return type was restructured from a flat object into logical groups. The following table maps every property from the old flat API to its new grouped path:

Old flat pathNew grouped path
viewer.pageIndexviewer.navigation.pageIndex
viewer.setPageIndexviewer.navigation.setPageIndex
viewer.nextviewer.navigation.next
viewer.prevviewer.navigation.prev
viewer.canNextviewer.navigation.canNext
viewer.canPrevviewer.navigation.canPrev
viewer.pageCountviewer.navigation.pageCount
viewer.scaleviewer.zoom.scale
viewer.setScaleviewer.zoom.setScale
viewer.zoomInviewer.zoom.zoomIn
viewer.zoomOutviewer.zoom.zoomOut
viewer.resetviewer.zoom.reset
viewer.canZoomInviewer.zoom.canZoomIn
viewer.canZoomOutviewer.zoom.canZoomOut
viewer.fitWidthviewer.fit.fitWidth
viewer.fitPageviewer.fit.fitPage
viewer.fitScaleviewer.fit.fitScale
viewer.activeFitModeviewer.fit.activeFitMode
viewer.scrollModeviewer.scroll.scrollMode
viewer.setScrollModeviewer.scroll.setScrollMode
viewer.containerRefviewer.container.ref
viewer.dimensionsviewer.container.dimensions
viewer.zoomAnchorRefviewer.container.zoomAnchorRef
function Viewer() {
const viewer = useViewerSetup();
return (
<>
<button onClick={viewer.prev} disabled={!viewer.canPrev}>Prev</button>
<span>{viewer.pageIndex + 1} / {viewer.pageCount}</span>
<button onClick={viewer.next} disabled={!viewer.canNext}>Next</button>
<button onClick={viewer.navigation.prev} disabled={!viewer.navigation.canPrev}>Prev</button>
<span>{viewer.navigation.pageIndex + 1} / {viewer.navigation.pageCount}</span>
<button onClick={viewer.navigation.next} disabled={!viewer.navigation.canNext}>Next</button>
<button onClick={viewer.zoomOut} disabled={!viewer.canZoomOut}>-</button>
<span>{Math.round(viewer.scale * 100)}%</span>
<button onClick={viewer.zoomIn} disabled={!viewer.canZoomIn}>+</button>
<button onClick={viewer.zoom.zoomOut} disabled={!viewer.zoom.canZoomOut}>-</button>
<span>{Math.round(viewer.zoom.scale * 100)}%</span>
<button onClick={viewer.zoom.zoomIn} disabled={!viewer.zoom.canZoomIn}>+</button>
<PDFDocumentView
containerRef={viewer.containerRef}
scale={viewer.scale}
scrollMode={viewer.scrollMode}
currentPageIndex={viewer.pageIndex}
onCurrentPageChange={viewer.setPageIndex}
zoomAnchorRef={viewer.zoomAnchorRef}
containerRef={viewer.container.ref}
scale={viewer.zoom.scale}
scrollMode={viewer.scroll.scrollMode}
currentPageIndex={viewer.navigation.pageIndex}
onCurrentPageChange={viewer.navigation.setPageIndex}
zoomAnchorRef={viewer.container.zoomAnchorRef}
/>
</>
);
}

useViewerSetup sits at the middle tier of a three-layer composition:

┌─────────────────────────────────────────────────────────────────┐
│ Tier 3 — Component layer │
│ │
│ PDFToolbar PDFDocumentView │
│ (consumes viewer) (consumes viewer slices) │
└──────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────┴──────────────────────────────────────┐
│ Tier 2 — Orchestration hook │
│ │
│ useViewerSetup() │
│ ├─ composes individual hooks │
│ ├─ wires cross-cutting behaviour │
│ └─ returns grouped state slices │
└──────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────┴──────────────────────────────────────┐
│ Tier 1 — Primitive hooks │
│ │
│ usePageNavigation useZoom useFitZoom │
│ useWheelZoom usePageDimensions │
└─────────────────────────────────────────────────────────────────┘

The five primitive hooks are independently useful, but combining them introduces cross-cutting behaviour that none of them should own individually. useViewerSetup handles these interactions:

When the user calls zoomIn(), zoomOut(), setScale(), or reset(), the hook wraps the underlying useZoom actions to also clear activeFitMode. Without this, the auto-reapply effect would immediately override the user’s manual zoom choice.

When activeFitMode is set (e.g. 'page-width'), the hook computes the corresponding scale via fitScale(activeFitMode) and reapplies it whenever the computed value changes. This happens when:

  • The container is resized (detected by useFitZoom’s ResizeObserver)
  • The user navigates to a page with different dimensions
  • The document is swapped

The reapply runs in a useEffect that watches activeFitScaleValue, ensuring the page always fills the available space while the fit mode is active.

useWheelZoom listens for Ctrl/Cmd+wheel events on the container and computes proportional zoom (each tick multiplies or divides the scale by a factor of 1.1). It writes anchor data — cursor position, scroll offsets, and the scale ratio — to zoomAnchorRef. useVisiblePages (inside PDFDocumentView) reads this anchor in a layout effect and adjusts the scroll position so the content under the cursor remains stationary. The onManualZoom callback clears the active fit mode, keeping the zoom and fit systems consistent.

A single containerRef is shared between three consumers:

  1. useFitZoom — attaches a ResizeObserver to track the container’s client dimensions for fit-scale computation.
  2. useWheelZoom — attaches a wheel event listener (with { passive: false }) for Ctrl/Cmd+wheel zoom.
  3. PDFDocumentView — uses it as the scroll container for virtualised page rendering.

All three handle late-mounting gracefully: if the ref target is null on first render (e.g. conditionally rendered content), they attach once the element mounts.

  • PDFViewer — Compound viewer component and page rendering primitives
  • Toolbar — Headless toolbar with prop-getter pattern
  • Examples — End-to-end compositions built from useViewerSetup and viewer slots