CloudPDF
DocsPricing
Start building

Setup

A headless viewer needs two packages: the one for your framework, and an engine. This page installs them, creates the engine for your whole app, and covers server-side rendering, bundlers and fonts.

Install#

npm install @embedpdf/react@next @cloudpdf/engine@next

The framework package holds every plugin, one import path each (@embedpdf/react/search, @embedpdf/react/annotation). A plugin you don’t import isn’t in your bundle.

It works with React 18 and later.

Create the engine once#

Create one engine when your app starts, and share it with every viewer:

// pdf.ts
import { cloudEngine } from '@cloudpdf/engine';
 
export const engine = cloudEngine();

It connects to CloudPDF SaaS. Your own server names itself: cloudEngine({ baseUrl: 'https://pdf.example.com' }). Creating the engine makes no request.

<Viewer> can also own the engine. Pass a function, and the viewer creates the engine when it mounts and destroys it when it unmounts:

owned-engine-cloud.tsx
import { Viewer } from '@embedpdf/react/runtime';
import { cloudEngine } from '@cloudpdf/engine';
import { plugins } from './pdf';

export function DocumentViewer() {
  return (
    <Viewer engine={() => cloudEngine({ baseUrl: 'https://pdf.example.com' })} plugins={plugins} />
  );
}

An engine you pass in yourself is never destroyed by the viewer, so one engine can serve every viewer in your app.

Register the plugins#

Plugins are a list you create once, next to the engine:

plugins.ts
import { stagePlugin } from '@embedpdf/react/stage';
import { renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { selectionPlugin } from '@embedpdf/react/selection';

export const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];

The viewer reads engine and plugins once, when it mounts. Creating the list inside a component would make a new list on every render; the viewer ignores it and warns you while you develop.

Order doesn’t matter, and a plugin that needs another says so when the app starts: the annotation plugin, for example, needs the interaction plugin.

Server-side rendering#

The headless packages render nothing on the server that needs the engine: the engine only starts in the browser. So a module-level engine is safe everywhere.

In the Next.js App Router, mark the component that renders <Viewer> as a client component:

ssr.tsx
'use client';

import { Viewer } from '@embedpdf/react/runtime';
import { engine, plugins } from './pdf';

export function DocumentViewer() {
  return <Viewer engine={engine} plugins={plugins}>{/* … */}</Viewer>;
}

Tokens#

Each document opens with a token your backend creates: it says who the user is and what they may do. Pass a function, so a long session gets fresh tokens:

tokens.tsx
import { Viewer } from '@embedpdf/react/runtime';
import { fetchDocumentToken } from './api';
import { engine, plugins } from './pdf';

export function DocumentViewer({ documentId }: { documentId: string }) {
  return (
    <Viewer
      engine={engine}
      plugins={plugins}
      initialDocuments={[{ source: { kind: 'token', token: () => fetchDocumentToken(documentId) } }]}
    />
  );
}

Authentication covers the three ways to open a document and what a token carries. Fonts, limits and storage are your server’s configuration.

Good to know#

One viewer, many documents#

A viewer can hold several open documents at once, like tabs. See Several documents.

Changing engine or plugins#

Both are read once. To use another engine or another set of plugins, mount a new <Viewer>, for example by giving it a new key.

The ready-made Viewer uses these same packages. If you start there, everything on these pages still applies when you move to headless.