# useComputedFields

URL: https://docs.forms.saastro.io/docs/use-computed-fields
> Reactively derives field values from other fields — recomputes whenever a dependency changes.

# useComputedFields

Keeps computed field values in sync with the fields they depend on. Any field whose config has a `computed` property gets its value recalculated automatically — once on mount, and again whenever one of its dependencies changes.

> **You probably don't need this directly.** It runs automatically inside [`useFormState`](/docs/use-form-state) (and therefore inside `<Form />`). Call it yourself only when building a custom form renderer with your own `react-hook-form` instance.

---

## Signature

```tsx

useComputedFields(methods, fields);
```

---

## Parameters

| Parameter | Type                                     | Required | Description                                    |
| --------- | ---------------------------------------- | -------- | ---------------------------------------------- |
| `methods` | `UseFormReturn<Record<string, unknown>>` | Yes      | react-hook-form instance from `useForm()`      |
| `fields`  | `Fields`                                 | Yes      | Form field configurations from `config.fields` |

---

## Return Value

This hook returns `void`. It operates as a side-effect only — writing computed values via `methods.setValue()`.

---

## The `computed` Field Prop

Any field can become computed by adding a `computed` config:

```ts
computed?: {
  /** Field names this computation depends on */
  dependsOn: string[];
  /** Pure function that computes the value from all form values */
  compute: (values: Record<string, unknown>) => unknown;
};
```

- The field is **read-only while `computed` is active** — enforced by the renderer since 0.20.0; before that the prop was documented but not applied.
- `compute` receives **all** current form values, but recomputation is only triggered by changes to the fields listed in `dependsOn`. If `compute` reads a field, list it in `dependsOn`.
- **Two shapes.** `{ dependsOn, compute }` is the function form — code-defined configs only, since a function cannot survive JSON. `{ $expr }` is the **serializable** form: it round-trips through the Hub and the server recomputes it (see below).
- **`FieldBuilder.computed(config)`** takes either shape. With a raw config, set the prop directly and merge via `.addFields()`.

---

## How It Works

1. Collects all fields whose config has `computed` — if there are none, it does nothing
2. On mount, runs an initial computation for every computed field (so `compute` must tolerate default/empty values)
3. Subscribes to form changes via `methods.watch()`; when the changed field is in a `dependsOn` list, recomputes the affected fields from `methods.getValues()`
4. Writes each result with `setValue(name, value, { shouldDirty: false })` — computed updates never mark the form dirty
5. Skips a dependency-triggered write when the new value is strictly equal (`===`) to the current one — note that a `compute` returning a fresh object or array every time will always write. The mount-time computation (step 2) always writes, so it overrides any `defaultValue` on the computed field
6. Unsubscribes on unmount

Computed values live in form state like any other field value: they pass through validation, transforms, and the submitted payload normally.

---

## Example: Declaring a Computed Field

Since the hook runs automatically inside `<Form />`, the typical "usage" is just the field config. `FieldBuilder` has no `computed()` method, so pass the computed field as a raw config through `addFields()`:

```tsx

// Raw config — the only way to declare a computed field
const computedFields: Fields = {
  total: {
    type: 'text',
    label: 'Total (€)',
    computed: {
      dependsOn: ['quantity', 'unitPrice'],
      compute: (values) =>
        String((Number(values.quantity) || 0) * (Number(values.unitPrice) || 0)),
    },
  },
};

const config = FormBuilder.create('order')
  .addField('quantity', (f) => f.type('number').label('Quantity').required())
  .addField('unitPrice', (f) => f.type('number').label('Unit price (€)').required())
  .addFields(computedFields)
  .addStep('main', ['quantity', 'unitPrice', 'total'])
  .build();
```

> Native number inputs produce **string** values, so coerce with `Number(...)` inside `compute` (with a fallback for empty values — the initial computation runs on mount, before the user types anything).

---

## Example: Custom Form Renderer

When driving `react-hook-form` yourself instead of using `<Form />`, wire the hook manually:

```tsx

function CustomFormRenderer({ config }: { config: FormConfig }) {
  const methods = useForm<Record<string, unknown>>({ defaultValues: {} });

  // Keeps computed field values in sync with their dependencies
  useComputedFields(methods, config.fields);

  const onSubmit = (values: Record<string, unknown>) => {
    console.log(values); // includes the computed values
  };

  return (
    <form onSubmit={methods.handleSubmit(onSubmit)}>
      {/* Render fields... computed fields update as dependencies change */}
    </form>
  );
}
```

No provider is required — the hook takes `methods` explicitly.

---

## Related

- [Conditional Logic](/docs/conditional-logic) — Show, hide, or disable fields based on other values (vs. computing a value)
- [Hidden Fields](/docs/hidden-fields) — Values resolved once at mount via JSON-serializable resolvers
- [useFormState](/docs/use-form-state) — Runs this hook automatically as part of the form engine
- [FormBuilder](/docs/formbuilder) — `addFields()` for raw configs, and what keeps a config JSON-serializable

---

## Serializable expressions (`$expr`) — and why the server recomputes

The function form cannot travel. A form hosted in the Hub is JSON, so a derived
value used to end up as a plain `hidden` field filled in by the **browser** —
which means anyone could POST `precio_mes: 0.01` and be believed.

```ts
computed: { $expr: { $op: '+', args: [
  5,
  { $if: { operator: 'AND', conditions: [{ field: 'conductor', operator: 'isTrue' }] },
    then: 1.5, else: 0 },
] } }
```

Nodes: a bare `number` is the literal, `$field` reads another field, `$op` does
`+ - * /`, `$round` rounds, and `$if` branches. The `$if` test takes a normal
`ConditionGroup` — or a `$cmp` when you need to compare **numbers**.

### Use `$cmp`, not `greaterThan`, against text fields

`ConditionGroup`'s numeric operators compare the **raw** value. Against a text
field holding `"1.234,56"` that is `NaN > 0` → `false`, and your guard silently
never fires. `$cmp` runs both sides through the same coercion as the rest of the
expression:

```ts
{ $if: { $cmp: '>', left: { $field: 'prima' }, right: 0 }, then: /* … */, else: 0 }
```

### Number coercion

Text fields arrive as strings. `"1.234,56"` and `"1,234.56"` both mean 1234.56;
**a lone separator is always the decimal separator**, so `"1.234"` is 1.234.
Anything that doesn't coerce falls back to the node's `fallback` (default `0`).
Dividing by zero yields `0`. `evaluateExpression` never returns `NaN`/`Infinity`.

### Dependencies are derived, and chains resolve in one pass

You don't declare `dependsOn` for `$expr` — it's read off the expression's
leaves, including the fields inside a `ConditionGroup`. Computed fields may
reference other computed fields; evaluation is topologically ordered, so
`ahorro = mes + dto` sees this pass's `mes`, not the previous one's.

### The server wins

```ts

const raw  = await request.json();
const data = applyComputedFields(form, raw);   // server-authoritative
const result = validateFormData(form, data);
if (!result.success) return Response.json({ errors: result.errors }, { status: 422 });
await persist(data);                            // `data`, never `raw`
```

`applyComputedFields` is sync, never throws, doesn't mutate its input, and lives
in the lean `@saastro/forms/validation` subpath so a Worker can import it without
pulling React. It is deliberately **separate** from `validateFormData`: validation
returns a verdict, recomputation returns a corrected payload — and a worker that
only observes validation without blocking would otherwise keep persisting the
original body while its logs said "valid".
