---
description: Conventions for form files. Covers file structure, validation, default values, submission, and multi-step patterns. Applies to any feature using react-hook-form.
paths:
  - "src/**/forms/**/*"
  - "src/**/*Form/index.tsx"
---

# Form Conventions

Rules for creating and modifying forms. Applies to any feature using react-hook-form.

---

## 1. File structure

Every form lives in its own folder under `forms/`:

```
Feature/
└── forms/
    └── MyForm/
        ├── index.tsx        # component entry
        ├── types.ts         # form-specific types
        ├── constants.ts     # field IDs as typed constants
        ├── schemas.ts       # Zod schema + resolver
        ├── fields.ts        # (optional) translated labels, placeholders, options, UI props
        └── utils/
            ├── values.ts    # getDefaultValues(), getDefaultValuesFromApi()
            ├── transforms.ts          # form ↔ API conversions
            ├── validations.ts         # custom Zod refinements (optional)
            └── fields.ts              # dynamic field definitions, e.g. conditional field arrays (optional)
```

Create `components/` inside the form folder when a section has its own queries, complex UI, or groups multiple visually related fields. A dedicated `XSection` component is preferred over inlining in `content.tsx` whenever the block has a distinct visual container or combines 3+ related elements (e.g. info alert + selector + helper text + dynamic list). Section components follow the `{ disabled, fields }` prop pattern — same as shared sections like `AssigneeDescriptionSection` or `DueDateSection`.

### `fields.ts` (root-level)

Use when label, placeholder, option list, or component prop computation would clutter `index.tsx`. It exports a factory function that receives `t()` and any runtime options and returns a typed object:

```typescript
// fields.ts
export const getMyFormFields = (t: TCustomFunction, options: { disabled: boolean }) => ({
  name: {
    label: t('namespace:name_label'),
    placeholder: t('namespace:name_placeholder'),
  },
  status: {
    options: STATUS_OPTIONS.map(o => ({ ...o, label: t(o.label) })),
    disabled: options.disabled,
  },
});
```

```tsx
// index.tsx
const fields = useMemo(() => getMyFormFields(t, { disabled }), [t, disabled]);
// <FormTextField label={fields.name.label} ... />
```

Keep `fields.ts` out of `utils/` — it is a presentation concern, not a data utility.

---

## 2. Validation

- **Always use Zod.** Never use Yup or other schema libraries.
- Define the schema in `schemas.ts` as a factory function that receives `t()`.
- Wire it to react-hook-form using `zodResolver` from `@hookform/resolvers/zod`.

```typescript
// schemas.ts
import { z } from 'zod';
import type { TCustomFunction } from 'src/utils/i18n';

export const createMyFormSchema = (t: TCustomFunction) =>
  z.object({
    name: z.string().min(1, t('validations:required_field')),
  });

// index.tsx
import { zodResolver } from '@hookform/resolvers/zod';
import { createMyFormSchema } from './schemas';

const form = useForm({
  resolver: zodResolver(createMyFormSchema(t)),
});
```

---

## 3. Default values

**The form component must receive `defaultValues` already parsed and ready. It must never fetch data nor call `reset()` when data arrives.**

### `defaultValues` vs `values`

- Use **`defaultValues`** in creation flows (data is set once when the form mounts).
- Use **`values`** in edit flows where the form must stay in sync with server data reactively (e.g. when the query refetches in the background).

### Async default values (e.g. edit form opened by ID)

1. Create a **provider** component as the entry point.
2. The provider fetches, handles loading/error, and transforms the API response to form shape.
3. Only when data is ready, the provider renders the form with `defaultValues` as a prop.

```
MyFormProvider   ← fetches, transforms, handles loading/error
  └── MyForm     ← receives defaultValues, never fetches, never calls reset()
```

Optionally, split further when the form lives in a drawer or its fields are reused elsewhere:

```
MyFormDrawer          ← chrome, open/close state
  └── MyFormProvider  ← fetches, transforms
        └── MyForm    ← useForm, submit
              └── MyFormContent  ← fields only, reads via useFormContext()
```

Create `MyFormContent` only if the fields need to be reused in another context.

### Resetting state on close

`MyFormDrawer` must **unmount** the Provider when closed. This guarantees a fresh form state the next time it opens. Use a `key` prop or conditional rendering:

```tsx
// YES — unmounts on close, fresh state on reopen
{isOpen && <MyFormProvider onClose={handleClose} />}

// Also valid — key changes force remount
<MyFormProvider key={entityId} onClose={handleClose} />
```

---

## 4. Submission

`onSubmit` must be as thin as possible — just call the mutation with the raw form values. The `mutationFn` owns the full pipeline: transform, validate output shape, call the API. This way any failure (Zod parse error, transform error, network error) goes through React Query's error flow.

```tsx
// YES — onSubmit passes raw values; mutationFn owns transform + request
const { mutateAsync, isPending } = useMyMutation();

const onSubmit = form.handleSubmit((data) => mutateAsync(data));

// mutation definition
const useMyMutation = () =>
  useMutation({
    mutationFn: (data: MyFormValues) => {
      const payload = transformFormToRequest(data); // Zod errors caught here
      return api.post('/endpoint', payload);
    },
    onSuccess: () => {
      onClose();
      showSuccessToast();
    },
  });

// NO — transform in onSubmit bypasses React Query error flow
const onSubmit = form.handleSubmit(async (data) => {
  const payload = transformFormToRequest(data); // Zod error escapes React Query
  await mutateAsync(payload);
});
```

- `transformFormToRequest` lives in `utils/transforms.ts`.
- Success callbacks (`onSuccess`, `onClose`, toast notifications) go in the mutation definition, not in `onSubmit`.

---

## 5. Form field components (material-hu)

material-hu provides **form-aware** variants of input components. These components **already include `Controller` and read form context (`useFormContext`) internally** — do NOT wrap them in `<Controller>`, do NOT pass `control`, and do NOT pass `required` as a prop (required/optional is determined by the Zod schema via the resolver).

| Component | Import |
|-----------|--------|
| `FormInputClassic` | `@material-hu/components/composed-components/Inputs/FormInputClassic` |
| `FormSelectClassic` | `@material-hu/components/composed-components/Selects/FormSelectClassic` |
| `FormAutocomplete` | `@material-hu/components/composed-components/Autocomplete/FormAutocomplete` |
| `FormDatePickerClassic` | `@material-hu/components/composed-components/DatePickers/FormDatePickerClassic` |
| `FormSwitchClassic` | `@material-hu/components/composed-components/Switches/FormSwitchClassic` |
| `FormCheckbox` | `@material-hu/components/composed-components/Checkboxes/FormCheckbox` |

Usage — pass only `name` and presentational props:

```tsx
// YES — Form* reads context internally
<FormInputClassic name="title" label="Título" />

// NO — double Controller wrapping
<Controller name="title" control={control}
  render={({ field }) => <FormInputClassic {...field} label="Título" />}
/>

// NO — control and required are handled by the resolver, not as props
<FormInputClassic name="title" control={control} required label="Título" />
```

Use raw `<Controller>` only when rendering a **non-Form*** component (e.g. a custom slider, a third-party picker).

---

## 6. Reading form state in subcomponents

- Use **`useFormContext()`** to access the form inside subcomponents that are always rendered within the same form tree.
- Pass the form as a **prop** only when the subcomponent is reused outside of a form context.

```tsx
// Subcomponent always inside the form — use context
function NameField() {
  const { control } = useFormContext<MyFormValues>();
  return <Controller name="name" control={control} render={...} />;
}

// Reusable standalone component — accept form as prop
function MyFormContent({ control }: { control: Control<MyFormValues> }) { ... }
```

---

## 7. Multi-step forms (stepper / wizard)

Choose the pattern based on how the steps relate to each other:

| Scenario | Pattern |
|----------|---------|
| Steps are independent (each validates and submits its own data) | **One form per step** — each step has its own `useForm` and calls `onNextStep(data)` |
| Everything submits at once and validation is globally shared | **Single parent form** — one `useForm` at stepper level; steps read/write via `useFormContext()` |
| One step drives the fields or validation rules of others | **Two contexts** — one form at the stepper for the driving state; one form per step for its own fields |
| Aggregation is simple and no validation is needed at the parent | **Props (state + setters)** — parent holds plain state and passes values and setters down explicitly |

For "one form per step": the parent stepper holds aggregated state and passes `defaultValues` or IDs to subsequent steps.
