Basic Usage
This guide covers the four core building blocks of @dfbe/core: FormEngineProvider, useFormEngine, FieldRenderer, and the ComponentRegistry. You need all four to get a working form.
1. FormEngineProvider
The FormEngineProvider is the root context provider for the form engine. It initializes the react-hook-form instance, manages step transitions, and compiles the dynamic Zod validation schema.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
schema | FormSchema | Yes | The JSON configuration for the form. |
dataset | DatasetConfig | No | External data sources for automatic object transformation. |
defaultValues | Record<string, unknown> | No | Initial values for the form fields. |
onSubmit | (data: unknown) => void | No | Callback triggered after the final step is submitted and validated. |
children | ReactNode | Yes | Your form layout and components. |
Usage
import { FormEngineProvider } from "@dfbe/core"; const mySchema = { /* ... */ }; export default function MyForm() { const handleSubmit = (data) => { console.log("Submission successful:", data); }; return ( <FormEngineProvider schema={mySchema} onSubmit={handleSubmit} defaultValues={{ email: "user@example.com" }} > <MyFormLayout /> </FormEngineProvider> ); }
2. useFormEngine()
The useFormEngine hook provides access to the engine's internal state and navigation methods. It must be used within a component wrapped by FormEngineProvider.
Return Values
| Value | Type | Description |
|---|---|---|
schema | FormSchema | The current form schema. |
currentStep | FormStep | The currently active step definition. |
currentStepIndex | number | The index (0-based) of the active step. |
isFirstStep | boolean | True if the user is on the first step. |
isLastStep | boolean | True if the user is on the last step. |
handleNext | (e?) => Promise<void> | Validates the current step and moves forward. |
handleBack | () => void | Moves back to the previous step. |
formMethods | UseFormReturn | The raw react-hook-form methods. |
showSummary | boolean | Indicates if the form is in the "Summary" state (post-last-step). |
setShowSummary | (show: boolean) => void | Programmatically control the summary state. |
validatedData | Record<string, unknown> | The fully transformed data after the last step is submitted. Empty {} until the final step is submitted. |
onFormSubmit | (data) => void | Internal submit handler. Pass to <form onSubmit={formMethods.handleSubmit(onFormSubmit)}>. |
dataset | DatasetConfig | The raw dataset config passed to the provider. |
datasetLookups | Record<string, Map<string, unknown>> | Pre-computed O(1) lookup Maps per field. |
Example: Navigation Controls
import { useFormEngine } from "@dfbe/core"; export function Navigation() { const { handleNext, handleBack, isFirstStep, isLastStep } = useFormEngine(); return ( <div className="flex gap-4"> <button onClick={handleBack} disabled={isFirstStep}> Back </button> <button onClick={handleNext}>{isLastStep ? "Submit" : "Next Step"}</button> </div> ); }
3. FieldRenderer
The FieldRenderer is a headless component that dispatches field definitions to your UI components based on the ComponentRegistry. It is used internally by the engine — you do not need to pass props to it directly from your consumer code.
Props
| Prop | Type | Description |
|---|---|---|
field | FormFieldDefinition | The field definition from the schema. |
registry | Record<string, ResourceRegistryEntry> | Optional override of the resource registry for this field (defaults to the schema's resourceRegistry). |
uidPrefix | string | Optional prefix for generated element IDs (useful for avoiding ID collisions in repeated sections). |
Usage
import { FieldRenderer } from "@dfbe/core"; export function Section({ section }) { return ( <div> <h3>{section.title}</h3> {section.fields.map((field) => ( <FieldRenderer key={field.name} field={field} /> ))} </div> ); }
4. ComponentRegistry — Connecting Your UI
The ComponentRegistry is the critical bridge between the JSON schema's field.type string and your actual React input components. Without it, FieldRenderer has nothing to render.
Step 1: Create Adapter Components
Each adapter receives a standardized FieldAdapterProps object from the engine:
import { Controller, useFormContext } from "react-hook-form"; import type { FieldAdapterProps } from "@dfbe/core"; function TextFieldAdapter({ field, computedState }: FieldAdapterProps) { const { control } = useFormContext(); return ( <Controller name={field.name} control={control} render={({ field: rhf, fieldState }) => ( <div> <label htmlFor={field.name}>{field.label}</label> <input {...rhf} id={field.name} placeholder={field.placeholder} aria-invalid={fieldState.invalid} disabled={computedState.disabled} /> {fieldState.error && <p>{fieldState.error.message}</p>} </div> )} /> ); }
FieldAdapterProps Interface
| Prop | Type | Description |
|---|---|---|
field | FormFieldDefinition | The field definition with computed label and helperText applied. |
computedState | ComputedFieldState | visible, required, disabled, computedValue from the rule engine. |
apiOptions | OptionItem[] | Resolved options for select / radio / combobox fields. |
isApiLoading | boolean | True while the API is fetching options for this field. |
Step 2: Build the Registry Object
Map each field.type string to its adapter component:
import type { ComponentRegistry } from "@dfbe/core"; const myRegistry: ComponentRegistry = { text: TextFieldAdapter, email: EmailFieldAdapter, number: NumberFieldAdapter, textarea: TextareaAdapter, select: SelectAdapter, radio: RadioAdapter, checkbox: CheckboxAdapter, "checkbox-group": CheckboxGroupAdapter, combobox: ComboboxAdapter, "searchable-select": SearchableSelectAdapter, date: DatePickerAdapter, file: FileUploadAdapter, markdown: MarkdownAdapter, // Render read-only markdown/HTML content };
Step 3: Inject via Context & Wire Everything Together
The registry is provided via ComponentRegistryContext, not as a prop to FormEngineProvider. This is the complete minimal setup:
import { ComponentRegistryContext, FormEngineProvider, FieldRenderer, useFormEngine, } from "@dfbe/core"; // Navigation bar — reads engine state function Navigation() { const { handleNext, handleBack, isFirstStep, isLastStep } = useFormEngine(); return ( <div> <button onClick={handleBack} disabled={isFirstStep}> Back </button> <button onClick={handleNext}>{isLastStep ? "Submit" : "Next"}</button> </div> ); } // Layout that renders a step function FormLayout() { const { currentStep } = useFormEngine(); return ( <form> {(currentStep.sections ?? []) .flatMap((s) => s.fields) .map((field) => ( <FieldRenderer key={field.name} field={field} /> ))} <Navigation /> </form> ); } // Root — registry wraps everything export function MyFormPage({ schema, onSubmit }) { return ( <ComponentRegistryContext.Provider value={myRegistry}> <FormEngineProvider schema={schema} onSubmit={onSubmit}> <FormLayout /> </FormEngineProvider> </ComponentRegistryContext.Provider> ); }
Tip: Keep the registry in a separate file (e.g.
my-registry.ts) so you can swap it with a mock registry during tests without changing your form layout code.