@playfast/reform-react
v1.4.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 scene factory 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 makeAppScene = () =>
scene(AppShell, {
provide: [makeAppLayer()],
})
const container = document.getElementById('root')!
createRoot(container).render(<Reform makeScene={makeAppScene} />)Reform invokes the factory after commit, builds one ManagedRuntime from the
resulting scene's closed layers, renders under a provider, and dispatches boot
events once bus subscribers exist. The factory and options are mount inputs: new
references on a parent re-render are ignored. Remount with a React key to build
a different scene. Unmount queues disposal so StrictMode's synthetic cleanup can
be cancelled without orphaning or rebuilding the runtime.
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(makeAppScene())
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 | Build and mount one Scene per React mount — 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 |
| useReformRuntime | hook | Read the exact runtime from the nearest provider |
| 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
Compose subscription is automatic. It 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).
Package-owned React integrations that need direct access to native stores or events
can read the provider's exact handle with useReformRuntime. The hook resolves the
nearest provider, including a lazy feature's nested child runtime, and throws
ComposeOutsideProvider when used outside a provider.
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
