Skip to content

React Architecture

Scope: React viewer toolkit (@scaryterry/pdfium/react, @scaryterry/pdfium/react/headless, @scaryterry/pdfium/react/editor).

See Architecture Map for a concrete module-to-layer map. See Contributor Playbook for required file shapes and test gates.

Keep React code split into clear layers:

  • src/headless/**
    • Framework-agnostic session, viewer, and editor ownership.
  • src/react/headless/**
    • Public React bindings over the headless ownership model.
  • src/react/components/**
    • Thin shipped-shell composition and public shell entry points.
  • src/react/internal/**
    • View-model helpers, rendering primitives, orchestration helpers, and panel-specific view/model logic.
  • src/react/hooks/**
    • React-facing state hooks, effects, and async lifecycle guards.

The key rule now is simple: shell behavior must not exist separately from headless behavior. Shipped shells compose the public headless surface; they do not own a second copy of the runtime.

Preferred panel shape:

  1. components/panels/<panel>.tsx
    • Thin export/wrapper only.
  2. internal/<panel>-view.tsx
    • UI rendering and event wiring.
  3. internal/<panel>-model.ts
    • Pure transformations, labels, and tab/state utilities.

This keeps JSX-heavy rendering isolated from pure model logic and simplifies race-focused unit tests.

Preferred hook shape:

  1. Hook file coordinates refs/effects and public return shape.
  2. Pure computation goes in internal/*-model.ts modules.
  3. Async guard behavior should rely on generation/request tokens instead of mutable booleans.

Examples in this repository:

  • use-visible-pages + internal/visible-pages-model.ts
  • provider lifecycle split across:
    • internal/use-pdfium-provider-controller.ts
    • internal/provider-* modules
  • Keep external store/cache operations centralized in internal modules (query-store, lifecycle helpers).
  • Keep format/copy/table concerns in internal/*-copy.ts and shared view primitives.
  • Minimize component-local side effects; prefer hook-level orchestration and explicit cleanup.
  • Is stale async completion ignored after dependency changes?
  • Are subscriptions/listeners/timers always cleaned?
  • Is pure logic separated from render wiring?
  • Are tests covering success, failure, and stale-race paths?