@playfast/reform-react
v1.0.1
Published
The React / DOM host for reform — mounts a renderer-neutral scene as a live React tree with fine-grained, tear-free reactivity.
Maintainers
Readme
@playfast/reform-react
The React / DOM host for reform. Mount a renderer-neutral scene as a live React tree with fine-grained, tear-free reactivity.
@playfast/reform-react is the bridge between a reform scene and the DOM. You build a scene with @playfast/reform; this package builds its runtime, renders its compositions, and keeps the tree in sync with the engine — subscribing to exactly the state slices each composition reads, and nothing more.
Install
bun add @playfast/reform-react @playfast/reform effect react react-domeffect, react (^19), and @playfast/reform are peer dependencies. Bring your own react-dom root.
Quick start
The app owns the React root; you hand Reform a closed scene and it does the rest:
import { createRoot } from 'react-dom/client'
import { Layer } from 'effect'
import { scene } from '@playfast/reform'
import { Reform } from '@playfast/reform-react'
import { AppShell } from './features/shell/shell.compose'
import { makeAppLayer } from './app/app.layer'
const AppScene = scene(AppShell, {
provide: [makeAppLayer()],
})
const container = document.getElementById('root')!
createRoot(container).render(<Reform scene={AppScene} />)Reform builds a single ManagedRuntime from the scene's closed layers for the mounted root, renders under a provider, dispatches any boot events after commit (so bus subscribers exist before events fire), and queues disposal when the scene changes or unmounts so StrictMode's synthetic cleanup can be cancelled.
If the root must produce output on its first render, prepare the runtime at an explicit ownership boundary and render the prepared handle:
import { createRoot } from 'react-dom/client'
import { prepareReform, ReformPrepared } from '@playfast/reform-react'
const prepared = prepareReform(AppScene)
createRoot(container).render(<ReformPrepared prepared={prepared} />)Create one prepared handle per React root. ReformPrepared renders immediately,
then boots and disposes the runtime from layout effects; if a prepared handle is
created but never mounted, call prepared.dispose().
Key concepts
| export | kind | purpose |
| --- | --- | --- |
| Reform | component | Mount a Scene — the primary entry point |
| prepareReform | function | Build a scene runtime before React renders |
| ReformPrepared | component | Render a prepared runtime on the first pass |
| PreparedReformRuntime | interface | Lifecycle handle returned by prepareReform |
| Compose | component | Render a single composition (memoised by class + shallow props) |
| ReformProvider | component | Provide a built runtime to child compositions |
| ReformRuntime | interface | The capabilities a host needs from a built reform runtime |
| ComposeOutsideProvider | error | <Compose> used without a surrounding <ReformProvider> |
| UnknownSlot | error | A composition was asked for a slot its manifest does not declare |
Full API reference: playbook/api.doc.md.
How reactivity works
There are no hooks to call — subscription is automatic. Compose tracks the state slices a composition reads during render and subscribes to them through useSyncExternalStore, so updates are correct under React 19's concurrent rendering (no tearing). As tracked slices change between renders, the live subscription re-points — subscribing newly-read slices and dropping stale ones — instead of tearing down and rebuilding, keeping the hot path O(1).
Slots filled by lazy features render through a feature host that drives load / mount and paints loading and retryable-failure placeholders.
This is a synchronous client host (
ManagedRuntime.runSync). The defaultReformcomponent builds after commit;prepareReform/ReformPreparedare the explicit path for first-render output.
The reform family
Core @playfast/reform · React Native host @playfast/reform-react-native · forms @playfast/reform-forms + @playfast/reform-forms-react · testing @playfast/reform-proof
License
MIT
