Skip to content

Calculated values

A field with calculateValue derives its value from an expression over other fields. It updates whenever a dependency changes — no user interaction, no useEffect in your code.

{
"name": "subtotal",
"type": "number",
"label": "Subtotal",
"calculateValue": "%qty% * %price%"
}

Open in playground

Calculated fields are ordinary fields in every other respect: they appear in form data, they’re included in the onSave payload, and they can carry validation. Most of the time you’ll want them read-only in your component — check field.calculateValue and render accordingly.

The interesting case is a calculated field that depends on another calculated field. parseSchema builds a dependency graph once, at parse time, and updateData walks it — so a chain of any depth resolves inside a single data write, not one render pass per link.

Four levels deep, one passOpen full playground

Change the quantity and every downstream field is already correct by the time updateData returns. That matters beyond render counts: form data is consistent the moment you write to it, whether or not the dependent fields are mounted. A calculated total that lives inside a collapsed section still holds the right number.

The graph is topologically sorted, so a diamond evaluates correctly and exactly once:

subtotal
╱ ╲
discount taxable
╲ ╱
total

total waits for both inputs rather than recomputing once per branch.

A cycle — a calculates from b, b calculates from a — is a schema authoring error. It’s detected when the schema is parsed, warned about once, and the fields involved are excluded from graph propagation:

[driven-form] Circular calculateValue dependency between: a, b.
These fields are excluded from synchronous propagation.

The warning is never debug-gated. Those fields silently stop auto-updating, which is exactly the kind of failure you want to hear about.

The graph is built from the flat field map, and row templates (columnFields) aren’t in it. Fields inside a repeater row keep the older effect-based propagation — correct, but settling over successive passes rather than in one write.

Form-level fields are fully in the graph, including ones that aggregate over rows. So a per-row lineTotal uses the effect path, while a form-level sum(%items%, 'lineTotal') settles synchronously. See cross-row aggregates.

Anything the expression language supports: arithmetic, ternaries, Number(), Math.*, and any registered expression function.

{
"name": "premium",
"type": "number",
"label": "Premium",
"calculateValue": "Number(%sumInsured%) * 0.005"
}

Open in playground

Number() is worth reaching for when a value might arrive as a string — input elements hand back strings, and '10' * 2 is 20 but '10' + 2 is '102'.