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.Label—Field label (respects accessibility.labelHidden)
FormField.Description—Field description from schema
FormField.Controlchildren?: ReactNode | (props) => ReactNodeDefault control or render function
FormField.Error—Validation error message
FormField.Help—Help 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.Header—Label 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
validationResolverResolver—Custom 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}
/>