vs TanStack Form

Of the libraries compared here, TanStack Form is the closest to Kin Form's mental model: a framework-agnostic core, controlled binding, type-safe field paths, and selective re-rendering through selectors. So the comparison is less about whether a given feature exists and more about how large the API surface is, how many distinct primitives you assemble to build a form, and where the type-safety and ergonomics diverge. Like the React Hook Form page, this one works through the same topics the guide covers, one at a time, against @tanstack/[email protected].

Field registration & binding model

Native input

import { useForm, Watch } from "@kintools/form-react";
import { required } from "@kintools/form-validators";
 
function LoginForm() {
  const form = useForm({
    initialValue: { email: "" },
    onSubmit: (form) => login(form.value),
  });
 
  return (
    <form onSubmit={form.handleSubmit}>
      {/* Only re-renders when the email field changes. */}
      <Watch api={form.field("email", { validators: required("Required") })}>
        {(field) => (
          <>
            <input
              value={field.value}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            {field.touched && field.invalid && <span>{field.error}</span>}
          </>
        )}
      </Watch>
 
      <button type="submit">Log in</button>
    </form>
  );
}

What's different:

Kin FormTanStack Form
Binding modelcontrolled (value / handleChange)controlled (field.state.value / handleChange)
Field primitiveresolve then watch: form.field(...) + <Watch>one <form.Field name>, bound to form
Submit wiringonSubmit={form.handleSubmit}handler does preventDefault + handleSubmit()
Reading the errorfield.error, a string | nullfield.state.meta.errors, an array

Both bind controlled, and the field primitives are about the same size: <Watch api={form.field("email", ...)}> versus <form.Field name="email" ...>. TanStack Form fuses resolving a field and watching it into one form-bound primitive; Kin Form keeps them separate (form.field to resolve, <Watch> / useWatch to watch). That split is what keeps the core lean and composition simple: a resolved FieldApi is just a value, so it drops straight into a reusable component with a one-line call site (see Form composition), with no form-bound component type to thread through.

This is the only section using a bare <input>. Everywhere else both sides bind to a controlled <TextInput> (or <CountrySelect>), since that is the case worth comparing.

Non-native input

import { useForm, Watch } from "@kintools/form-react";
import { required } from "@kintools/form-validators";
 
function ProfileForm() {
  const form = useForm({
    initialValue: { country: "" },
    onSubmit: (form) => save(form.value),
  });
 
  return (
    <form onSubmit={form.handleSubmit}>
      <Watch api={form.field("country", { validators: required("Required") })}>
        {(field) => (
          <>
            <CountrySelect value={field.value} onChange={field.handleChange} />
            {field.error && <span>{field.error}</span>}
          </>
        )}
      </Watch>
 
      <button type="submit">Save</button>
    </form>
  );
}

What's different:

Kin FormTanStack Form
Non-native inputsSame <Watch> as any other fieldSame <form.Field> as any other field

Both treat a custom-component field exactly like a native one: no extra primitive. Groups and arrays are where they diverge (see Form composition): a TanStack Form FieldGroupApi is a separate type, a Kin Form group is just a FieldApi whose value is an object.

Reusable field component

Real forms bind through reusable field components. This is where the two libraries diverge most: in Kin Form, the call site collapses to one typed element; in TanStack Form, a typed reusable field needs the full createFormHook apparatus and still does not collapse the call site. So the sections below use a <TextField> on the Kin Form side and inline <form.Field> on the TanStack Form side, the idiomatic shape for each.

INFO

To build one reusable, type-safe text field and reuse it across forms, the concepts you learn are:

  • Kin Form: FieldApi, useWatch
  • Tanstack Form: createFormHookContexts, createFormHook, fieldContext, useAppForm, form.AppField, useFieldContext, and for groups withFieldGroup / FieldGroupApi and the fields mapping

The API surface matrix sets these side by side with the other libraries.

import { type FieldApi, useWatch } from "@kintools/form-react";
 
function TextField<TParentValue>(
  { api, label }: { api: FieldApi<string, TParentValue>; label: string },
) {
  const field = useWatch(api);
 
  return (
    <label>
      {label}
      <TextInput
        value={field.value}
        onBlur={field.handleBlur}
        onChange={field.handleChange}
      />
      {field.invalid && field.touched &&
        <span>{field.error}</span>}
    </label>
  );
}
 
<TextField
  api={form.field("email", { validators: required() })}
  label="Email"
/>;

What's different:

Kin FormTanStack Form
What the component receivesresolved api: FieldApi<string, TParentValue> propfield off React context (useFieldContext), only inside form.AppField
One-time setupnonecreateFormHookContexts() + createFormHook({...}) → useAppForm
Value-type safetyFieldApi<string, TParentValue> checks ituseFieldContext<string>(): you assert it, or lose it with AnyFieldApi
Call site<TextField api={form.field(...)} /><form.AppField name>{(field) => <field.TextField/>}</form.AppField>
Cross-form reuseplain import, passed a propregistered in one createFormHook, used via its useAppForm forms

Kin Form: resolve the field, pass the FieldApi down. No setup, and it is the same FieldApi every other field uses.

TanStack Form: first build the wiring (createFormHookContexts, createFormHook, a component registry), then read the field from context inside the component. Several APIs to learn before the first typed field renders, and the call site still needs the form.AppField wrapper. The lighter route, passing the field object down directly, skips the wiring but types it as AnyFieldApi (23 any parameters), so the value type is gone.

Per-node validation

import { useForm } from "@kintools/form-react";
 
function SignupForm() {
  const form = useForm<{ username: string }>({
    initialValue: { username: "" },
  });
 
  return (
    <TextField
      api={form.field("username", {
        asyncValidator: async (field) =>
          (await checkUsernameTaken(field.value)) ? "Username taken" : null,
        validationDebounceMs: 300,
      })}
      label="Username"
    />
  );
}

This is a place the two are close. Both ship a built-in async-validation debounce (validationDebounceMs on Kin Form, asyncDebounceMs on TanStack Form), so neither needs the hand-rolled lodash/debounce the React Hook Form page shows. Both also run async validation only after the synchronous rules for that field have passed, so an expensive check never fires for a value already known bad.

What's different:

Kin FormTanStack Form
Sync validationone validators array, first truthy winsvalidators.onChange / .onBlur, one fn per hook
Async validationone asyncValidator slotvalidators.onChangeAsync / .onBlurAsync
DebouncevalidationDebounceMsasyncDebounceMs, plus per-hook overrides
Config shapeone array + one async slot + one numbera validators object keyed by event

Same coverage, different shape. Kin Form: one validators array, one async slot, one debounce number. TanStack Form: a validators object keyed by timing (change, blur, submit; sync and async), so "validate on blur only" is a one-key change. Kin Form always runs sync validators on change and leaves blur-only display to the render (field.touched).

Schema validation

This is a bundling tradeoff, not a clear win for either side: TanStack Form builds Standard Schema support into core, Kin Form keeps it in a separately versioned package for a lean core.

import { useForm } from "@kintools/form-react";
import { toSchemaValidator } from "@kintools/form-validators";
import { z } from "zod";
 
const signupSchema = z.object({
  email: z.email(),
  password: z.string().min(8),
});
type Signup = z.infer<typeof signupSchema>;
 
function SignupForm() {
  const form = useForm<Signup>({
    initialValue: { email: "", password: "" },
    schemaValidator: toSchemaValidator(signupSchema),
    onSubmit: (form) => signUp(form.value),
  });
 
  // A field's own `validators` still run alongside the schema when present;
  // `field.error` prefers a field's own message, falling back to its slice
  // of the schema result.
  return (
    <form onSubmit={form.handleSubmit}>
      <TextField api={form.field("email")} label="Email" />
      <TextField api={form.field("password")} label="Password" />
      <button type="submit">Sign up</button>
    </form>
  );
}

What's different:

Kin FormTanStack Form
Adapter packagetoSchemaValidator() from @kintools/form-validatorsnone; a Standard Schema is a validator itself
Where a schema attachesany node, via its own schemaValidatorfield or form validators.onChange
Schema + hand-written rulescoexist; error prefers the field's own messagemerge into one errors; a field's rules replace the form schema
Standard Schema librariesany (zod, valibot, arktype, ...)any (zod, valibot, arktype, effect, ...)

Kin Form's edge is scope and separation, not ergonomics: a schema can sit on any node, and its output is tracked apart from a field's own validators-produced message under the hood, so a field can carry both without one overwriting the other; field.error surfaces whichever exists, preferring the field's own. On TanStack Form a field-level validators entry replaces the form schema for that field instead of running alongside it.

Cross-field validation

Both are declarative here, unlike React Hook Form's manual trigger(). They declare the link from opposite ends of the relationship.

function SignupForm() {
  const form = useForm<Signup>({
    initialValue: { email: "", password: "", confirmPassword: "" },
  });
 
  return (
    <>
      <TextField
        api={form.field("password", { dependents: ["confirmPassword"] })}
        label="Password"
      />
      <TextField
        api={form.field("confirmPassword", {
          validators: (field) =>
            field.value !== form.value.password ? "Passwords must match" : null,
        })}
        label="Confirm password"
      />
    </>
  );
}

What's different:

Kin FormTanStack Form
Which field declaresthe source field lists dependentsthe dependent field lists onChangeListenTo
Reading the other valueform.value.passwordfieldApi.form.getFieldValue("password")
Trigger granularitydependents, re-runs on any value changeonChangeListenTo / onBlurListenTo, per event
Fan-outone array on the source covers allone array per dependent field

Same spirit, opposite ends. Kin Form puts the wiring on the field being watched, so adding a dependent is an edit to the source field's dependents. TanStack Form puts it on the field doing the watching, so the field that owns the rule also declares what re-triggers it. Both avoid a manual refire call.

The tradeoff is fan-out versus locality: one source feeding many dependents is a single array in Kin Form (one onChangeListenTo per dependent in TanStack Form), while co-locating each dependent's rule with its triggers means removing the dependent takes its wiring with it, where a Kin Form source can be left with a stale dependents entry.

Kin Form's side also needs no per-dependent subscription: the source's own value-change callback, which fires anyway, does the revalidation, so nothing registers or tears down as fields mount and unmount.

Dirty tracking & reset

function ProfileForm() {
  const form = useForm({
    initialValue: { firstName: "", lastName: "" },
    onSubmit: (form) => save(form.value),
  });
 
  return (
    <form onSubmit={form.handleSubmit}>
      <Watch api={form.field("firstName")}>
        {(field) => (
          <>
            <TextInput value={field.value} onChange={field.handleChange} />
            {field.dirty && <span>Edited</span>}
          </>
        )}
      </Watch>
 
      <Watch api={form} select={(f) => f.dirty}>
        {(f, dirty) => (
          <button disabled={!dirty} onClick={() => f.reset()}>
            Discard changes
          </button>
        )}
      </Watch>
    </form>
  );
}

What's different:

Kin FormTanStack Form
Whole-form dirtyform.dirty, flips back on revertform.state.isDirty, stays set after a revert
Differs from default nowfield.dirty!field.state.meta.isDefaultValue
Per-field subscriptionfield.dirty in a scoped Watchfield.state.meta.isDirty, from the store slice
Resetform.reset(value?), moves the baselineform.reset(values?, opts?)
Reset one fieldform.resetField(name, value?)form.resetField(name)

The models differ. Kin Form's dirty is a live deepEqual against the baseline, so typing a character then deleting it leaves the field clean. TanStack Form's meta.isDirty means "edited at some point" and stays set after a revert; !meta.isDefaultValue is the check that matches Kin Form's dirty.

Submission handling

Both name a callback for "the form failed validation," separate from the success path. Only Kin Form also has one for "the submit function itself threw."

const form = useForm<Signup>({
  initialValue: { email: "", password: "" },
  onSubmit: async (form) => {
    await signUp(form.value);
  },
  onSubmitInvalid: (form) => {
    form.touched = true; // Reveal errors on never-blurred fields.
  },
  onSubmitError: (form, error) => {
    toast.error("Sign up failed"); // Called automatically, no wrapper needed.
  },
});
 
// handleSubmit itself calls preventDefault when given an event.
<form onSubmit={form.handleSubmit}>;

What's different:

Kin FormTanStack Form
Validation failedonSubmitInvalidonSubmitInvalid
onSubmit itself throwsonSubmitError, automaticno callback; canSubmit goes false, catch it yourself
Binding to <form>onSubmit={form.handleSubmit}handler does preventDefault + handleSubmit()
Submit-in-progress stateform.submittingform.state.isSubmitting / .canSubmit

onSubmitInvalid is parity. The gap: TanStack Form has no callback for "the submit function threw," so a failed request inside onSubmit is yours to catch, and an uncaught throw flips canSubmit to false until an input changes. Kin Form's onSubmitError fires automatically and leaves the form submittable.

Server-side validation errors

A submit that fails a check only the server can run comes back with per-field messages to surface on the form.

const form = useForm<Signup>({
  initialValue: { email: "", password: "" },
  onSubmit: async (form) => {
    const res = await signUp(form.value);
    if (res.fieldErrors) form.setErrors(res.fieldErrors); // { email: "Taken" }
  },
});

What's different:

Kin FormTanStack Form
Applying a setform.setErrors(map)formApi.setErrorMap({ onSubmit: { form, fields } }), or return that shape from a validators.onSubmitAsync
Message shapeflat dotted-path → message map (same as a schema){ form?, fields? } nested under an onSubmit key
Scopeany nodethe form
Clearingautomatic, on each field's next value changeon next submit
Progressive-enhancement flownone: the same callcreateServerValidate + mergeForm + useTransform, in per-framework packages

Both surface the response imperatively in onSubmit; the call is what differs. Kin Form's setErrors takes a flat path → message map (the same shape a schemaValidator produces), works on any node, and clears each message once that field is edited. TanStack Form's setErrorMap writes a { form, fields } object under the onSubmit error-map key, form-only, cleared on the next submit.

For no-JS progressive enhancement TanStack Form adds a separate createServerValidate/mergeForm apparatus in per-framework packages; Kin Form has no equivalent, and no need for one, the setErrors call is the same either way.

Async initial values

Neither accepts an async defaultValues the way React Hook Form does, so this is closer to a wash. Both lean on an external data hook.

function ProfilePage() {
  const { data, isLoading } = useQuery({
    queryKey: ["profile"],
    queryFn: fetchProfile,
  });
 
  if (isLoading || !data) return <p>Loading...</p>;
  return <ProfileForm initialValue={data} />;
}
 
function ProfileForm({ initialValue }: { initialValue: Profile }) {
  const form = useForm({
    initialValue,
    onSubmit: (form) => save(form.value),
  });
 
  return (
    <form onSubmit={form.handleSubmit}>
      {/* ... */}
    </form>
  );
}

What's different:

Kin FormTanStack Form
Async defaultsinitialValue is synchronousdefaultValues is synchronous
Loading statefrom your data-fetching hooksame
Populating once loadedmount the form after data arrivessame
Resetting to loaded dataform.reset(data)form.reset(data)

Both keep the form unmounted until the data is present, then pass it straight in as the initial value, so there is nothing to reconcile afterwards. TanStack Form's docs also show a keep-mounted variant (feed data?.field ?? "", then call form.reset(data) in an effect when it lands); either works.

Reactivity & selective re-rendering

Both support selective re-rendering; the subscription model is what differs.

Kin Form: each FieldApi node has its own subscribers, so a notification reaches only the subscribers of the nodes that changed (and any ancestor group being watched). State (value, error, touched, dirty, ...) lives on one object, so a single useWatch select can subscribe to any slice of it.

TanStack Form: FieldApi, FieldGroupApi, and FormApi all read from one shared @tanstack/store, so any change notifies every subscriber, each of which runs its selector to decide whether to re-render. The selectors keep the re-renders precise; the cost is only the notify-all fan-out, negligible for typical form sizes.

The feature matrix tracks this difference as the "Localized subscription" row.

import { type FieldApi, useWatch } from "@kintools/form-react";
 
function Field<TParentValue>({ api }: { api: FieldApi<string, TParentValue> }) {
  // One hook covers all field state (including value).
  const [value, error] = useWatch(
    api,
    (f) => [f.value, f.touched ? f.error : null] as const,
  );
 
  return (
    <label>
      <input value={value} />
      {error && <span>{error}</span>}
    </label>
  );
}
 
<Field api={form.field("email")} />;

Form composition

TextField above is the leaf case. Groups and arrays add more: a group needs child-path addressing, an array also needs stable item identity across a reorder. In Kin Form each stays one typed FieldApi prop; in TanStack Form each is a distinct primitive (withFieldGroup / FieldGroupApi, form.Field with mode="array").

Group field

A reusable component for a nested object (e.g. an address, reused for shipping and billing):

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

What's different:

Kin FormTanStack Form
Reusable-group primitivesame FieldApi<Address, TParentValue> as any fieldwithFieldGroup({ defaultValues, render }), a distinct HOC
Binding it to a locationform.field("shipping"), a resolved fieldfields="shipping" prop (path string or key-to-path map)
Building child pathsapi.field("line1"), relativegroup.AppField name="line1", via the fields mapping
The group's own value typeFieldApi<Address, TParentValue>: right theredefaultValues stands in for the shape (type only)
Group-level validationvalidators on the group's FieldApinone on withFieldGroup; a whole-group rule must live on the form
Distinct concepts to learnone (FieldApi)withFieldGroup, FieldGroupApi, group.AppField, fields, createFieldMap

Kin Form uses one concept: a group is a FieldApi whose value is an object, and api.field("line1") reads into it like any other field.

TanStack Form uses withFieldGroup, a separate wrapper with its own group object (a FieldGroupApi, which is neither FormApi nor FieldApi). You bind it to a place in the form with a fields prop: a path string, or a map from group key to form path. It is capable, and the map even lets the group's shape differ from the form's, but it is one more model to learn, and it takes no validators, so a whole-group rule has to live on the form instead of in the component.

Array field

The per-item mechanics line up almost exactly. Two gaps: Kin Form keeps a stable React key across a reorder and TanStack Form does not, and making the array component reusable is one typed prop in Kin Form versus a choice between three imperfect options in TanStack Form.

import { type FieldApi, useWatch } from "@kintools/form-react";
 
function ItemsField<TParentValue>(
  { api }: { api: FieldApi<string[], TParentValue> },
) {
  const value = useWatch(api, (g) => g.value);
 
  return (
    <>
      {value.map((_, i) => {
        const field = api.field(`${i}`);
        return (
          <div key={field.id}>
            <TextInput value={field.value} onChange={field.handleChange} />
            <button type="button" onClick={() => api.moveItem("", i, i - 1)}>
              Move up
            </button>
            <button type="button" onClick={() => api.removeItem("", i)}>
              Remove
            </button>
          </div>
        );
      })}
      {api.error && <span>{api.error}</span>}
      <button type="button" onClick={() => api.pushItem("", "")}>Add</button>
    </>
  );
}
 
<ItemsField
  api={form.field("items", {
    validators: (g) => (g.value.length ? null : "Add at least one item"),
  })}
/>;

What's different:

Kin FormTanStack Form
What holds the arraythe array is a FieldApi<form.Field mode="array">, a mode flag
Mutation helpersa six-method family on the fieldthe same six, *Value names
Array-level validationthe field's own validatorsvalidators on the array field
Stable key across reorderfield.id follows the itemnone built in; key by index
Reusable componentone typed prop, generic, no setupthree trade-offs (below)

Making the array component reusable is where TanStack Form has no clean answer. Three routes, each conceding something:

  • withFieldGroup + a fields prop (above): generic and fully typed, but adds the createFormHook setup, a third API object (group), and every caller option re-declared as a render prop.
  • Pass the whole form to form.Field: typed and setup-free, but pinned to one form's exact shape (ReactFormExtendedApi is invariant over its 12 type args), so one extra field breaks it.
  • A { form, name } pair with <Field> / useField (React Hook Form's Controller style): generic and setup-free, but drops to AnyFormApi / casts and an any value.

Kin Form's resolved FieldApi<string[], TParentValue> prop is all three at once: generic, typed, no setup, because TParentValue is an opaque pass-through and validators go on form.field(name, { validators }) at the call site.

Multistep forms

Neither ships an official multi-step or wizard component. Kin Form ships a dedicated hook, useMultistep. TanStack Form does not; its official multi-step example hand-rolls step state.

import { useForm, useMultistep } from "@kintools/form-react";
 
type Signup = {
  credentials: { email: string; password: string };
  address: { line1: string };
};
 
function SignupWizard() {
  const form = useForm<Signup>({
    initialValue: signupDefaults,
    onSubmit: signUp,
  });
 
  // Each step name is the DeepKey of that step's own FieldApi.
  // `next()` touches, waits for validation, and gates the advance.
  const { stepName, stepField, isLastStep, next } = useMultistep(
    form,
    ["credentials", "address"] as const,
  );
 
  return (
    <form onSubmit={form.handleSubmit}>
      {stepName === "credentials" && (
        <>
          <TextField api={stepField.field("email")} label="Email" />
          <TextField api={stepField.field("password")} label="Password" />
        </>
      )}
      {stepName === "address" && (
        <TextField api={stepField.field("line1")} label="Line 1" />
      )}
      {isLastStep
        ? <button type="submit">Sign up</button>
        : <button type="button" onClick={next}>Next</button>}
    </form>
  );
}

What's different:

Kin FormTanStack Form
Dedicated wizard APIuseMultistep hooknone; hand-rolled with useState
Step-validation ceremonynext(): touch, wait, gate, built inform.validateField(...) per field, by hand
Step to field mappingeach step name is a DeepKey (stepField)a field-name list you maintain per step
Branching / redirectingonBeforeNext returns a step to jump tocustom step state logic
Unvalidated navigationback(), jump(index or name)custom setStep calls

Neither ships a wizard component, but Kin Form has useMultistep and TanStack Form does not. TanStack Form's Form Groups narrow the gap (a group per step, advancing on onGroupSubmit), but there is no equivalent to next() doing touch, wait, and gate in one call, or onBeforeNext redirecting to another step.

Conditional fields

A field's meaning can depend on a sibling: picking type: "boolean" should turn defaultValue into a radio group, reset its stale value, and validate it differently than type: "string" does. The point here is AttributeForm and DefaultValueField, not another field component: Kin Form's SelectField/RadioField follow the exact same shape as the TextField already built above, so their bodies aren't repeated below - see the source or try it live for the real, four-type version. TanStack Form's side stays inline, matching every other section on this page - see Reusable field component for what reusing its own field.TextField would cost here.

import { type FieldApi, useForm, useWatch } from "@kintools/form-react";
 
type Attribute = {
  type: "string" | "boolean";
  defaultValue: string | boolean | null;
};
 
function AttributeForm() {
  const form = useForm<Attribute>({
    initialValue: { type: "string", defaultValue: "" },
    onSubmit: (form) => save(form.value),
  });
 
  const typeField = form.field("type", {
    onValueChanged: () => {
      form.unregisterField("defaultValue", { resetValue: true });
    },
  });
 
  return (
    <form onSubmit={form.handleSubmit}>
      <SelectField api={typeField} label="Type">
        <option value="string">String</option>
        <option value="boolean">Boolean</option>
      </SelectField>
      <DefaultValueField form={form} />
      <button type="submit">Save</button>
    </form>
  );
}
 
// Split from AttributeForm: a component calling useForm shouldn't also call
// useWatch itself: everything below it that needs to re-render on a field's
// value should watch it as a child instead.
function DefaultValueField({ form }: { form: FieldApi<Attribute> }) {
  const type = useWatch(form, (f) => f.value.type);
 
  return type === "boolean"
    ? (
      <RadioField
        api={form.field("defaultValue", {
          validators: (field) =>
            typeof field.value === "boolean" ? null : "Pick true or false",
        }) as FieldApi<boolean, Attribute>}
        label="Default value"
      />
    )
    : (
      <TextField
        api={form.field("defaultValue", {
          validators: (field) => field.value ? null : "Required",
        }) as FieldApi<string, Attribute>}
        label="Default value"
      />
    );
}

What's different:

Kin FormTanStack Form
Reset on switchunregisterField("defaultValue", { resetValue: true }) in onValueChangedlisteners.onChange, on the source field
Revalidate on switchnot needed - each branch registers a fresh fieldonChangeListenTo, on the dependent field
Reading the siblingnot needed - each branch registers its own validatorfieldApi.form.getFieldValue("type")
Per-type validationone validators function per branchone branching validators.onChange function
Reusable widgetRadioField, a prop-based componentinline radios inside form.Field's render prop

Kin Form needs nothing declared on defaultValue itself: onValueChanged resets it when type changes, and each branch registers it fresh with its own validators, so nothing reads the sibling or revalidates by hand. TanStack Form's defaultValue field stays mounted across the switch, so it still splits the two concerns across fields - listeners resets on type, onChangeListenTo pulls the retrigger back onto defaultValue.

DefaultValueField is split out from Kin Form's AttributeForm for the same reason EnumOptionsField is: a component calling useForm shouldn't also call useWatch itself. TanStack Form's form.Field render prop doesn't have this problem, since resolving and subscribing to a field are the same step there.