# useFormState

URL: https://docs.forms.saastro.io/docs/use-form-state
> Main orchestrator hook — builds Zod schemas, sets up React Hook Form, coordinates plugins, steps, and submission.

# 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 calls `useFormState` internally. Use it only when building a fully custom form UI from scratch.

---

## Signature

```tsx

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 `fields` and `steps` parameters exist for the type signature but their values are never consumed. Everything is read from `config` after plugin transforms. If `config.steps` is missing or empty, a single synthetic `default` step 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](/docs/i18n))                         |
| `getErrorMessage`   | `() => string`            | Resolve the error message                                                                                                             |
| `resetError`        | `() => void`              | Clear the error state                                                                                                                 |
| `isSubmitting`      | `boolean`                 | Alias of `loading`: `true` during the **whole** submit pipeline (transforms, plugins, submit actions). Not RHF's `formState.isSubmitting`, which never covers the `validateAndNext` path |
| `resetSubmitState`  | `() => void`              | Clear the bespoke submit state (success screen, error, submit-actions result) — the part RHF's `methods.reset()` can't touch          |
| `stepHistory`       | `string[]`                | Stack of visited step IDs                                                                                                             |

### Dirty / validating flags — use `useFormFlags`

`isDirty`, `isValidating`, `dirtyFields`, `touchedFields` are deliberately **not** on this hook's return. RHF's `formState` is a read-subscription Proxy: reading `isDirty` inside the orchestrator would subscribe the entire form to per-keystroke churn. Use the dedicated selector hook, which pays the subscription only where it's consumed:

```tsx

const { methods } = useFormState({ config, fields: config.fields, steps: config.steps });
const { isDirty, isValidating, dirtyFields, touchedFields } = useFormFlags(methods);
```

`useFormFlags` also exposes RHF's own `isSubmitting`/`isSubmitted`/`submitCount` — those only track `methods.handleSubmit` flows (headless usage), not the package's `validateAndNext` pipeline.

> **Render from the returned `config`.** It is your input config after running plugin `transformConfig()` and applying the `locale`/`i18n` overlay. Reading labels, placeholders, options, or steps from the config you passed in silently drops plugin transforms and translations.

---

## What It Does Internally

1. **Config resolution** — If a `pluginManager` exists, runs `transformConfig()`; then, if `config.locale` is set, applies the `i18n` overlay (see [Internationalization](/docs/i18n)). The result is the returned `config`
2. **Schema compilation** — For each field, converts `ValidationRules` or raw Zod schemas into a unified `z.object()`. Chains any `customValidators` as `superRefine` calls — but only when the field has an explicit `schema` and a plugin registers the validator; otherwise they are skipped. Fields with `required: true` and no explicit schema get a presence-only schema (also exported as `buildRequiredOnlySchema`)
3. **Default values** — Uses `field.value` when set; otherwise per-type defaults: `false` for `checkbox`/`switch`, `[]` for `checkbox-group`/`switch-group`/`button-checkbox`, `{ from: '', to: '' }` for `daterange`, `[]` for `repeater`, and `''` for everything else. Exceptions: `file` always defaults to `null` and `hidden` to `''` — `field.value` is ignored for these two (hidden values come from [resolvers](/docs/hidden-fields))
4. **React Hook Form** — Creates its own `useForm` instance with `zodResolver(schema)` — no provider needed
5. **Hidden + computed fields** — Runs [`useHiddenFieldResolvers`](/docs/use-hidden-field-resolvers) and [`useComputedFields`](/docs/use-computed-fields) automatically
6. **Plugin lifecycle** — Fires `onFormInit` on mount, subscribes `onFieldChange` via `watch()`
7. **validateAndNext** — Validates the current step's fields (focusing the first invalid one), then advances with conditional step routing and fires `onStepChange` hooks. On the last step, it submits instead

> **Throws at render time** if the configured `initialStep` doesn't exist, if no steps can be resolved, or if a step references a field name missing from `config.fields`.

---

## Example: Custom Form UI

```tsx

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 `onSubmit` calls `validateAndNext()` instead of `methods.handleSubmit(...)`: RHF's `handleSubmit` validates the **entire** schema, which would block intermediate steps whose later fields are still empty. `validateAndNext` validates only the current step's fields and submits automatically on the last step.

---

## Related

- [Submitting Forms](/docs/submit) — The submit pipeline `handleSubmit` runs (transforms, plugins, submit actions)
- [Multi-Step Forms](/docs/multi-step) — Step definitions and conditional routing
- [Validation](/docs/validation) — Schemas, validation rules, and per-step validation behavior
- [useFormStepsInfo](/docs/use-form-steps-info) — Step progress/status info (requires the `FormProvider` shown above)
