---
title: "PDF.js EventBus guide: Events, hooks, and custom pub/sub patterns"
canonical_url: "https://www.nutrient.io/blog/pdfjs-eventbus-guide/"
md_url: "https://www.nutrient.io/blog/pdfjs-eventbus-guide.md"
last_updated: "2026-06-08T11:03:52.639Z"
description: "Learn how to use the PDF.js EventBus for page lifecycle events, scale changes, search results, and custom annotation state management in React applications."
---

**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](https://www.nutrient.io/blog/pdfjs-react-viewer-setup.md).

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:

```tsx

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)`:

```tsx

// 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:

| Event               | Payload                                   | When                                      |
| ------------------- | ----------------------------------------- | ----------------------------------------- |
| `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:

| Event              | Payload                  | When                  |
| ------------------ | ------------------------ | --------------------- |
| `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:

| Event                    | Payload                                | When                                                            |
| ------------------------ | -------------------------------------- | --------------------------------------------------------------- |
| `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:

```tsx

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:

```tsx

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:

```tsx

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:

| Feature                 | Events to listen to                                |
| ----------------------- | -------------------------------------------------- |
| Page navigation toolbar | `pagechanging`                                     |
| Zoom controls           | `scalechanging`                                    |
| Custom overlay layers   | `textlayerrendered`, `pagerendered`                |
| Search results display  | `updatefindcontrolstate`, `updatefindmatchescount` |
| Annotation rendering    | `textlayerrendered` (to create overlay containers) |
| Highlight positioning   | `scalechanging`, `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.

```js

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.

<!-- See the related guides on [search][search-post] and [navigation, zoom, and rotation][navigation-post] for events that bridge into PDF.js EventBus events as well. -->

[Learn more about Nutrient Web SDK](https://www.nutrient.io/sdk/web-overview/) | [Migration guide](https://www.nutrient.io/guides/web/about/migration-guides/migrating-from-mozilla-pdfjs.md) | [Contact Sales](https://www.nutrient.io/contact-sales/?=sdk)

## FAQ

#### Should I use <code>eventBus.on</code> or <code>eventBus._on</code>?

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 <code>dispatch</code> need a <code>source</code> 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 <code>EventBus</code> 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 <code>textlayerrendered</code> 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 <code>EventBus</code> 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 <code>eventBus.off</code>?

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.

<!-- [search-post]: /blog/pdfjs-text-search-pdffindcontroller/ -->

<!-- [navigation-post]: /blog/pdfjs-navigation-zoom-rotation/ -->
---

## Related pages

- [Accessibility Untangled Why It Matters Guide](/blog/accessibility-untangled-why-it-matters-guide.md)
- [The business case for accessibility: Five ways it drives enterprise value](/blog/5-ways-accessibility-drives-enterprise-value.md)
- [Advanced Techniques For React Native Ui Components](/blog/advanced-techniques-for-react-native-ui-components.md)
- [Auto Tagging And Document Accessibility In Dotnet Sdk](/blog/auto-tagging-and-document-accessibility-in-dotnet-sdk.md)
- [Best Document Viewers](/blog/best-document-viewers.md)
- [The CEO’s AI playbook: Why decision architecture beats model selection](/blog/ceo-ai-playbook-decision-architecture.md)
- [Create Pdfs With React](/blog/create-pdfs-with-react.md)
- [Complete Guide To Pdfjs](/blog/complete-guide-to-pdfjs.md)
- [The CTO’s AI playbook: Why accountability architecture beats orchestration](/blog/cto-ai-playbook-accountability-architecture.md)
- [Document Viewer](/blog/document-viewer.md)
- [Digital Signatures](/blog/digital-signatures.md)
- [or](/blog/how-to-build-a-javascript-pdf-viewer-with-pdfjs.md)
- [Emerging threats: Your logging system may be an agentic threat vector](/blog/emerging-threats-your-logging-system.md)
- [Document Ai Vs Ocr](/blog/document-ai-vs-ocr.md)
- [or](/blog/how-to-build-a-reactjs-pdf-viewer-with-react-pdf.md)
- [How To Build A Reactjs Viewer With Pdfjs](/blog/how-to-build-a-reactjs-viewer-with-pdfjs.md)
- [or](/blog/how-to-convert-html-to-pdf-using-wkhtmltopdf-and-python.md)
- [base_url tells WeasyPrint where to resolve relative asset paths](/blog/how-to-generate-pdf-reports-from-html-in-python.md)
- [Linearized Pdf](/blog/linearized-pdf.md)
- [Nutrient Vs Conga Composer](/blog/nutrient-vs-conga-composer.md)
- [Pdf Page Labels](/blog/pdf-page-labels.md)
- [Open Pdf In Your Web App](/blog/open-pdf-in-your-web-app.md)
- [Pdf Sdk Performance Benchmark](/blog/pdf-sdk-performance-benchmark.md)
- [Online Document Viewer](/blog/online-document-viewer.md)
- [Pdf Sdk Compliance Security Checklist](/blog/pdf-sdk-compliance-security-checklist.md)
- [Pdf Ua Compliance Guide](/blog/pdf-ua-compliance-guide.md)
- [Pdfjs Limitations Commercial Upgrade](/blog/pdfjs-limitations-commercial-upgrade.md)
- [Pdfjs React Viewer Setup](/blog/pdfjs-react-viewer-setup.md)
- [Process Flows](/blog/process-flows.md)
- [or](/blog/sample-blog-updated.md)
- [Wcag2 Accessibility Requirements Documents](/blog/wcag2-accessibility-requirements-documents.md)
- [Convert an HTML file to PDF.](/blog/top-ten-ways-to-convert-html-to-pdf.md)
- [Web Sdk Is Now Headless](/blog/web-sdk-is-now-headless.md)
- [Vector Pdf](/blog/vector-pdf.md)
- [What Is A Vpat](/blog/what-is-a-vpat.md)
- [What Are Annotations](/blog/what-are-annotations.md)
- [What Is Pdf Ua](/blog/what-is-pdf-ua.md)
- [Why Your Ai Agent Hallucinates Pdf Table Data](/blog/why-your-ai-agent-hallucinates-pdf-table-data.md)

