---
url: https://formhaus.dev/recipes/derived-values.md
description: >-
  Show an invoice total that updates on every value change, and fill a due date
  from the issue date until the user edits it. Formhaus React and Vue renderers.
---

# How do I derive values from what the user enters?

Read `onValuesChange` (React), `@values-change` or the handle's `values` (Vue) for anything computed from the form, such as a total. Use `onFieldChange` (React) or `fieldChange` (Vue) with `setValue` to fill one field from another until the user edits it. Requires `@formhaus/react` or `@formhaus/vue` 0.16.

## Definition

An invoice with line items and a due date that follows the issue date by 14 days.

```json
{
  "$schema": "https://formhaus.dev/schema/form-definition.json",
  "formatVersion": "1.1",
  "id": "invoice",
  "title": "New invoice",
  "submit": { "label": "Create invoice" },
  "fields": [
    { "key": "client", "type": "text", "label": "Client", "validation": { "required": true } },
    { "key": "issueDate", "type": "date", "label": "Issue date", "validation": { "required": true } },
    {
      "key": "dueDate",
      "type": "date",
      "label": "Due date",
      "helperText": "14 days after the issue date until you change it",
      "validation": { "required": true }
    },
    {
      "key": "lines",
      "type": "group",
      "label": "Lines",
      "itemLabel": "Line {index}",
      "addLabel": "Add line",
      "defaultItems": 1,
      "validation": { "required": true },
      "fields": [
        { "key": "description", "type": "text", "label": "Description", "validation": { "required": true } },
        { "key": "quantity", "type": "number", "label": "Quantity", "defaultValue": 1, "validation": { "required": true, "min": 1 } },
        { "key": "price", "type": "number", "label": "Unit price", "validation": { "required": true, "min": 0 } }
      ]
    }
  ]
}

```

## React

```tsx
import { useRef, useState } from 'react';
import { FormRenderer, type FormRendererHandle } from '@formhaus/react';
import { parseDefinition } from '@formhaus/core';
import invoice from './invoice.json';

const definition = parseDefinition(invoice);

type Line = { quantity?: number | null; price?: number | null };

function total(lines: unknown) {
  return ((lines as Line[] | undefined) ?? []).reduce((sum, line) => sum + (line.quantity ?? 0) * (line.price ?? 0), 0);
}

function addDays(date: string, days: number) {
  const next = new Date(`${date}T00:00:00Z`);
  next.setUTCDate(next.getUTCDate() + days);
  return next.toISOString().slice(0, 10);
}

export function InvoiceForm({ onDone }: { onDone: (values: Record<string, unknown>) => void }) {
  const form = useRef<FormRendererHandle>(null);
  const dueDateEdited = useRef(false);
  const [amount, setAmount] = useState(0);

  return (
    <>
      <FormRenderer
        ref={form}
        definition={definition}
        onSubmit={onDone}
        onValuesChange={(values) => setAmount(total(values.lines))}
        onFieldChange={(key, value) => {
          if (key === 'dueDate') dueDateEdited.current = true;
          if (key === 'issueDate' && !dueDateEdited.current && typeof value === 'string' && value)
            form.current?.setValue('dueDate', addDays(value, 14));
        }}
      />
      <p>Total: {amount.toFixed(2)}</p>
    </>
  );
}
```

## Vue

```vue
<script setup lang="ts">
import { computed, ref } from 'vue';
import { FormRenderer, type FormRendererHandle } from '@formhaus/vue';
import { parseDefinition } from '@formhaus/core';
import invoice from './invoice.json';

const definition = parseDefinition(invoice);
const emit = defineEmits<{ done: [values: Record<string, unknown>] }>();
const form = ref<FormRendererHandle>();
let dueDateEdited = false;

type Line = { quantity?: number | null; price?: number | null };

const amount = computed(() =>
  ((form.value?.values.lines as Line[] | undefined) ?? []).reduce((sum, line) => sum + (line.quantity ?? 0) * (line.price ?? 0), 0),
);

function addDays(date: string, days: number) {
  const next = new Date(`${date}T00:00:00Z`);
  next.setUTCDate(next.getUTCDate() + days);
  return next.toISOString().slice(0, 10);
}

function onFieldChange(key: string, value: unknown) {
  if (key === 'dueDate') dueDateEdited = true;
  if (key === 'issueDate' && !dueDateEdited && typeof value === 'string' && value)
    form.value?.setValue('dueDate', addDays(value, 14));
}
</script>

<template>
  <FormRenderer ref="form" :definition="definition" @field-change="onFieldChange" @submit="(values) => emit('done', values)" />
  <p>Total: {{ amount.toFixed(2) }}</p>
</template>
```

`@values-change="(values) => …"` works too, the same way as `onValuesChange` in React.

## Use a computed value later in the form

Write the computed value to a `hidden` field with `setValue`, then recall it in a text with `{{total}}` and branch on it with a `gte` condition.

A booking that adds two seat counts into `total`. The next step shows it with `{{total}}` and asks for a group request from 10 seats. Requires `@formhaus/core` 0.21 and `formatVersion` `"1.5"`.

```json
{
  "$schema": "https://formhaus.dev/schema/form-definition.json",
  "formatVersion": "1.5",
  "id": "booking-total",
  "title": "Book seats",
  "submit": { "label": "Book" },
  "steps": [
    {
      "id": "seats",
      "title": "Seats",
      "fields": [
        { "key": "adults", "type": "number", "label": "Adults", "defaultValue": 1, "validation": { "required": true, "min": 0 } },
        { "key": "children", "type": "number", "label": "Children", "defaultValue": 0, "validation": { "required": true, "min": 0 } },
        { "key": "total", "type": "hidden" }
      ]
    },
    {
      "id": "summary",
      "title": "Booking for {{total}}",
      "description": "{{adults}} adults and {{children}} children.",
      "fields": [
        {
          "key": "groupNote",
          "type": "textarea",
          "label": "Group request",
          "show": [{ "field": "total", "gte": 10 }]
        }
      ]
    }
  ]
}

```

A `hidden` field holds text, so write the sum as a string. `gte` compares it as a number.

### React

```tsx
import { useRef } from 'react';
import { FormRenderer, type FormRendererHandle } from '@formhaus/react';
import { parseDefinition } from '@formhaus/core';
import booking from './booking-total.json';

const definition = parseDefinition(booking);

export function BookingForm({ onDone }: { onDone: (values: Record<string, unknown>) => void }) {
  const form = useRef<FormRendererHandle>(null);
  return (
    <FormRenderer
      ref={form}
      definition={definition}
      onSubmit={onDone}
      onValuesChange={(values) => {
        const total = String(Number(values.adults ?? 0) + Number(values.children ?? 0));
        if (values.total !== total) form.current?.setValue('total', total);
      }}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { FormRenderer, type FormRendererHandle } from '@formhaus/vue';
import { parseDefinition } from '@formhaus/core';
import booking from './booking-total.json';

const definition = parseDefinition(booking);
const emit = defineEmits<{ done: [values: Record<string, unknown>] }>();
const form = ref<FormRendererHandle>();

function onValuesChange(values: Record<string, unknown>) {
  const total = String(Number(values.adults ?? 0) + Number(values.children ?? 0));
  if (values.total !== total) form.value?.setValue('total', total);
}
</script>

<template>
  <FormRenderer ref="form" :definition="definition" @values-change="onValuesChange" @submit="(values) => emit('done', values)" />
</template>
```

* The computation needs app code. A form published as JSON only, such as a hosted form, cannot compute values.
* Write only when the value differs, so the callback does not trigger itself.
* See [Answer recall](/guide/fields#answer-recall) and [Conditions](/guide/conditions).

## How it works

* `onValuesChange` and `valuesChange` fire once on mount with the initial values, then once for every value-changing operation: user edits, `setValue` from the handle, adding or removing group items, values cleared or restored by conditions, and `reset`. A change that clears several hidden fields fires once. Errors, loading and step navigation do not fire them, and neither does writing a value equal to the current one.
* They may fire when the values end up unchanged: a write to a hidden field, a `reset` to equal content, a skipped step and undoing that skip when the submit fails.
* They fire again when a new `definition` creates a new form. They never fire during server rendering.
* The payload is a frozen shallow copy of the values. Group items are shared with the form, so treat them as read-only and change them with `setValue`.
* In Vue, the handle's `values` is reactive: read it in a `computed` or a template. `useFormEngine().values` is reactive the same way.
* `onFieldChange` fires for user edits only. `setValue` from the handle does not fire it, so the derived write never marks the due date as edited.
* The first edit of `dueDate` sets `dueDateEdited`, and later issue dates leave the due date alone.
* `setValue` runs the same visibility and validation updates as typing. A value for a field hidden by its conditions is cleared at once. A `setValue` inside `onValuesChange` fires a nested `onValuesChange` right away, including on the first call at mount. The nested call gets the new values before the outer call returns.
* Write a derived value only when it differs: `if (values.total !== next) form.setValue('total', next)`. An equal value fires nothing, so the chain stops. Two callbacks that keep changing each other's fields never stop.
* `getValues()` returns a shallow copy of the current values.
* The handle and events work the same on `HeadlessFormRenderer`.
* To restore derived values with a draft, save `dueDateEdited` next to the draft.

## Related

* [Getting started](/guide/#definition-and-initial-values): `initialValues` and the renderer handle
* [Repeatable groups](/guide/groups): line items as group values
* [FormEngine](/api/form-engine#subscriptions): `subscribeValues` and `getValuesSnapshot` on the engine
