Skip to main content

React API

import {
FormRuntimeProvider, FormRuntimeRoot,
FormField, FormArray,
FieldComposer, ArrayComposer,
useFormField, useFormArray, useFormLayout, useDatasourceOptions, useRemoteFormDefinition,
useFormRuntime, useCreateFormRuntime, toRHFValidationRules,
createDefaultRendererMaps,
} from "formwright/react";

Compound components

FormField

Compound component for composing a single field with your own inputs. Use inside FormRuntimeProvider. Does not require FormRuntimeRoot.

import { FormField } from "formwright/react";

<FormField.Root path="email">
<FormField.Label />
<FormField.Control>
{({ value, onChange, onBlur, error, state }) => (
<input
value={typeof value === "string" ? value : ""}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
disabled={state.disabled}
aria-invalid={Boolean(error)}
/>
)}
</FormField.Control>
<FormField.Error />
<FormField.Help />
</FormField.Root>

FormField.Control with no children renders the default HTML control for the field type.

PartPropsDescription
FormField.Rootpath: stringRequired wrapper — provides field context
FormField.LabelField label (respects accessibility.labelHidden)
FormField.DescriptionField description from schema
FormField.Controlchildren?: ReactNode | (props) => ReactNodeDefault control or render function
FormField.ErrorValidation error message
FormField.HelpHelp text from schema

FormField.Control render function receives FormFieldControlRenderProps:

{
field: ResolvedFieldModel;
state: DerivedFieldState;
value: unknown;
error?: string;
onChange: (value: unknown) => void;
onBlur?: () => void;
loading?: boolean;
options?: SelectOption[];
}

FormArray

Compound component for composing array fields manually. Use inside FormRuntimeProvider.

import { FormArray, FormField } from "formwright/react";

<FormArray.Root path="items">
<FormArray.Header />
<FormArray.Items>
{(item, index) => (
<FormArray.Item key={item.id} index={index}>
<FormField.Root path={`items.${index}.name`}>
<FormField.Control />
<FormField.Error />
</FormField.Root>
<FormArray.Remove index={index} />
</FormArray.Item>
)}
</FormArray.Items>
<FormArray.Add>Add item</FormArray.Add>
</FormArray.Root>
PartPropsDescription
FormArray.Rootpath: stringRequired wrapper — provides array context
FormArray.HeaderLabel and description
FormArray.Itemschildren: (item, index) => ReactNodeIterates items, calls render function
FormArray.Itemindex: numberItem shell wrapper
FormArray.Addchildren?Add button. Disabled when array is disabled/readonly
FormArray.Removeindex: number, children?Remove button for item at index

Components

<FormRuntimeProvider>

Wraps the form in a React Hook Form FormProvider and wires the runtime engine. Must be an ancestor of FormRuntimeRoot and all form hooks.

<FormRuntimeProvider
runtime={runtime}
initialValues={{ name: "Alice" }}
hiddenFieldPolicy="clear"
>
<FormRuntimeRoot rootLayoutId="root" />
</FormRuntimeProvider>
PropTypeDefaultDescription
runtimeFormRuntimerequiredFrom createFormRuntime()
initialValuesRecord<string, unknown>{}Pre-fill field values
validationResolverResolverCustom RHF resolver (Zod, Yup, etc.)
hiddenFieldPolicy"keep" | "unregister" | "clear""keep"Behavior when a field becomes hidden
childrenReactNoderequired

hiddenFieldPolicy:

  • "keep" — hidden field value stays in form state (default)
  • "clear" — hidden field value set to undefined
  • "unregister" — field unregistered from RHF

<FormRuntimeRoot>

Resolves and renders the layout tree. Must be inside FormRuntimeProvider.

<FormRuntimeRoot
rootLayoutId="root"
fieldSlots={{ Label: MyLabel, Error: MyError }}
fieldRendererMap={{ select: MySelect, date: MyDatePicker }}
/>
PropTypeDescription
rootLayoutIdstringID of the root layout node to render
fieldSlotsFieldRendererSlotsSlot overrides for field composition
arraySlotsArrayRendererSlotsSlot overrides for array field composition
fieldRendererMapRecord<string, FieldRendererComponent>Replace full renderer by field type key
arrayFieldRendererMapRecord<string, ArrayFieldRendererComponent>Replace array renderer by key
layoutRendererMapRecord<string, LayoutRendererComponent>Replace layout renderer by type

<FieldComposer>

Renders the standard field shell (label, description, control, error, help) around your custom control. Use inside custom FieldRendererComponent implementations.

import { FieldComposer, type RenderFieldProps } from "formwright/react";

function MyCustomRenderer(props: RenderFieldProps) {
return (
<FieldComposer
field={props.field}
state={props.state}
label={props.field.uiField?.label ?? props.field.path}
description={props.field.uiField?.description}
helpText={props.field.uiField?.helpText}
error={props.error}
slots={props.slots}
>
<input
value={typeof props.value === "string" ? props.value : ""}
onChange={(e) => props.onChange(e.target.value)}
onBlur={props.onBlur}
/>
</FieldComposer>
);
}
PropTypeDescription
fieldResolvedFieldModelField model
stateDerivedFieldStateDerived state
labelstringRequired
descriptionstringOptional
helpTextstringOptional
errorstringOptional
slotsFieldRendererSlotsSlot overrides forwarded from parent
childrenReactNodeThe control element

<ArrayComposer>

Renders the array field shell (header, item shells, add/remove actions) around your custom array control.

import { ArrayComposer, type RenderArrayProps } from "formwright/react";

function MyArrayRenderer(props: RenderArrayProps) {
return (
<ArrayComposer
field={props.field}
state={props.state}
label={props.field.uiField?.label ?? props.field.path}
footer={<button onClick={() => props.append()}>Add item</button>}
slots={props.slots}
>
{props.items.map((item, index) => (
<div key={item.id}>
{/* render item fields */}
<button onClick={() => props.remove(index)}>Remove</button>
</div>
))}
</ArrayComposer>
);
}

Hooks

useFormField(path)

Returns field model, derived state, current value, and RHF-wired change handlers.

const { field, state, value, error, setValue, onBlur } = useFormField("email");
PropertyTypeDescription
fieldResolvedFieldModelSchema definition + UI metadata
stateDerivedFieldStatevisible, disabled, readonly, required
valueunknownCurrent deserialized field value
errorstring | undefinedValidation error message
setValue(v)(value: unknown) => voidUpdate field value (serializes via field plugin)
onBlur()() => voidTrigger blur/validation

DerivedFieldState shape:

{
path: string;
visible: boolean;
disabled: boolean;
readonly: boolean;
required: boolean;
loading?: boolean;
errors?: string[];
}

useFormArray(path)

Returns array state and RHF-wired append/remove handlers.

const { items, append, remove, visible, disabled, readonly, itemType } = useFormArray("lineItems");
PropertyTypeDescription
itemsArray<{ id: string; value: unknown }>Current array items with stable IDs
append(value?)(value?: unknown) => voidAdd an item (auto-builds default if omitted)
remove(index)(index: number) => voidRemove item at index
visiblebooleanDerived visibility
disabledbooleanDerived disabled state
readonlybooleanDerived readonly state
itemType"primitive" | "object"Array item type from schema

useFormLayout(id)

Returns layout node and derived layout state.

const { layout, state } = useFormLayout("shipping-section");
// state.visible — whether the layout node is visible

useDatasourceOptions(path)

Loads options for a select field. Handles loading, caching, and re-fetch on dependency changes.

const { options, loading, error } = useDatasourceOptions("country");
PropertyTypeDescription
optionsSelectOption[] | undefinedLoaded options
loadingbooleanFetch in progress
errorstring | undefinedLoad error message

useRemoteFormDefinition(options)

Loads and caches backend-provided RemoteFormPayload values. Transport-agnostic: use REST, RPC, GraphQL, or any custom loader.

const { status, isFetching, payload, error, reload, invalidate } = useRemoteFormDefinition({
key: "profile-form",
loader: ({ signal }) =>
loadRemoteForm({
url: "/api/forms/profile",
init: { signal },
}),
staleTimeMs: 60_000,
retry: 1,
});
OptionTypeDescription
keystringCache key for dedupe and reuse
loader({ key, signal }) => Promise<RemoteFormPayload>Required async loader
autoLoadbooleanAuto-load on mount. Default true
staleTimeMsnumberReuse cached payload while fresh
cacheTimeMsnumberKeep cache entry alive after load
retrynumberRetry count for failed loads
retryDelayMsnumber | ((attempt, error) => number)Retry delay or backoff function
PropertyTypeDescription
status"idle" | "loading" | "success" | "error"Overall state
isFetchingbooleanRequest currently in flight
payloadRemoteFormPayload | undefinedValidated payload
errorError | undefinedLast load error
updatedAtnumber | undefinedLast successful load timestamp
reload()() => Promise<void>Force a fresh request
invalidate()() => voidClear local cache entry

Use this hook when your schema comes from the backend. See Remote Schemas for the full integration flow.

useFormRuntime()

Returns the raw FormRuntime from context.

const runtime = useFormRuntime();

useCreateFormRuntime(options)

Creates a FormRuntime inside React. Use when the runtime depends on props or state. Memoizes the runtime instance.

function MyForm({ context }: { context: RuntimeContext }) {
const runtime = useCreateFormRuntime({
form,
plugins: registerBasicPlugins(),
context,
});

return (
<FormRuntimeProvider runtime={runtime}>
<FormRuntimeRoot rootLayoutId="root" />
</FormRuntimeProvider>
);
}

Slot interfaces

FieldRendererSlots

{
Shell?: React.ComponentType<FieldShellSlotProps>;
Label?: React.ComponentType<FieldLabelSlotProps>;
Description?: React.ComponentType<FieldDescriptionSlotProps>;
Control?: React.ComponentType<FieldControlSlotProps>;
Error?: React.ComponentType<FieldErrorSlotProps>;
Help?: React.ComponentType<FieldHelpSlotProps>;
}
SlotKey props
Shellfield, state, children
Labelfield, state, label
Descriptionfield, state, description?
Controlfield, state, value, onChange, onBlur, error?, options?, loading?, defaultControl
Errorfield, state, error?
Helpfield, state, helpText?

ArrayRendererSlots

{
Shell?: React.ComponentType<ArrayShellSlotProps>;
Header?: React.ComponentType<ArrayHeaderSlotProps>;
ItemShell?: React.ComponentType<ArrayItemShellSlotProps>;
Actions?: React.ComponentType<ArrayActionsSlotProps>;
}

FieldRendererComponent

Type for components registered in fieldRendererMap:

type FieldRendererComponent = (props: RenderFieldProps) => React.JSX.Element | null;

RenderFieldProps:

{
path: string;
field: ResolvedFieldModel;
state: DerivedFieldState;
value: unknown;
error?: string;
onChange: (value: unknown) => void;
onBlur?: () => void;
loading?: boolean;
options?: SelectOption[];
slots?: FieldRendererSlots;
}

toRHFValidationRules(dataField, required)

Converts a field schema definition to React Hook Form RegisterOptions. Used inside custom renderers that call useController directly.

import { toRHFValidationRules } from "formwright/react";

const rules = toRHFValidationRules(field.definition, state.required);
const { field: controller } = useController({ name: path, rules });

createDefaultRendererMaps()

Returns the default renderer maps. Spread these as a base and override specific keys:

import { createDefaultRendererMaps } from "formwright/react";

const { fieldRendererMap, arrayFieldRendererMap, layoutRendererMap } = createDefaultRendererMaps();

<FormRuntimeRoot
rootLayoutId="root"
fieldRendererMap={{ ...fieldRendererMap, select: MySelect }}
arrayFieldRendererMap={arrayFieldRendererMap}
layoutRendererMap={layoutRendererMap}
/>