SDK

React

Last updated on

Headless React bindings, document tokens, and pagination for composing a document from a Paradoc artifact.

@paradoc/react binds React components to a Paradoc form artifact. One tree is the source of truth for the paginated browser preview and for the PDF. The package ships behavior only: providers, hooks, measurement, and page planning. It emits no styled markup and ships no stylesheet.

The visible components (Document, Field, Pages, Table, and the rest) are source you install into your project. They are documented in the Components area. This page is the reference for the runtime they import.

Installation

npm install @paradoc/react react react-dom

React 19 is required. The root entry is browser-safe: it loads no PDF engine, browser driver, CSS compiler, or font files. To check a composition or render it to PDF on the server, add @paradoc/react-pdf.

EntryPurpose
@paradoc/reactProviders, hooks, tokens, typography, furniture, and page planning
@paradoc/react/discoveryNode-only conventions that pair a composition with its artifact and sample data

Bind an artifact

ArtifactProvider scopes one document store. Hooks under it read focused values from the artifact and the data.

import { ArtifactProvider, useField, type DocumentData } from "@paradoc/react";
import type { Form } from "@paradoc/types";

function Name() {
  const name = useField("name");
  return (
    <p>
      {name.field.label}: {name.text}
    </p>
  );
}

export function Profile({ artifact, data }: { artifact: Form; data: DocumentData }) {
  return (
    <ArtifactProvider artifact={artifact} data={data}>
      <Name />
    </ArtifactProvider>
  );
}

ArtifactProvider takes:

PropTypeDescription
artifactFormThe form artifact every hook reads from
dataDocumentData{ fields, parties, annexes?, defs? }, the same render values FormData carries. Supplied defs are core's evaluated values; otherwise the provider evaluates them.
formatFormatOptionsOptional formatter, blank placeholder, partial, and progressive policy

Inputs are immutable snapshots. A provider update republishes only the selectors whose resolved value changed. Each provider creates an isolated store, including during SSR.

An installed Document component wraps ArtifactProvider and the token providers for you. Use the provider directly only when you write your own document root.

Hooks

Artifact data

HookReturns
useArtifact()The bound Form
useField(path)FieldBinding: field, raw value, formatted text, and blank (true when text is the blank placeholder)
useList(path)ListBinding: the list field, its item definition, rows, and text(index, path?) for one cell
useTotals(names)One TotalBinding (name, label, value, text) per computed definition in defs, in the order given
useParty(role)The parties filled for one declared role, always as an array
usePartyContact(role, index?, paths?)PartyContactBinding: roleLabel, nameText from the party record, and organizationText, addressText, contactText from the fields paths names (organization, address, and contact, one path or several), each left undefined when unbound or blank
useAnnex(path)AnnexBinding: the annex slot, its label, the attachment, and its text
useAnnexPicture(path)AnnexBinding plus picture, set only when the attachment is an image
useFormatter()The formatter registry, for components that format values themselves

Paths use Paradoc's shared field-path grammar: dot-separated object members and either bracket or dot notation for zero-based list indexes (lineItems[0].unitPrice and lineItems.0.unitPrice are equivalent). Empty segments and the unsafe members __proto__, prototype, and constructor are rejected. A path the artifact does not declare throws UnknownFieldPathError. A path that resolves to a fieldset or a list throws CompositeFieldPathError, because it has no single value to print. An undeclared role throws UnknownPartyRoleError, and an undeclared definition throws UnknownDefinitionError. A hook used outside ArtifactProvider throws MissingArtifactProviderError.

Signing

useSignature(role, index?, type?) returns a SignatureBinding for one signing mark: roleLabel, partyText, fieldLabel, required, the rule and dateRule strings, and the invisible marker the seal flow locates. type is "signature" (the default) or "initials". Core owns SIGNATURE_RULE and INITIALS_RULE; @paradoc/react re-exports them alongside its DATE_RULE for the rules the default components draw.

When format omits formatter or progressive, ArtifactProvider inherits that setting from the nearest FormatterProvider. A failed computed definition throws in a finished render, reports defs.<name> during a check, and prints the progressive placeholder in partial mode.

Document settings and paper

HookReturns
useDocumentTokens()The resolved DocumentTokens in scope, or the defaults outside a document
useDocumentTokensAround(children)The tokens a subtree resolves to, read without rendering it
useDocumentSettings(children)DocumentSettingsBinding: the drawn paper, tokens, and geometry
usePaperGeometry()The PageGeometry of the paper being drawn
useDrawnPaper()The DrawnPaper in scope, or null
useFitToWidth(frameRef, contentRef, width?)A Fit that scales a sheet down to its frame

Pages

HookReturns
usePagination({ budget, onPaginate })PaginationBinding: plan, measureRef, repaginate(), ready, and error
usePagePlan()The PagePlan of the page being drawn, or null outside one
usePage()The PageContextValue of the page being drawn, or null
usePageNumber(){ page, pages }, 1-based; { page: 1, pages: 1 } outside a page
useKeepVisible(id)Whether a keep belongs on the current page
useSectionVisible(id)Whether a section has content on the current page
useFontReadiness(family?){ ready, error }; without family, it is ready immediately; with one, it waits for document.fonts.ready and uses the family as the readiness cache key

Packets

Hook or functionReturns
usePdfPages(options)PdfPagesBinding: status, painted pages, and attachmentReason for a PDF part drawn in the preview. Needs the optional pdfjs-dist peer
resolvePartPlacement(input)Where a bundled part sits: placed, pending, attached, or unplaced, with a label

Document tokens

Tokens carry one document's branding. A document root (an installed Document or Bundle) takes them as tokens, and renderPdf accepts a last override for one render.

TokenTypeDefaultDescription
accentColorstringnoneColour for section headings and the emphasised total. A hex triplet, a CSS colour function, or a named colour
pageSize"letter" | "a4""letter"The paper
marginPxnumber48The margin on all four sides, in CSS pixels
logoUint8Array | stringnoneThe organization's mark. Bytes become a data: URI; keep it small
dir"ltr" | "rtl""ltr"Which way lines run
langstring"en"BCP-47 language tag exposed on the document root
typography{ scale?, flow? }regular / regularThe document's rhythm (below)
<Document
  artifact={artifact}
  data={data}
  tokens={{ accentColor: "#1d4ed8", pageSize: "a4", marginPx: 64, lang: "en-GB" }}
>
  {children}
</Document>

pageSize, marginPx, dir, lang, and typography are root-only (ROOT_ONLY_TOKEN_KEYS). Set them on the outermost root. A Document inside a Bundle that sets one throws NestedPaperTokenError. A root that resolves one differently from the paper drawing it throws RootTokenMismatchError. A value no renderer can act on throws InvalidDocumentTokenError, naming the token.

PAGE_SIZES gives each sheet in CSS pixels (letter is 816 by 1056, A4 is 794 by 1123). pageGeometry(tokens) adds the margin and the content box, and resolveDocumentTokens(...layers) resolves a stack of inputs over DEFAULT_DOCUMENT_TOKENS.

Typography

typography has two knobs, each compact, regular, or roomy (TYPOGRAPHY_LEVELS). scale moves the size and leading of every text role. flow moves the space between blocks at the root. DEFAULT_TYPOGRAPHY is regular for both.

A level is one step along the Tailwind scale, not a number, so every class it produces is one the default PDF engine accepts. scaleTextClasses(classes, scale) and flowGapClasses(classes, flow) rewrite a class string for a level, which is how copy-owned components follow the token. See Typography for how the installed components apply it.

Page furniture

Furniture is what every page carries outside the flow. The same PageFurniture object goes to the preview's Pages and to renderPdf, so both outputs draw one declaration.

interface PageFurniture {
  header?: ReactNode; // drawn in the top margin of every page
  footer?: ReactNode; // drawn in the bottom margin of every page
  stamp?: ReactNode; // drawn across the whole page, behind the content
}

Bands are drawn inside the margin, so furniture never changes the page plan. A header or footer taller than its margin, or a stamp taller than the sheet, throws PageFurnitureOverflowError, naming the slot. A stamp with a word or a whitespace-nowrap line wider than the sheet throws PageStampTooWideError. An engine that cannot draw a declared slot throws UnsupportedFurnitureError, and content it cannot draw in a slot throws UnsupportedFurnitureContentError.

usePageNumber prints the current page inside furniture. The installed PageNumber component uses it and marks its slots with PAGE_COUNTER_ATTRIBUTE so the PDF engine fills the counters itself. Usage with Pages is on the Pages component page.

Pagination

The preview paginates by measuring. Every KeepTogether renders with a data-keep-id, measureKeeps(root) reads their heights, and planPages(keeps, budget) assigns them to pages that fit the content height. usePagination runs both after the fonts are ready and again whenever the measured tree resizes.

import { usePagination, usePaperGeometry } from "@paradoc/react";
import type { ReactNode } from "react";

function PageCount({ children }: { children: ReactNode }) {
  const { contentHeightPx } = usePaperGeometry();
  const { plan, measureRef, ready } = usePagination({ budget: contentHeightPx });
  return (
    <>
      <div ref={measureRef} style={{ position: "absolute", visibility: "hidden" }}>
        {children}
      </div>
      <p>{ready && plan ? `${plan.pages.length} pages` : "Measuring"}</p>
    </>
  );
}

A PagePlan lists the keep ids on each page (pages), the repeated table headers (repeats), the sections on each page, the keep that starts each page after the first (breaks), keeps too tall for a page (oversize), the budget, and the measured fonts. Pass the plan to renderPdf so the PDF breaks where the preview did and embeds the measured fonts. The installed Pages component does all of this and exposes the plan through onPaginate.

Errors

ErrorThrown when
MissingArtifactProviderErrorA hook runs outside ArtifactProvider
UnknownFieldPathError / InvalidFieldPathErrorA path is not declared, or does not follow the shared dot-and-bracket grammar
CompositeFieldPathErrorA path resolves to a fieldset or list
UnknownPartyRoleError / PartyIndexOutOfRangeErrorA role is not declared, or has fewer parties than the index
UnknownDefinitionErrorA useTotals name is not in defs
UnknownAnnexErrorAn annex slot is not declared
InvalidListValueErrorA list field holds something other than an array
ArtifactFieldFormatErrorA serializer rejects a value that carries data
InvalidDocumentTokenErrorA token value is invalid
NestedPaperTokenError / RootTokenMismatchErrorA nested document sets a root-only token, or a root disagrees with the paper drawing it
MultipleDocumentRootsErrorMore than one document root sits under one piece of page furniture
PageFurnitureOverflowError / PageStampTooWideErrorFurniture does not fit
InvalidPagePlanInputErrorplanPages gets invalid keeps or budget
MissingImageSourceError / MissingImageSizeError / UndecodableImageErrorAn image cannot be drawn

Discovery

@paradoc/react/discovery holds the project conventions that paradoc dev and paradoc check share, for authoring tools that need the same answers. It reads files and artifacts only; it never loads a composition.

  • A composition is a .tsx or .jsx file. Its default export is the component. discoverCompositions and paradoc dev find compositions under a compositions/ directory; paradoc check takes the file you name.
  • Its artifact is the one whose text/tsx or text/jsx layer resolves to the file. If none does, an artifact file of the same name beside it is used.
  • Its sample data is a sibling <name>.sample.{ts,tsx,js,mjs,jsx} default export, or else a sample export on the composition itself.
import { discoverCompositions } from "@paradoc/react/discovery";

for (const entry of await discoverCompositions(process.cwd())) {
  console.log(entry.id, entry.artifact?.relative, entry.problems);
}

discoverCompositions(root) returns one DiscoveredComposition per file, with its artifact, samples, and any problems. findCompositionArtifact(root, file), siblingArtifact(root, file), and sampleSources(root, file) answer the same questions for one file.

On this page