---
title: "Getting Started — React"
description: "Build your own PDF viewer UI with the EmbedPDF headless libraries."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/getting-started"
---

# Getting Started

The headless libraries give you the engine and the plugin system — you own
every pixel of the UI. An app depends on two packages: the adapter for your
framework, and an engine.

## Installation

```sh
pnpm add @embedpdf/react @embedpdf/engine
```

The React adapter exposes every feature as one import line per vertical —
registration, components, and hooks travel together, and deleting the line
removes the feature from your bundle.

## Your first viewer

`localEngine()` creates the engine — synchronously, allocating nothing until
first use, so a module-scope `const engine = localEngine()` is safe (even under
SSR). Hand it to `<Viewer>`: the viewer warms it up on mount and PDFium boots
in a worker in the background, so the stage and render layer draw the pages the
moment a document is ready — no worker wiring, no lifecycle to manage. This is
the whole app:

**`basic.tsx`**

```tsx
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';

// `localEngine()` IS the engine — created synchronously, costing nothing until
// first use (no worker, no WASM). Safe at module scope, even under SSR. The
// viewer warms it up on mount, PDFium boots in the background in a worker, and
// only opening a document awaits it — the UI renders at t≈0.
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];

// The local engine opens bytes: fetch lazily, under the loading tab.
const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <div style={{ height: 500 }}>
        {/* Document UI is defined over a document — gate it on having one. */}
        <DocumentGate fallback={<p>Loading…</p>}>
          <Stage style={{ height: '100%' }}>{() => <RenderLayer />}</Stage>
        </DocumentGate>
      </div>
    </Viewer>
  );
}
```

Prefer the viewer to own the engine's lifetime — created on mount, destroyed on
unmount? Pass a thunk instead: `engine={() => localEngine()}`. See the
[Engine getting started](https://www.embedpdf.com/docs/engine/getting-started) for the ownership model,
fallback fonts, cloud, and SSR.
