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 — 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 "@kin-form/react/index.ts";
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 doesSetting 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 — from removeItem or any other value change — cancelling its pending debounced validation and unregistering its own children first.
What's next
- Dynamic Arrays — the array-specific half of the child registry
- Flat vs. Nested Structure — choosing nested fields vs. flat dotted paths, level by level
FieldApi— full reference on JSR