This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/react-pdf-loading-states-errors-passwords.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Loading states, errors, and password prompts in react-pdf

Table of contents

    A guide to handling every loading state in react-pdf — from document and page spinners to download progress bars, error callbacks, and password-protected PDF prompts.
    Loading states, errors, and password prompts in react-pdf
    TL;DR
    • Both <Document> and <Page> accept loading, error, and noData props — each takes a string, a React element, or a render function.
    • Track download progress with onLoadProgress({ loaded, total }). Be ready for total === 0 when the server omits Content-Length.
    • The document lifecycle is onSourceSuccessonLoadProgressonLoadSuccess; the page lifecycle adds text-layer, annotation, and render callbacks.
    • For password-protected files, implement onPassword(callback, reason) and inspect PasswordResponses.NEED_PASSWORD/INCORRECT_PASSWORD. Call callback(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>
    PropWhen shownDefault
    loadingWhile the PDF is being fetched/parsed"Loading PDF..."
    errorWhen loading fails"Failed to load PDF file."
    noDataWhen 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:

    1. onSourceSuccess/onSourceError — Resolving the file prop to loadable data
    2. onLoadProgress — Download progress (may fire multiple times)
    3. 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:

    1. onLoadSuccess/onLoadError — Page data loaded
    2. onGetTextSuccess/onGetTextError — Text layer data extracted
    3. onGetAnnotationsSuccess/onGetAnnotationsError — Annotations loaded
    4. onRenderSuccess/onRenderError — Canvas rendering complete
    5. onRenderTextLayerSuccess/onRenderTextLayerError — Text layer rendered
    6. onRenderAnnotationLayerSuccess/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 Document and Page have independent loading, error, and noData props.
    • Each accepts a string, React element, or render function.
    • onLoadProgress gives download progress but total may be 0.
    • Password handling uses a callback pattern — store the callback ref and call it when ready.
    • The PasswordResponses constant tells you whether it’s a first attempt or a retry.
    • Default behavior without onPassword uses window.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

    Can I show different spinners for the document load and individual page renders?

    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.

    Why is 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.

    What’s the difference between 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.

    What happens if I don’t pass 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.

    How do I cancel out of the password prompt cleanly?

    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.

    Why does my error component never show even when the PDF is broken?

    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.

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?