> ## Documentation Index
> Fetch the complete documentation index at: https://docs.platform.embeddables.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Form instance, hooks & events

> JavaScript & server init, form instance methods, React hooks, and the analytics events Forms emits

Install, init, and walkthrough examples live in [Overview & setup](/sdks/forms/overview). **This page** is the reference for `initForms` / `initFormsServer`, the form instance methods, the React hooks, and the analytics events Forms emits. Which fields a form reads and writes depends on its schema.

<h2 id="initForms">
  Set up & use forms
</h2>

In the browser, call `initForms`; on the server, call `initFormsServer`. Each checks your Core setup once, then lets you open a form with `getForm`.

<Tabs>
  <Tab title="JavaScript">
    ```typescript theme={null}
    import { initForms } from '@embeddables/forms'

    const forms = initForms(options)
    const form = forms.getForm({ formId })
    ```
  </Tab>

  <Tab title="Server">
    <span id="server" />

    ```typescript theme={null}
    import { initEmbeddablesServer } from '@embeddables/core/server'
    import { initFormsServer } from '@embeddables/forms/server'

    const server = initEmbeddablesServer({ ...config })
    const forms = initFormsServer({ server })
    const form = forms.getForm({ formId })
    ```

    Use this when Forms runs during a server page load (SSR). You open forms the same way with `getForm`, but answers stay in memory for that request — they are not saved to the visitor's browser or sent to Analytics unless you set that up separately.

    ```typescript theme={null}
    const serverFormData = forms.getServerFormData()
    // pass into client initForms:
    // serverFormData: [{ formId: 'signup', serverFormData: serverFormData['signup'] }]
    ```

    `getServerFormData()` on the `initFormsServer` client returns the current values for every initialized form, keyed by form id. Feed each entry into the client `initForms` [`serverFormData`](#options) option to hydrate the browser form from SSR.
  </Tab>
</Tabs>

<ParamField path="options" type="InitFormsOptions" required>
  Init configuration. `core` is required; the rest are optional. See **Options** below.
</ParamField>

<ParamField path="formId" type="string" required>
  Passed to `getForm`. Must be the id of a schema registered on Core. An unknown id throws a
  `FormsError`. `getForm` returns **one instance per form id** — the first call builds it, every
  later call returns that same instance.
</ParamField>

<h3 id="options">
  Options
</h3>

Passed to `initForms`. Per-form options (`customValidations`, `serverFormData`) are arrays keyed by `formId`.

<AccordionGroup>
  <Accordion title="core" description="Initialized Core instance with registered schemas">
    ```typescript theme={null}
    import { initEmbeddables } from '@embeddables/core'
    import { initForms } from '@embeddables/forms'

    const core = initEmbeddables({ ...config, publishableKey: 'pk_sandbox_<your-key>' })
    const forms = initForms({ core })
    ```

    <ParamField body="core" type="EmbeddablesInstance" required>
      An initialized `@embeddables/core` instance with at least one form schema registered (via
      `config.forms` from `em build`, or `forms` on `initEmbeddables`). Init throws a `FormsError` if
      Core is missing required methods or has no form schemas. Forms composes on Core for identity but
      never reads or writes the identity entry.
    </ParamField>
  </Accordion>

  <Accordion title="customValidations" description="Runtime validator functions for CLI-authored schemas">
    ```typescript theme={null}
    const forms = initForms({
      core,
      customValidations: [
        {
          formId: 'signup',
          customValidations: {
            email: ({ value }) => (value.endsWith('@example.com') ? 'No example.com addresses' : null),
          },
        },
      ],
    })
    ```

    <ParamField body="customValidations" type="{ formId, customValidations }[]">
      Per-field validator functions, matched to schemas by `formId`. Use this for CLI-generated
      schemas, which cannot carry functions themselves (a schema must survive a JSON round trip; only
      `validations.custom` may be a function). Each validator receives `{ value, values }` and returns
      a string, a list of strings, or `null`. An unknown `formId` or field key throws a `SchemaError`.
    </ParamField>
  </Accordion>

  <Accordion title="serverFormData" description="SSR hydration seed from initFormsServer">
    ```typescript theme={null}
    const forms = initForms({
      core,
      serverFormData: [{ formId: 'signup', serverFormData: { email: 'maria@gmail.com' } }],
    })
    ```

    <ParamField body="serverFormData" type="{ formId, serverFormData }[]">
      Per-form values used to seed the form on init — typically the output of
      `initFormsServer().getServerFormData()`. On overlapping keys, `serverFormData` overrides
      `localStorage`; `localStorage` fills only the gaps. An unknown `formId` throws a `SchemaError`.
    </ParamField>
  </Accordion>

  <Accordion title="analyticsInstance" description="Optional analytics client for event tracking">
    ```typescript theme={null}
    import { initAnalytics } from '@embeddables/analytics'

    const analytics = initAnalytics({ core })
    const forms = initForms({ core, analyticsInstance: analytics })
    ```

    <ParamField body="analyticsInstance" type="AnalyticsInstance">
      Optional analytics client used **only** for event tracking. When provided, a successful `.set()`
      emits `data:updated` plus one `field:updated` per changed field, and `.submit()` emits
      `form:submitted` — see [Analytics events](#analytics-events). A rejected `trackEvent()` never fails
      a `.set()` / `.submit()`; the failure surfaces as `trackError` on the result. In React, the CLI
      wires Analytics into Forms automatically from Core — pass `analyticsInstance` only to override that
      default. Omitting it disables event tracking; durable saves still run when Core resolves
      persistence config.
    </ParamField>
  </Accordion>
</AccordionGroup>

<h3 id="methods">
  Methods and hooks
</h3>

**React** covers the `forms()` module factory and the hooks — register the factory on `EmbeddablesProvider` before calling hooks. **JavaScript & Server** lists methods on the form from [`initForms`](#initForms) or [`initFormsServer`](#server) — the same names on both entry points; SSR uses in-memory storage only (no `localStorage`, durable save, or Analytics on `.set()` / `.submit()`). After `set`, `submit`, or `validate`, check `result.ok` and `result.errors` — a failed validation does not throw an error.

<Tabs>
  <Tab title="React">
    <span id="hooks" />

    React bindings from `@embeddables/forms/react`. Register Forms on `EmbeddablesProvider`'s `modules` prop with the `forms()` module factory, then bind fields with these hooks. Every hook returns `null` / empty state until Forms is ready — handle that before calling instance methods.

    <AccordionGroup>
      <Accordion title="forms()" description="Module factory for EmbeddablesProvider">
        <span id="forms-module" />

        ```typescript theme={null}
        import { EmbeddablesProvider } from '@embeddables/core/react'
        import { forms } from '@embeddables/forms/react'

        <EmbeddablesProvider config={config} modules={[forms({ analyticsInstance })]}>
          {children}
        </EmbeddablesProvider>
        ```

        Returns an `EmbeddablesReactModule` for the provider's `modules` prop. The generated CLI setup
        wires this for you; pass options only to override. `core` is supplied by the provider — do not pass
        it here. Form schemas must be on `config` (from `em build`), not on `forms()`.

        <ParamField body="customValidations" type="{ formId, customValidations }[]">
          Same as [customValidations](#options) on `initForms`.
        </ParamField>

        <ParamField body="serverFormData" type="{ formId, serverFormData }[]">
          Same as [serverFormData](#options) on `initForms` — typically SSR hydration seed.
        </ParamField>

        <ParamField body="analyticsInstance" type="AnalyticsInstance">
          Optional analytics client for auto-emitted form events. The generated React setup wires Analytics
          automatically when both SDKs are installed.
        </ParamField>
      </Accordion>

      <Accordion title="useForm()" description="Reactive form, values, and errors for one form">
        ```typescript theme={null}
        const { form, values, errors } = useForm({ formId: 'signup' })
        ```

        <ParamField body="formId" type="string" required>
          Id of a declared form. No type arguments needed — the id narrows the form and the schema map comes
          from the registry `em build` augments.
        </ParamField>

        <ResponseField name="form" type="FormInstance | null">
          The shared instance for this form id, or `null` until Forms is ready. Every `useForm` with the same
          id shares one instance.
        </ResponseField>

        <ResponseField name="values" type="Partial<FormValues>">
          Reactive stored values.
        </ResponseField>

        <ResponseField name="errors" type="FieldErrors">
          Reactive per-field validation messages.
        </ResponseField>
      </Accordion>

      <Accordion title="useFormField()" description="Commit-on-blur binding for one field">
        ```typescript theme={null}
        const { value, error, setValue, onBlur } = useFormField({ form, key: 'email' })
        ```

        Typing updates local draft state via `setValue`; `form.set()` runs on `onBlur` (and only when the
        draft changed). Wire the input's `onChange` to `setValue` and its `onBlur` to `onBlur`.

        <ParamField body="form" type="FormInstance | null" required>
          The instance from `useForm` (may be `null`).
        </ParamField>

        <ParamField body="key" type="FormFieldKey" required>
          The field key to bind.
        </ParamField>
      </Accordion>

      <Accordion title="useFormErrors()" description="Just the reactive validation errors">
        ```typescript theme={null}
        const errors = useFormErrors(form)
        ```

        <ParamField body="form" type="FormInstance | null" required>
          The instance from `useForm`. Returns the reactive `FieldErrors` map.
        </ParamField>
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="JavaScript & Server">
    <span id="javascript" />

    Everything you can call on the form from `getForm` — in plain JavaScript or from `useForm` in React.

    <AccordionGroup>
      <Accordion title="set()" description="Validate and persist a patch atomically">
        ```typescript theme={null}
        const result = await form.set({ email: 'maria@gmail.com' })
        if (!result.ok) console.log(result.errors)
        ```

        Applies every key in one call or writes nothing. On success it persists locally, fires a
        best-effort durable save, and — when Analytics is wired — emits `data:updated` plus one
        `field:updated` per changed field ([Analytics events](#analytics-events)). Bind it to `blur`, a
        "next step" button, or a short debounce — never per-keystroke `onChange`.

        <ParamField body="patch" type="Partial<FormValues>" required>
          The field keys to write, typed to the schema. An empty patch resolves immediately with
          `ok: true` and does not validate, write, or emit. Unknown or non-serializable keys fail
          validation. A custom validator that throws propagates synchronously out of `.set()`.
        </ParamField>
      </Accordion>

      <Accordion title="get() / getAll()" description="Read stored values by field key">
        ```typescript theme={null}
        const email = form.get('email')
        const all = form.getAll()
        ```

        <ParamField body="key" type="FormFieldKey" required>
          For `get(key)` — a declared field key. Returns the stored value typed by the field's declared
          `type`, or `undefined` if unset or undeclared. Values are served from an in-memory copy taken
          at init and are not refreshed by writes from another instance or tab.
        </ParamField>

        `getAll()` takes no arguments and returns the stored values for every declared field.
      </Accordion>

      <Accordion title="getValueByProtocolFieldId()" description="Read a value by the field's protocolFieldId">
        ```typescript theme={null}
        const value = form.getValueByProtocolFieldId('protocol_email')
        ```

        <ParamField body="protocolFieldId" type="ProtocolFieldId" required>
          A field's declared `protocolFieldId`. Returns the stored value typed like `get()` for the
          backing field, or `undefined` when no field maps to the id or the value is unset.
        </ParamField>
      </Accordion>

      <Accordion title="validate()" description="Check values without writing or tracking">
        ```typescript theme={null}
        const result = await form.validate({ email: 'not-an-email' })
        // or validate every declared field against stored values:
        const all = await form.validate()
        ```

        Runs validation without touching storage or Analytics. With a patch, it validates only those
        keys against a merged snapshot and updates their errors; with no argument, it validates every
        declared field and replaces `errors()` wholesale.

        <ParamField body="patch" type="Partial<FormValues>">
          Optional. The keys to validate. Omit to validate the whole form. An empty patch resolves with
          `ok: true` and no error-state change.
        </ParamField>
      </Accordion>

      <Accordion title="submit()" description="Validate every field, then emit form:submitted">
        ```typescript theme={null}
        const result = await form.submit()
        if (result.ok) console.log(result.values)
        ```

        Validates every declared field, fires a best-effort durable save, and — when Analytics is wired
        — emits `form:submitted` ([Analytics events](#analytics-events)). Not idempotent: every call
        emits another event, so guard against double-clicks. Nothing is emitted when validation fails.
        Takes no arguments.
      </Accordion>

      <Accordion title="errors() / clear() / subscribe()" description="Read errors, reset the form, observe changes">
        ```typescript theme={null}
        const errors = form.errors()
        form.clear()
        const unsubscribe = form.subscribe(() => rerender())
        ```

        `errors()` returns the current per-field validation messages. `clear()` removes this form's
        stored values (other forms on the origin are untouched). `subscribe(listener)` registers a
        synchronous listener for value and error changes and returns an unsubscribe function — the React
        hooks use it internally.

        <ParamField body="listener" type="() => void" required>
          For `subscribe` — called on every value or error mutation. A listener that throws never
          affects form state.
        </ParamField>
      </Accordion>
    </AccordionGroup>
  </Tab>
</Tabs>

<h3 id="analytics-events">
  Analytics events
</h3>

Emitted automatically **only** when an `analyticsInstance` is wired (directly, or via the CLI in React). Each is a standard Analytics event — see the [Analytics event reference](/sdks/analytics/reference#events) for the full field list.

<Warning>
  When Analytics is wired, do **not** also call `analytics.trackEvent(...)` yourself for these
  events — you will double-count. Forms owns `data:updated`, `field:updated`, and `form:submitted`
  for the forms it manages.
</Warning>

<AccordionGroup>
  <Accordion title="data:updated" description="Emitted once per successful set(), carrying every changed key">
    ```typescript theme={null}
    // sent automatically on a successful form.set({ ... })
    {
      event_name: 'data:updated',
      data: { email: { value: 'maria@gmail.com', label: 'Email' } },
    }
    ```

    <ResponseField name="data" type="object">
      One entry per changed key; each has `value` (stringified, max 1024 chars) and `label` (the field's
      label, max 256 chars).
    </ResponseField>
  </Accordion>

  <Accordion title="field:updated" description="One per changed field on set()">
    ```typescript theme={null}
    // one event per changed field, alongside data:updated
    {
      event_name: 'field:updated',
      field_key: 'email',
      field_type: 'email',
      field_value: 'maria@gmail.com',
    }
    ```

    <ResponseField name="field_key" type="string">
      The changed field's key.
    </ResponseField>

    <ResponseField name="field_type" type="string">
      The field's declared type (`text`, `email`, `number`, `boolean`, `select`, `multiselect`, `json`).
    </ResponseField>

    <ResponseField name="field_value" type="string | number | boolean | object | array">
      The raw new value.
    </ResponseField>

    <ResponseField name="registry_field_id / protocol_field_id" type="string">
      Included only when the field declares a `registryId` / `protocolFieldId`.
    </ResponseField>
  </Accordion>

  <Accordion title="form:submitted" description="Emitted once per successful submit()">
    ```typescript theme={null}
    // sent automatically on a successful form.submit()
    { event_name: 'form:submitted', form_key: 'signup' }
    ```

    <ResponseField name="form_key" type="string">
      The submitted form's id. Answers are sent as separate `data:updated` / `field:updated` events, not
      on this event.
    </ResponseField>
  </Accordion>
</AccordionGroup>

<h3 id="returns">
  Returns
</h3>

`getForm` returns a `FormInstance`. Its write methods resolve to result objects that never reject on validation failure.

<ResponseField name="ok" type="boolean" required>
  `true` when the operation succeeded. `set`, `submit`, and `validate` all return it.
</ResponseField>

<ResponseField name="errors" type="FieldErrors" required>
  Per-field validation messages; an empty object means no errors.
</ResponseField>

<ResponseField name="values" type="Partial<FormValues>">
  Present on `submit` and `validate` results — the current stored values.
</ResponseField>

<ResponseField name="trackError" type="unknown">
  Present on `set` / `submit` results only when an `analyticsInstance` was configured and its
  `trackEvent` rejected. The field values are still persisted; the error is reported, never thrown.
</ResponseField>

```json theme={null}
{
  "ok": true,
  "errors": {},
  "values": { "email": "maria@gmail.com" }
}
```
