Skip to main content

Mental Model

Formwright separates form concerns into three independent layers. Understanding this separation makes it easier to decide where to put custom logic.

The three layers

Schema → Runtime → Renderer

Schema (formwright/schema) — a plain data structure that describes your form: which fields exist, how they're laid out, what rules govern their behavior. No React, no side effects.

Runtime (formwright/core) — evaluates the schema against current form values and produces derived state: which fields are visible, disabled, required; what their values should be. Also pure and synchronous.

Renderer (formwright/react) — subscribes to the runtime's derived state via React context and renders UI. This is the only layer with React dependencies.

What this means in practice

I want to...I change...
Add a new field or layout sectionSchema (buildForm)
Add a show/hide ruleSchema (rules)
Change how a field type looksRenderer (custom renderer or slot)
Replace one input with a design-system componentRenderer (fieldRendererMap)
Add custom conditional logicPlugin (operator)
Add a new field effectPlugin (effect)
Load options from an APIPlugin (datasource) or registerAsyncPlugins()

How rendering works

  1. FormRuntimeProvider watches RHF field values via watch()
  2. On each change, calls runtime.evaluate(values) — synchronous, pure
  3. Produces a snapshot: { fieldState, layoutState, fieldOptions, values }
  4. Passes snapshot to all children via React context
  5. FormRuntimeRoot walks the layout tree and renders each node

Async work (remote data sources) happens in the useDatasourceOptions hook — not in evaluate().

Import paths

Formwright is published as a single formwright package with sub-path exports:

import { buildForm, field, layout } from "formwright/schema";
import { createFormRuntime } from "formwright/core";
import { FormRuntimeProvider, FormRuntimeRoot } from "formwright/react";
import { registerBasicPlugins, registerAsyncPlugins } from "formwright/plugins";

You can also import from the root:

import { buildForm, createFormRuntime, FormRuntimeProvider } from "formwright";

Customization decision guide

SituationApproach
Change label style, error style, or wrapperfieldSlots
Replace one input type with a design system componentfieldRendererMap
Build a complex domain widget (date picker, signature pad)fieldRendererMap + FieldComposer
Replace a layout node's renderinglayoutRendererMap
Add new conditional operators (fuzzy match, date comparison)Custom operator plugin
Add new field effectsCustom effect plugin
Add a new field typeCustom field plugin

See Customization for examples of each.