react-pdf-kit-nextjs-pages-router
Use this skill when: the developer asks to add a PDF viewer using
@react-pdf-kit/viewer to a Next.js Pages Router project (pages/
directory) on Next.js 15+. For App Router use
react-pdf-kit-nextjs-app-router. Next.js 14 is not supported by
v2 — see react-pdf-kit-nextjs14-pdfjs-override.
The Pages Router runs every page through SSR by default. The viewer is
client-only (it relies on browser APIs), so it must be mounted with
next/dynamic and ssr: false.
Gotchas
- The viewer cannot render on the server. A direct import crashes
during SSR with
TypeError: Promise.withResolve is not a function(or awindow is not definedstyle error). Always import the viewer vianext/dynamicwithssr: false, and put'use client'on the viewer component. - Install only
@react-pdf-kit/viewer. Do NOT installpdfjs-distseparately. It is an auto-installed peer dependency. No worker configuration is required —RPConfighandles it. Version overrides and custom worker URLs live inreact-pdf-kit-worker-config. _app.tsxis the wrong place to mount the viewer._app.tsxruns on the server too. Keep the viewer in its own dynamically-imported,ssr: falsecomponent.- Use Next.js 15+. Next.js 14 + webpack is unsupported by v2.
RPDefaultLayoutis deprecated in v2. UseRPLayout.
Procedure
1. Install the library
pnpm add @react-pdf-kit/viewer
2. Create the viewer as a client-only component
// components/PdfViewer.tsx
'use client'
import {
RPConfig,
RPProvider,
RPLayout,
RPPages,
} from '@react-pdf-kit/viewer'
export default function PdfViewer({ src }: { src: string }) {
return (
<RPConfig>
<RPProvider src={src}>
<RPLayout toolbar>
<RPPages />
</RPLayout>
</RPProvider>
</RPConfig>
)
}
Note the default export: next/dynamic works most ergonomically
with default exports. RPConfig handles the worker and theming;
licenseKey is an optional prop (without it, the viewer is in trial
mode with a watermark).
3. Mount via next/dynamic with ssr: false
// pages/document/[id].tsx
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
const PdfViewer = dynamic(() => import('../../components/PdfViewer'), {
ssr: false,
loading: () => <div>Loading viewer...</div>,
})
export default function DocumentPage() {
const router = useRouter()
const id = router.query.id as string | undefined
return (
<main style={{ height: '100vh' }}>
{id ? <PdfViewer src={`/api/documents/${id}.pdf`} /> : null}
</main>
)
}
The id guard avoids mounting the viewer with undefined on the
first client render (before the router has hydrated).
Verify
pnpm install
pnpm build # must succeed under SSR + production build
pnpm dev # visit /document/<id>
SSR should produce HTML containing the loading state, then the viewer
mounts client-side after hydration. The first PDF page should render
with the default toolbar, text selection should work, and there should
be no Promise.withResolve is not a function error in the console.
References
next/dynamicAPI: https://nextjs.org/docs/pages/api-reference/components/dynamic- Companion skills:
react-pdf-kit-nextjs14-pdfjs-override: read this if the project is on Next.js 14 (v2 is unsupported there).react-pdf-kit-nextjs-app-router: for App Router projects.react-pdf-kit-worker-config: forpdfjs-distversion overrides and custom worker URLs.