This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/pdfjs-eventbus-guide.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. PDF.js EventBus guide: Events, hooks, and custom pub/sub patterns

Table of contents

    The EventBus is PDF.js’s internal pub/sub system that powers every viewer interaction. This guide covers built-in events, React hook patterns, and how to extend it for custom annotation features.
    PDF.js EventBus guide: Events, hooks, and custom pub/sub patterns
    TL;DR
    • The EventBus is PDF.js’s internal pub/sub channel — every interaction (page change, zoom, render, search) fires through it.
    • Subscribe with eventBus.on(name, handler) and clean up with eventBus.off(name, handler) in useEffect returns.
    • Dispatch your own events (e.g. annotationcolorchange) to bridge React components and PDF.js’s vanilla JS class instances.

    Prerequisites

    This guide assumes you have a PDFViewer composed from pdfjs-dist/web/pdf_viewer.mjs and an EventBus instance shared between the viewer, link service, and find controller. If you haven’t wired those up yet, start with our blog on how to set up a custom PDF.js viewer in React.

    You’ll also need:

    • pdfjs-dist 4.x or later
    • A React app (the hook patterns below use React 18+ effect semantics)

    Creating an EventBus

    Import the viewer layer and construct a single EventBus instance that every component will share:

    const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");
    const eventBus = new pdfjs.EventBus();

    The EventBus is shared across all PDF.js components — PDFViewer, PDFLinkService, PDFFindController — and your own custom code.

    Subscribing and unsubscribing

    Register a handler with eventBus.on(name, handler) and remove it with eventBus.off(name, handler):

    // Subscribe.
    eventBus.on("pagechanging", (evt) => {
    console.log("Now on page:", evt.pageNumber);
    });
    // Unsubscribe.
    eventBus.off("pagechanging", handler);

    Built-in events reference

    PDF.js fires a fixed set of built-in events. The tables below group the most useful ones — by page lifecycle, scale and rotation, and search — with their payloads and when each fires.

    Page lifecycle

    These events fire as the document loads and as pages scroll into view and finish rendering:

    EventPayloadWhen
    pagesinit{}All pages initialized after document load
    pagechanging{ pageNumber }User scrolls to a different page
    pagerendered{ pageNumber, cssTransform, timestamp }A page canvas finishes rendering
    textlayerrendered{ pageNumber }Text layer overlay is ready on a page

    Scale and rotation

    These events fire whenever the zoom level or page rotation changes:

    EventPayloadWhen
    scalechanging{ scale, presetValue }Zoom level changes
    rotationchanging{ pagesRotation }Page rotation changes

    Search/find

    These events drive find operations and report their results back to your UI:

    EventPayloadWhen
    find{ type, query, caseSensitive, ... }You dispatch this to trigger search
    updatefindmatchescount{ matchesCount: { current, total } }Match count updates
    updatefindcontrolstate{ state, matchesCount }Find state changes (FOUND, NOT_FOUND, WRAPPED, PENDING)

    Pattern: React hook for EventBus events

    Wrap subscription and cleanup in a reusable hook so each listener is removed when the component unmounts:

    function useEventBus(eventBus, eventName, handler) {
    React.useEffect(() => {
    eventBus?.on(eventName, handler);
    return () => {
    eventBus?.off(eventName, handler);
    };
    }, [eventBus, eventName, handler]);
    }
    // Usage.
    useEventBus(eventBus.current, "pagechanging", (evt) => {
    setCurrentPage(evt.pageNumber);
    });

    Pattern: Custom events via EventBus

    The EventBus isn’t limited to built-in events. You can dispatch your own custom events and use it as a cross-component communication channel:

    class PDFAnnotationState {
    #eventBus;
    #color = "red";
    #tool = null;
    #showAnnotations = true;
    constructor({ eventBus }) {
    this.#eventBus = eventBus;
    }
    get color() {
    return this.#color;
    }
    set color(newColor) {
    this.#color = newColor;
    this.#eventBus.dispatch("annotationcolorchange", {
    source: this,
    color: newColor,
    });
    }
    get tool() {
    return this.#tool;
    }
    set tool(newTool) {
    this.#tool = newTool;
    this.#eventBus.dispatch("annotationtoolchange", {
    source: this,
    tool: newTool,
    });
    }
    get showAnnotations() {
    return this.#showAnnotations;
    }
    set showAnnotations(show) {
    this.#showAnnotations = show;
    this.#eventBus.dispatch("annotationshowchange", {
    source: this,
    showAnnotations: show,
    });
    }
    }

    Then listen to these events from any component:

    function useAnnotationState(eventBus, annotationState) {
    const [color, setColor] = React.useState(annotationState?.color);
    const [tool, setTool] = React.useState(annotationState?.tool);
    const [show, setShow] = React.useState(annotationState?.showAnnotations);
    React.useEffect(() => {
    const onColor = (evt) => setColor(evt.color);
    const onTool = (evt) => setTool(evt.tool);
    const onShow = (evt) => setShow(evt.showAnnotations);
    eventBus?.on("annotationcolorchange", onColor);
    eventBus?.on("annotationtoolchange", onTool);
    eventBus?.on("annotationshowchange", onShow);
    return () => {
    eventBus?.off("annotationcolorchange", onColor);
    eventBus?.off("annotationtoolchange", onTool);
    eventBus?.off("annotationshowchange", onShow);
    };
    }, [eventBus, annotationState]);
    return { color, tool, show };
    }

    Why EventBus instead of React state?

    PDF.js components (PDFViewer, PDFFindController, etc.) are not React components — they’re vanilla JS class instances. They can’t subscribe to React state. The EventBus bridges this gap:

    1. Toolbar changes annotation color → sets annotationState.color = "blue"
    2. Setter dispatches annotationcolorchange via EventBus
    3. Annotation component listens via .on("annotationcolorchange") and updates its React state

    This pattern keeps your custom PDF features integrated with PDF.js’s own event system instead of creating a parallel state management layer.

    Key events for common features

    This table maps common viewer features to the events you’ll typically subscribe to:

    FeatureEvents to listen to
    Page navigation toolbarpagechanging
    Zoom controlsscalechanging
    Custom overlay layerstextlayerrendered, pagerendered
    Search results displayupdatefindcontrolstate, updatefindmatchescount
    Annotation renderingtextlayerrendered (to create overlay containers)
    Highlight positioningscalechanging, rotationchanging

    Gotchas

    • Always clean up listeners in useEffect return functions — PDF.js EventBus doesn’t clean up automatically
    • The dispatch method requires a source property in the event object by convention
    • EventBus events are synchronous — heavy handlers will block rendering
    • textlayerrendered fires per page, not once for the whole document

    How Nutrient Web SDK handles this

    Nutrient Web SDK exposes the same kinds of viewer events through standard DOM-style addEventListener/removeEventListener calls. Event names are TypeScript-typed, so your editor can complete them and flag typos.

    instance.addEventListener("viewState.currentPageIndex.change", (pageIndex) => {
    console.log("Now on page:", pageIndex + 1);
    });
    instance.addEventListener("viewState.zoom.change", (zoom) => {
    console.log("Zoom level:", zoom);
    });

    For broader viewer changes, listen to viewState.change and receive the full state diff.

    Learn more about Nutrient Web SDK | Migration guide | Contact Sales

    FAQ

    Should I use eventBus.on or eventBus._on?

    Use eventBus.on/eventBus.off. The _on/_off forms exist as legacy internal aliases, but PDF.js’s public documentation and viewer source use the non-underscored names. Sticking to the public form makes your code easier to read and less likely to break on a future PDF.js version.

    Why does dispatch need a source property?

    PDF.js’s own components inspect evt.source to avoid reacting to events they themselves dispatched (preventing feedback loops). When you call eventBus.dispatch("myevent", { source: this, ... }), you’re following the same convention so downstream listeners can filter by origin.

    Are EventBus events synchronous?

    Yes. Handlers fire in the order they were subscribed, on the same microtask. Long-running work in a listener will block subsequent listeners and delay the rendering pipeline — wrap expensive operations in queueMicrotask, setTimeout, or requestIdleCallback when needed.

    Does textlayerrendered fire once or per page?

    Per page. Each PageView fires its own textlayerrendered event when its text layer overlay finishes rendering. That makes it a useful hook for inserting per-page overlays (highlights, annotations), but a poor signal for “the whole document is ready.”

    Can I have multiple EventBus instances?

    Technically yes, but you almost never want to. PDF.js’s viewer components are constructed with a shared EventBus so that PDFViewer, PDFLinkService, PDFFindController, and your code all see the same events. Separate buses defeat the purpose of having one.

    What happens if I forget to call eventBus.off?

    The handler keeps running for the lifetime of the EventBus, even after the React component that registered it has unmounted. That typically manifests as memory leaks and stale-state warnings (handler closes over old state). Always clean up in the useEffect return.

    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?