# React layers

Declare a React composition as a form layer, render it to PDF, and seal it

Canonical URL: https://docs.asterdoc.com/guides/react-layers/

A React layer makes a React component the document for a form. The artifact names the component's file, the component reads the form's fields and parties, and `render()` and `seal()` produce a PDF from it. This guide builds a one-page service agreement that a client signs.

## Install the packages

**npm**

```bash
npm install @asterdoc/core @asterdoc/react @asterdoc/react-pdf react react-dom
```

**pnpm**

```bash
pnpm add @asterdoc/core @asterdoc/react @asterdoc/react-pdf react react-dom
```

**yarn**

```bash
yarn add @asterdoc/core @asterdoc/react @asterdoc/react-pdf react react-dom
```

**bun**

```bash
bun add @asterdoc/core @asterdoc/react @asterdoc/react-pdf react react-dom
```

The composition uses three installed components: `Document`, `Field`, and `Signature`. Register the `@asterdoc` namespace once, as [Installation](/components/installation) shows, then add them:

```bash
npx shadcn@4 add @asterdoc/document @asterdoc/field @asterdoc/signature
```

They land in `components/asterdoc/`, and from then on they are your source to edit.

## Declare the layer

A React layer is a file layer with MIME type `text/tsx` (or `text/jsx`). Its `path` names the composition module, relative to the artifact file.

```ts title="documents/agreement.ts"
import { a } from '@asterdoc/core'

export const agreement = a
  .form()
  .name('service-agreement')
  .version('1.0.0')
  .title('Service Agreement')
  .fields({
    service: { type: 'text', label: 'Service', required: true },
    fee: { type: 'money', label: 'Fee', required: true },
  })
  .parties({
    client: { label: 'Client', partyType: 'person', signature: { required: true } },
  })
  .fileLayer('composition', {
    mimeType: 'text/tsx',
    path: 'agreement-document.tsx',
    signatures: {
      'client-signature': { party: { role: 'client' }, type: 'signature', placement: 'flow' },
    },
  })
  .defaultLayer('composition')
  .build()
```

The layer must be a file layer. An inline layer with a React MIME type fails validation, because there is no module for it to name.

The `signatures` map makes the layer a seal target. A `flow` slot sits wherever the composition draws that party's `Signature` block, so nothing else has to describe where the signature goes.

## Write the composition

The module's default export is the component. It receives the form as `artifact` and the filled `fields`, `parties` and `annexes` as `data`, so `<Field as="image" path="annexes.<slot>">` draws an attached picture in a render and a seal.

```tsx title="compositions/agreement-document.tsx"
import type { ReactLayerComponentProps } from '@asterdoc/react-pdf'
import { Document } from '@/components/asterdoc/document'
import { Field } from '@/components/asterdoc/field'
import { Signature } from '@/components/asterdoc/signature'

export default function AgreementDocument({ artifact, data }: ReactLayerComponentProps) {
  return (
    <Document artifact={artifact} data={data}>
      <Field path="service" />
      <Field path="fee" />
      <Signature party="client" />
    </Document>
  )
}
```

`Field` prints the label and the formatted value from the artifact. `Signature` prints the client's name and the signing rule the seal measures the field from.

## Render the PDF

Core does not render React itself. Pass `reactLayerRenderers()` in `renderers`, which registers one renderer for both `text/tsx` and `text/jsx`. The `components` map binds the layer's path to the component you imported.

```ts title="render.ts"
import { writeFile } from 'node:fs/promises'
import { reactLayerRenderers } from '@asterdoc/react-pdf'
import { agreement } from './documents/agreement'
import AgreementDocument from './documents/agreement-document'

const renderers = reactLayerRenderers({
  components: { 'agreement-document.tsx': AgreementDocument },
})

const draft = agreement.fill({
  fields: {
    service: 'Website redesign',
    fee: { amount: 4800, currency: 'USD' },
  },
  parties: {
    client: { id: 'client-0', name: 'Ada Lovelace' },
  },
})

const pdf = await draft.render<Uint8Array>({ layer: 'composition', renderers })
await writeFile('agreement.pdf', pdf)
```

Without a renderer registered for the layer's MIME type, `render()` throws `UnregisteredLayerRendererError`. A path that neither `components` nor an import can bind throws `UnboundReactLayerError`.

Binding through `components` works in a bundled application, where the module is already imported. In a Node process that can load `.tsx`, you can leave out `components` and set `baseDir` to the artifact file's directory; the renderer then imports the path and takes its default export. It refuses an absolute path or one that leaves `baseDir`, because importing a module runs it. Leaving `baseDir` unset turns the import route off entirely — `process.cwd()` is never used — so an artifact you do not control should bind through `components` only.

## Seal it

Bind a signer to the client, then seal with the same `renderers`. The React renderer writes the PDF itself, so the seal needs no `SealAdapter`.

```ts
const signable = await draft
  .addSigner('client-signer', { person: { name: 'Ada Lovelace' } })
  .addSignatory('client', 'client-0', { signerId: 'client-signer' })
  .seal({ renderers })

signable.canonicalPdfBytes // the flattened PDF a signer signs
signable.canonicalPdfHash  // 'sha256:...'
signable.signatureMap      // the client's signature field, in PDF coordinates
```

The seal renders twice. On the first pass the renderer hands each `Signature` block an invisible marker, and core locates the markers in the PDF to place the fields. The second pass is the clean document that is hashed. See [Sealing](/guides/sealing-and-conversion) for slots, placements, and `prepareSeal()`.

## Preview while you edit

[`asterdoc dev`](/cli/commands/dev) previews compositions in the browser. It finds every composition under a `compositions/` directory. [`asterdoc check`](/cli/commands/check) checks one composition against its artifact. It takes an explicit target, the composition file or the artifact file, and pairs a composition with the artifact whose layer `path` points at it, or else with a same-name artifact file beside it. Both read artifacts from `.yaml`, `.yml`, or `.json` files. To use them, save the form as JSON (`agreement.toJSON()`) beside the composition and keep the layer's `path` pointing at it. The full conventions are in [Discovery](/sdk/react#discovery).

## Next steps

* [Packets](/guides/packets): seal this agreement with other documents as one signable PDF.
* [React](/sdk/react) and [React PDF](/sdk/react-pdf): the hooks, tokens, `renderPdf`, and renderer options.
* [Renderer registry](/sdk/renderers#renderer-registry) and [React layers](/concepts/rendering#react-layers): how `renderers` is consulted.
* [Components](/components): every installed component, and complete blocks such as the [purchase order](/components/blocks/purchase-order).
