@k8ordo/form

How it works

How @k8ordo/form works underneath. None of it is needed to use the package, but knowing why it is written this way makes the edge cases easier to reason about.

On this page

One schema, at work in three places

The schema is used twice, both times on the server: once to derive the fields, once to validate the submission. The browser only ever receives the JSON derived from it.

text
server   formFields(schema)       → { fields, arrays, rules, dropped }
           ↓ props (JSON)
client   useForm(fields, state)   → props for <form>, input for each field
           ↓ submit
server   parseForm(schema, data)  → typed data, or errors per field

So zod never enters the browser bundle. A schema can be written with zod or zod/mini; as long as nothing in the browser reads it, the choice does not change the bundle size.

The DOM holds the values

Values stay in the DOM and are never copied into React state. State holds only the five things the DOM cannot express:

  • Which message to show for a field the browser has judged invalid
  • Which server errors are still current
  • The identity of each repeated row
  • One dirty flag, read back from the DOM
  • The row counts that adding or removing a row is measured against

Since nothing is copied, typing never re-renders, and both the browser’s reset and React’s reset after an action simply work.

Why it works without JavaScript

The constraints are attributes in the server-rendered HTML, so the browser checks input itself before JavaScript arrives.

Once JavaScript arrives, useForm sets noValidate and runs the same checks in zod’s wording. It is set from JavaScript, never written into the HTML, so without JavaScript the browser’s own checks stay on.

After a failed submission, the submitted values render as the fields’ defaultValue, so nothing typed is lost even without JavaScript.

Why the wording never drifts

For each kind of check, formFields hands the schema a value that fails only that check and keeps the message that comes back. The text shown in the browser is exactly the text zod produces on the server.

How required is decided

JSON Schema’s required means “the key is present”, but a form submits something for every control. So formFields emits required only when the schema rejects what the control submits when left empty.

What an empty control submits depends on the control: "" for text, false for an unchecked checkbox, nothing for a number, a file or a choice. parseForm hands the schema exactly the same values, so the two sides agree. Below, bio accepts an empty string and age may be left out, so neither gets required.

ts
z.object({ bio: z.string() });
// bio: { name: 'bio', type: 'text' }

z.object({ title: z.string().min(1) });
// title: { name: 'title', required: true, type: 'text', minLength: 1 }

z.object({ age: z.coerce.number().optional() });
// age: { name: 'age', type: 'number', step: 'any' }

Reporting what the browser cannot check

Some checks cannot be expressed as HTML attributes: a .refine() on the whole schema, a regex with flags. Rather than dropping them silently, formFields lists them in dropped.

Outside production it also prints the list with console.warn. Every one of them still runs on the server; the browser just does not check them.

A schema a form cannot express is an error

Every submitted value is a string, so z.number() or z.date() would make a field nothing can satisfy, and shapes such as z.record or tuples have no name to submit under. formFields throws on these, with the reason.

A form that silently discards what the person did, or one that can never validate, is a mistake to report, not something to paper over.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2