Nested Objects

A nested object in your form's value is represented by the same FieldApi (see Concepts), decomposed into its own lazily-populated registry of child fields instead of edited as one atomic value.

const form = new FormApi({
  initialValue: { email: "", address: { line1: "", city: "" } },
});
 
const address = form.field("address");
const line1 = address.field("line1");

Resolve the intermediate field first

field resolves relative to the field it's called on, not the whole form. For a field nested under an object, resolve the parent field before the child:

const address = form.field("address");
const line1 = address.field("line1");

You only have to do this if something needs "address" as its own node — its own validators/schemaValidator, or its own aggregated touched/invalid. Otherwise, addressing the nested value directly by its flat dotted path (form.field("address.line1")) is fine: it's the normal shape for schema-validated forms, which don't need an intermediate field per nesting level.

What's not allowed is registering the same path both ways — resolving "address" as its own node and also addressing something under it as a flat field directly on form:

// Throws: "address" is already registered on `form`, so a flat field at
// "address.line1" would leave two disconnected nodes tracking overlapping
// (not identical) slices of value.
form.field("address");
form.field("address.line1");

This collision is caught explicitly rather than silently creating an orphaned duplicate. Use whichever form you reach for consistently for that path.

Binding to a nested field

parent.field(name) works the same way regardless of whether the path resolves to a leaf or a nested object: a group api is passed into a reusable component the same way a leaf api is, following the same shape as TextField in Basic:

import type { ReactNode } from "react";
import type { FieldApi } from "@kintools/form-react";
 
export type AddressFieldProps<TParentValue> = {
  api: FieldApi<Address, TParentValue>;
};
 
export function AddressField<TParentValue>(
  { api }: AddressFieldProps<TParentValue>,
): ReactNode {
  return (
    <fieldset>
      <TextField api={api.field("line1")} label="Line 1" />
      <TextField api={api.field("city")} label="City" />
    </fieldset>
  );
}

Aggregated state

A node's touched/invalid/validating reflect itself or any registered child:

address.invalid; // true if `address` itself has a node-level error,
// OR any field registered under it does

Setting touched cascades to every registered child. handleBlur/ handleChange still make sense on a node with children: bind them directly to a custom control that edits the whole nested object atomically (e.g. a date-range picker backed by { start, end }), same as for a leaf. Avoid calling handleBlur on a node you're also decomposing into individually-focusable child inputs; that forces every descendant touched for what wasn't really a single control's blur event.

The child registry

children is the Map of every child field created so far via field, populated lazily; a field never requested (e.g. its input never rendered) won't appear yet:

for (const [name, field] of form.children) {
  console.log(name, field.value, field.invalid);
}

onChildrenChanged(cb) notifies when the set of children changes (a field registered or unregistered), not when an existing child's state changes. It's a separate channel from the ordinary subscribe/notify path (see Reactivity), meant for introspection tooling (like the devtools panel) rather than typical useWatch consumers.

A child is unregistered automatically once its path stops existing in its parent's value, whether from removeItem or any other value change, cancelling its pending debounced validation and unregistering its own children first.

What's next