---
title: "Setup — React"
description: "Install the headless packages for your framework, create the engine once, and use them with server-side rendering."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/setup"
---

# 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

```sh
pnpm add @embedpdf/react@next @embedpdf/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:

```ts
// pdf.ts
import { localEngine } from '@embedpdf/engine';

export const engine = localEngine();
```

Creating the engine is instant and allocates nothing. The first viewer that uses it starts it, and
PDFium boots in a Web Worker in the background.

`<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.tsx`**

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

export function DocumentViewer() {
  return <Viewer engine={() => localEngine()} 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`**

```typescript
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`**

```tsx
'use client';

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

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

## Bundlers and self-hosting

The engine's worker and WebAssembly ship inside the package, and your build picks them up.

Vite, webpack 5 and Rspack need no configuration, and neither does Next.js, with webpack or
Turbopack.

There's nothing to copy, and nothing is fetched from a CDN. The engine loads from your own site, so
it works offline and behind a firewall.

If your setup can't bundle a worker, for example with a strict Content Security Policy, host the
files yourself: [Self-hosting](https://www.embedpdf.com/docs/viewer/react/self-hosting) lists every option, with notes for Parcel
and plain esbuild.

## Fonts

Text the engine draws, such as a text box or a filled form field, needs a font with its characters.
For languages the built-in fonts don't cover, give the engine a fallback font:

```ts
export const engine = localEngine({
  fallbackFonts: [{ key: 'noto-cjk', familyName: 'Noto Sans CJK', url: '/fonts/NotoSansCJK.ttf' }],
});
```

[Fonts](https://www.embedpdf.com/docs/engine/documents/fonts) covers registering fonts and using them by name. Every other
engine option is in the engine's [Setup](https://www.embedpdf.com/docs/engine/setup#options).

> **The same viewer, on your server**
>
> Move a headless viewer to CloudPDF by changing one line: `localEngine()` becomes
> `cloudEngine()`. Your plugins, components and hooks stay as they are.
> Files stay on your server, the first page shows without downloading the PDF, and fonts, limits and
> storage are the server's to configure.
>
> [Set up CloudPDF](https://www.cloudpdf.com/docs/headless/react/setup)

## Good to know

### One viewer, many documents

A viewer can hold several open documents at once, like tabs. See
[Several documents](documents/tabs).

### 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](https://www.embedpdf.com/docs/viewer/react) uses these same packages. If you start there, everything on
> these pages still applies when you move to headless.
