useFormState
The central hook that wires everything together. It builds the Zod schema from your field configs, initializes React Hook Form, runs plugin lifecycle hooks, and coordinates step navigation with submission.
You probably don’t need this directly. The
<Form />component callsuseFormStateinternally. Use it only when building a fully custom form UI from scratch.
Signature
import { useFormState } from '@saastro/forms';
const {
config: resolvedConfig, // render from this, not your input config
currentStepId,
loading,
submitted,
error,
methods,
nextStep,
prevStep,
handleSubmit,
validateAndNext,
isLast,
getSuccessMessage,
getErrorMessage,
resetError,
stepHistory,
} = useFormState({ config, fields: config.fields, steps: config.steps });
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
config | FormConfig | Yes | The form configuration (from FormBuilder.build()). Fields, steps, plugins, and submit config are all read here |
fields | Fields | Yes | Accepted by the signature but currently ignored — the hook reads config.fields. Pass config.fields |
steps | Steps | No | Also currently ignored — the hook reads config.steps. Pass config.steps |
The
fieldsandstepsparameters exist for the type signature but their values are never consumed. Everything is read fromconfigafter plugin transforms. Ifconfig.stepsis missing or empty, a single syntheticdefaultstep containing all fields is created.
Return Value
| Property | Type | Description |
|---|---|---|
config | FormConfig | The plugin-transformed and locale-resolved config. Always render fields, steps, labels, and buttons from this — not your input |
currentStepId | string | Active step ID |
loading | boolean | true while submitting |
submitted | boolean | true after successful submission |
error | Error | null | Last submission error |
methods | UseFormReturn | React Hook Form instance (register, control, watch, etc.) |
nextStep | (values) => boolean | Raw advance to the next step — runs no validation and fires no onStepChange callbacks. Prefer validateAndNext |
prevStep | () => boolean | Navigate to the previous step (fires plugin and config onStepChange callbacks on success) |
handleSubmit | (data) => Promise<void> | Submit handler (runs plugin hooks + submit actions) |
validateAndNext | (e?) => Promise<void> | Validate current step, then navigate — or submit on the last step. This is the supported navigation path |
isLast | boolean | Whether the current step is the last one |
getSuccessMessage | () => string | Resolve the success message (default: '✅ Thank you!' — override via successMessage or i18n) |
getErrorMessage | () => string | Resolve the error message |
resetError | () => void | Clear the error state |
stepHistory | string[] | Stack of visited step IDs |
Render from the returned
config. It is your input config after running plugintransformConfig()and applying thelocale/i18noverlay. Reading labels, placeholders, options, or steps from the config you passed in silently drops plugin transforms and translations.
What It Does Internally
- Config resolution — If a
pluginManagerexists, runstransformConfig(); then, ifconfig.localeis set, applies thei18noverlay (see Internationalization). The result is the returnedconfig - Schema compilation — For each field, converts
ValidationRulesor raw Zod schemas into a unifiedz.object(). Chains anycustomValidatorsassuperRefinecalls — but only when the field has an explicitschemaand a plugin registers the validator; otherwise they are skipped. Fields withrequired: trueand no explicit schema get a presence-only schema (also exported asbuildRequiredOnlySchema) - Default values — Uses
field.valuewhen set; otherwise per-type defaults:falseforcheckbox/switch,[]forcheckbox-group/switch-group/button-checkbox,{ from: '', to: '' }fordaterange,[]forrepeater, and''for everything else. Exceptions:filealways defaults tonullandhiddento''—field.valueis ignored for these two (hidden values come from resolvers) - React Hook Form — Creates its own
useForminstance withzodResolver(schema)— no provider needed - Hidden + computed fields — Runs
useHiddenFieldResolversanduseComputedFieldsautomatically - Plugin lifecycle — Fires
onFormIniton mount, subscribesonFieldChangeviawatch() - validateAndNext — Validates the current step’s fields (focusing the first invalid one), then advances with conditional step routing and fires
onStepChangehooks. On the last step, it submits instead
Throws at render time if the configured
initialStepdoesn’t exist, if no steps can be resolved, or if a step references a field name missing fromconfig.fields.
Example: Custom Form UI
import { useFormState } from '@saastro/forms';
import { FormProvider } from 'react-hook-form';
function CustomForm({ config }) {
const {
config: resolvedConfig,
methods,
currentStepId,
loading,
submitted,
error,
validateAndNext,
prevStep,
isLast,
stepHistory,
getSuccessMessage,
} = useFormState({
config,
fields: config.fields,
steps: config.steps,
});
if (submitted) return <p>{getSuccessMessage()}</p>;
// Render from the RETURNED config so plugin transforms and
// i18n overlays reach the UI. Fall back to all fields when
// the config defines no steps (a synthetic step is used).
const stepFields =
resolvedConfig.steps?.[currentStepId]?.fields ??
Object.keys(resolvedConfig.fields);
return (
<FormProvider {...methods}>
<form
onSubmit={(e) => {
e.preventDefault();
validateAndNext();
}}
>
<p>Step: {currentStepId}</p>
{stepFields.map((name) => (
<input
key={name}
{...methods.register(name)}
placeholder={resolvedConfig.fields[name]?.label ?? name}
/>
))}
{stepHistory.length > 0 && (
<button type="button" onClick={prevStep}>
Back
</button>
)}
<button type="submit" disabled={loading}>
{isLast ? 'Submit' : 'Next'}
</button>
{error && <p>{error.message}</p>}
</form>
</FormProvider>
);
}
The form’s
onSubmitcallsvalidateAndNext()instead ofmethods.handleSubmit(...): RHF’shandleSubmitvalidates the entire schema, which would block intermediate steps whose later fields are still empty.validateAndNextvalidates only the current step’s fields and submits automatically on the last step.
Related
- Submitting Forms — The submit pipeline
handleSubmitruns (transforms, plugins, submit actions) - Multi-Step Forms — Step definitions and conditional routing
- Validation — Schemas, validation rules, and per-step validation behavior
- useFormStepsInfo — Step progress/status info (requires the
FormProvidershown above)