Loading states, errors, and password prompts in react-pdf
Table of contents
react-pdf — from document and page spinners to download progress bars, error callbacks, and password-protected PDF prompts.
- Both
<Document>and<Page>acceptloading,error, andnoDataprops — each takes a string, a React element, or a render function. - Track download progress with
onLoadProgress({ loaded, total }). Be ready fortotal === 0when the server omitsContent-Length. - The document lifecycle is
onSourceSuccess→onLoadProgress→onLoadSuccess; the page lifecycle adds text-layer, annotation, and render callbacks. - For password-protected files, implement
onPassword(callback, reason)and inspectPasswordResponses.NEED_PASSWORD/INCORRECT_PASSWORD. Callcallback(null)to abort if the user cancels.
react-pdf provides built-in props for every loading state — document loading, page loading, errors, and password prompts. This guide covers all of them.
Document loading states
The Document component has three display-state props:
<Document file={file} loading={<Spinner />} error={<ErrorMessage />} noData={<EmptyState />}> <Page pageNumber={1} /></Document>| Prop | When shown | Default |
|---|---|---|
loading | While the PDF is being fetched/parsed | "Loading PDF..." |
error | When loading fails | "Failed to load PDF file." |
noData | When the file prop is null/undefined | "No PDF file specified." |
Each accepts a string, React element, or function:
// String.<Document loading="Please wait..." />
// React element.<Document loading={<div className="spinner"><Spinner /></div>} />
// Function (render function).<Document loading={() => <CustomLoader />} />Page loading states
Page has the same three props, independently from Document:
<Document file={file}> <Page pageNumber={1} loading={<PageSkeleton />} error={<PageError />} noData={<NoPageSelected />} /></Document>This means you can show a document-level spinner while the PDF downloads, then page-level skeletons while individual pages render.
Loading progress
Track download progress with onLoadProgress:
function PDFWithProgress() { const [progress, setProgress] = useState(0); const [loaded, setLoaded] = useState(false);
return ( <Document file={file} onLoadProgress={({ loaded, total }) => { if (total > 0) { setProgress(Math.round((loaded / total) * 100)); } }} onLoadSuccess={() => setLoaded(true)} loading={ <div> <progress value={progress} max={100} /> <span>{progress}%</span> </div> } > <Page pageNumber={1} /> </Document> );}onLoadProgress may be called multiple times during download. total can be 0 if the server doesn’t send Content-Length.
Document callbacks
<Document file={file} onSourceSuccess={() => { // File source resolved (URL fetched, File read, etc.). }} onSourceError={(error) => { // Failed to resolve file source. console.error("Source error:", error); }} onLoadSuccess={(pdf) => { // PDF parsed successfully. console.log("Pages:", pdf.numPages); }} onLoadError={(error) => { // Failed to parse PDF. console.error("Load error:", error); }}> <Page pageNumber={1} /></Document>The loading lifecycle is:
onSourceSuccess/onSourceError— Resolving thefileprop to loadable dataonLoadProgress— Download progress (may fire multiple times)onLoadSuccess/onLoadError— PDF parsing complete
Page callbacks
Page has its own set of lifecycle callbacks, independent from Document:
<Page pageNumber={1} onLoadSuccess={(page) => { console.log("Page loaded:", page.pageNumber); }} onLoadError={(error) => { console.error("Page load error:", error); }} onRenderSuccess={() => { console.log("Canvas rendered"); }} onRenderError={(error) => { console.error("Render error:", error); }}/>Page rendering lifecycle:
onLoadSuccess/onLoadError— Page data loadedonGetTextSuccess/onGetTextError— Text layer data extractedonGetAnnotationsSuccess/onGetAnnotationsError— Annotations loadedonRenderSuccess/onRenderError— Canvas rendering completeonRenderTextLayerSuccess/onRenderTextLayerError— Text layer renderedonRenderAnnotationLayerSuccess/onRenderAnnotationLayerError— Annotation layer rendered
Password-protected PDFs
react-pdf handles password-protected PDFs via the onPassword callback:
import { PasswordResponses } from "react-pdf";
function ProtectedPDF() { const [file, setFile] = useState(null);
const handlePassword = (callback, reason) => { if (reason === PasswordResponses.NEED_PASSWORD) { const password = prompt("This PDF is password-protected. Enter password:"); callback(password); } else if (reason === PasswordResponses.INCORRECT_PASSWORD) { const password = prompt("Incorrect password. Try again:"); callback(password); } };
return ( <Document file={file} onPassword={handlePassword}> <Page pageNumber={1} /> </Document> );}Custom password dialog
For full control over the prompt’s appearance, store the callback and render your own dialog:
function ProtectedPDF() { const [passwordNeeded, setPasswordNeeded] = useState(false); const [password, setPassword] = useState(""); const callbackRef = useRef(null);
const handlePassword = (callback, reason) => { callbackRef.current = callback; setPasswordNeeded(true); };
const submitPassword = () => { if (callbackRef.current) { callbackRef.current(password); setPasswordNeeded(false); setPassword(""); } };
// If the user cancels, call `callback(null)` to abort the loading task — // otherwise it hangs waiting for a password forever. const cancelPassword = () => { callbackRef.current?.(null); setPasswordNeeded(false); setPassword(""); };
return ( <> <Document file={file} onPassword={handlePassword}> <Page pageNumber={1} /> </Document>
{passwordNeeded && ( <dialog open> <h3>Password Required</h3> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && submitPassword()} /> <button onClick={submitPassword}>Submit</button> <button onClick={cancelPassword}>Cancel</button> </dialog> )} </> );}PasswordResponses
The reason argument passed to onPassword is one of two PasswordResponses values:
import { PasswordResponses } from "react-pdf";
PasswordResponses.NEED_PASSWORD; // 1 — first attempt.PasswordResponses.INCORRECT_PASSWORD; // 2 — wrong password, try again.If you don’t provide onPassword, react-pdf falls back to window.prompt.
Key points
- Both
DocumentandPagehave independentloading,error, andnoDataprops. - Each accepts a string, React element, or render function.
onLoadProgressgives download progress buttotalmay be 0.- Password handling uses a callback pattern — store the callback ref and call it when ready.
- The
PasswordResponsesconstant tells you whether it’s a first attempt or a retry. - Default behavior without
onPassworduseswindow.prompt.
How Nutrient Web SDK handles this
All the per-component loading props, error handling callbacks, and custom password dialogs shown above are built into Nutrient Web SDK:
// Loading states, errors, and password dialogs are built in.const instance = await NutrientViewer.load({ container: "#pdf-container", document: "document.pdf", // Built-in loading spinner, error messages, and password prompt. // No `loading`/`error`/`noData` props needed.});No per-component loading/error/noData props, no custom password dialog, no onLoadProgress tracking. Nutrient provides polished, accessible loading states, error messages, and password prompts out of the box — matching your app’s theme automatically.
Learn more about Nutrient Web SDK | Migration guide | Contact Sales
FAQ
Yes. <Document> and <Page> each have their own loading, error, and noData props. A common pattern is a fullscreen spinner on <Document loading> while the PDF downloads, then per-page skeletons on <Page loading> while each page renders to canvas.
total zero in onLoadProgress?PDF.js reports total from the HTTP Content-Length header. If the server doesn’t send one — common with chunked transfer encoding, some proxies, or Content-Encoding: gzip responses — total stays 0. Guard your percentage math with if (total > 0) and fall back to an indeterminate spinner.
onSourceSuccess and onLoadSuccess?onSourceSuccess fires when the file prop has been resolved to loadable data (URL fetched, File/Blob read, etc.). onLoadSuccess fires later, after PDF.js finishes parsing the document and exposes numPages and the page-fetching API.
onPassword?react-pdf falls back to window.prompt with default English messages (“Enter the password to open this PDF file.”/“Invalid password. Please try again.”). It works but it’s not customizable, not styleable, and not localized — provide onPassword for anything user-facing.
Call the callback with null (or any falsy value). PDF.js treats that as a cancellation and rejects the loading task — onLoadError will fire with a PasswordException. If you just close the dialog without invoking the callback, the loading task stays pending forever.
Two common reasons: (1) the error happened during source resolution, not document loading — wire up onSourceError separately, and (2) you used a render function that returns null for some error types. Make sure your error render path is hit by logging inside onLoadError first.