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.