# React Client (https://form.dimah.dev/docs/react)



`@dimah-form/react` is a headless fill-session runtime. It tracks visible fields, local answers, draft autosave, and submit state. It does not ship inputs or CSS.

***

## Setting Up the Client [#setting-up-the-client]

Create a shared client instance configured with your server's base path and type definition:

```ts title="lib/form-client.ts"
"use client";

import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
});

export const { useFormClient, useFormResponse } = formClient;
```

***

## The `useFormResponse` Hook [#the-useformresponse-hook]

The `useFormResponse` hook connects your UI to a questionnaire fill session:

```tsx title="components/questionnaire.tsx"
"use client";

import { emptyToNull, fieldLabel, fieldOptions } from "@dimah-form/react";
import type { FormSnapshot, ResponseRecord } from "@dimah-form/react";
import { useFormResponse } from "@/lib/form-client";
import type { Form } from "@/lib/form";

interface QuestionnaireProps {
  form: FormSnapshot;
  response?: ResponseRecord;
}

export function Questionnaire({ form, response }: QuestionnaireProps) {
  const q = useFormResponse<Form["$Infer"]["answers"]["onboarding"]>({
    snapshot: form,
    response, // Pass existing row when editing an existing draft
    autosave: { debounceMs: 1000 },
    validate: "submit", // "submit" (default) or "change"
    onSubmitted: (row) => {
      console.log("Successfully submitted:", row.id);
    },
  });

  if (q.inactive) {
    return <p>This form is currently unavailable.</p>;
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void q.submit();
      }}
      className="mx-auto max-w-md space-y-4"
    >
      <h2 className="text-xl font-bold">{form.title}</h2>

      {/* Render visible fields based on showWhen logic */}
      {q.visibleFields.map((field) => {
        const binding = q.field(field.id);

        return (
          <div key={field.id} className="space-y-1">
            <label className="block text-sm font-medium">
              {fieldLabel(field)}
              {binding.required && <span className="ml-1 text-red-500">*</span>}
            </label>
            <input
              type="text"
              value={typeof binding.value === "string" ? binding.value : ""}
              disabled={binding.disabled}
              onChange={(e) => binding.onChange(emptyToNull(e.target.value))}
              className="w-full rounded border p-2"
            />
            {binding.error && (
              <p className="text-xs text-red-500">{binding.error}</p>
            )}
          </div>
        );
      })}

      {q.error && <p className="text-sm text-red-500">{q.error}</p>}

      <div className="flex gap-2">
        <button
          type="button"
          disabled={q.pending !== undefined || q.locked}
          onClick={() => void q.saveDraft()}
          className="rounded border px-4 py-2"
        >
          {q.pending === "save" ? "Saving..." : "Save Draft"}
        </button>

        <button
          type="submit"
          disabled={q.pending !== undefined || q.locked}
          className="rounded bg-black px-4 py-2 text-white"
        >
          {q.pending === "submit" ? "Submitting..." : "Submit"}
        </button>
      </div>
    </form>
  );
}
```

***

## Session State Reference [#session-state-reference]

`useFormResponse` returns a comprehensive state object:

| Property              | Type                                                                    | Description                                                                         |
| :-------------------- | :---------------------------------------------------------------------- | :---------------------------------------------------------------------------------- |
| **`q.visibleFields`** | `FormField[]`                                                           | Fields that satisfy `showWhen` conditions based on current answers.                 |
| **`q.answers`**       | `TAnswers`                                                              | Current in-memory dictionary of answers.                                            |
| **`q.status`**        | `"draft" \| "submitted" \| "abandoned"`                                 | Lifecycle status of the current response.                                           |
| **`q.dirty`**         | `boolean`                                                               | `true` if local answers have unpersisted modifications.                             |
| **`q.pending`**       | `"save" \| "submit" \| "reopen" \| "abandon" \| "refresh" \| undefined` | Active asynchronous mutation in flight.                                             |
| **`q.locked`**        | `boolean`                                                               | `true` if `status !== "draft"` (response is frozen from edits).                     |
| **`q.error`**         | `string \| undefined`                                                   | Top-level error message from the last failed operation.                             |
| **`q.issues`**        | `Record<string, string>`                                                | Map of field ID to human-readable validation error message.                         |
| **`q.issueCodes`**    | `Record<string, string>`                                                | Map of field ID to machine-readable issue code (e.g., `"REQUIRED"`, `"TOO_SHORT"`). |
| **`q.completion`**    | `{ answered, total, percent }`                                          | Progress metrics for visible fields.                                                |
| **`q.inactive`**      | `boolean`                                                               | `true` if the form status is not `"active"` and no existing response was loaded.    |

***

## Actions & Methods [#actions--methods]

### `q.field(id)` [#qfieldid]

Returns a typed `FormFieldBinding` helper object for connecting an input control:

```ts
const {
  field, // The FormField definition object
  value, // Current answer value (or undefined)
  error, // Error message string for this field
  errorCode, // Issue code (e.g. "REQUIRED")
  disabled, // boolean (true when locked or pending)
  required, // boolean
  onChange, // (value: unknown) => void
  onBlur, // () => void
} = q.field("fullName");
```

### `q.setAnswer(id, value)` [#qsetanswerid-value]

Directly sets an answer value in the local state. Passing `null` removes the answer key.

### `q.saveDraft(options?)` [#qsavedraftoptions]

Explicitly saves the current answers to the backend as a draft with optimistic concurrency (`updatedAt`) checks.

### `q.submit(options?)` [#qsubmitoptions]

Validates all visible required fields against the snapshot schema. If valid, persists answers and locks the response to `"submitted"`.

### `q.reopen()` [#qreopen]

Unlocks a previously `"submitted"` or `"abandoned"` response back to `"draft"`.

### `q.abandon()` [#qabandon]

Marks the current draft as `"abandoned"`, preventing further edits.

### `q.refresh()` [#qrefresh]

Fetches the latest response record from the server and syncs local state.

### `q.reset()` [#qreset]

Resets in-memory answers back to the last persisted server state.

***

## Advanced Features [#advanced-features]

### 1. Automatic Draft Autosave [#1-automatic-draft-autosave]

Enable debounced background autosaving so users never lose progress:

```tsx
const q = useFormResponse({
  snapshot: form,
  autosave: { debounceMs: 1500 }, // Debounce timer in milliseconds
});
```

### 2. Validation Timing (`validate`) [#2-validation-timing-validate]

* **`"submit"` (default)**: Validates required fields only when the user attempts to submit. Once submitted, subsequent edits immediately clear resolved errors.
* **`"change"`**: Re-validates fields in real-time as the user types.

```tsx
const q = useFormResponse({
  snapshot: form,
  validate: "change",
});
```

### 3. Resuming Existing User Drafts [#3-resuming-existing-user-drafts]

If your users are authenticated, pass `resume: true` with a `respondentId` to automatically reconnect to their existing draft:

```tsx
const q = useFormResponse({
  snapshot: form,
  respondentId: session.user.id,
  resume: true,
});
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Widgets" href="/docs/widgets" description="Build a reusable widget dispatcher with Tailwind CSS and shadcn/ui." />

  <Card title="Auth" href="/docs/auth" description="Protect endpoints and associate respondentId with server sessions." />

  <Card title="Errors" href="/docs/errors" description="Handle validation issue codes and internationalize error messages." />
</Cards>
