{"_id":"@dmytromykhailiuk/preact-signal-hook-forms","_rev":"3-1dec5cf72a2fc9b65def075bfbaef1b7","name":"@dmytromykhailiuk/preact-signal-hook-forms","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@dmytromykhailiuk/preact-signal-hook-forms","version":"0.1.0","keywords":["preact","preact-signals","signals","forms","form","form-validation","validation","react-hook-form","hooks","useform","usefieldarray","field-array","reactive","zero-rerender","typescript","typed","resolver","schema-validation","zod","valibot","yup"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/preact-signal-hook-forms@0.1.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/preact-signal-hook-forms#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-hook-forms/issues"},"dist":{"shasum":"d3a08b83e93a473ecf9a31267587077350666131","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-hook-forms/-/preact-signal-hook-forms-0.1.0.tgz","fileCount":33,"integrity":"sha512-Rf3BTmMGvT2rro4VxVONHLfXuBATevhqCaIO4S+/84nQY4Tn4Ixw/ZhSqqUwY5ON1N2MAwOdP1R9Yjak17y9Rg==","signatures":[{"sig":"MEQCICJxMylH1HakEv4TDEYXJTleglN+P8/5AjfXV4hUD2+fAiB1UWxWe0j3I+H2whl3jy9yAIFH5+YVwjw1f5rJpHTgCQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":372576},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json","./resolvers/yup":{"import":{"types":"./dist/resolvers/yup.d.ts","default":"./dist/resolvers/yup.js"},"require":{"types":"./dist/resolvers/yup.d.cts","default":"./dist/resolvers/yup.cjs"}},"./resolvers/zod":{"import":{"types":"./dist/resolvers/zod.d.ts","default":"./dist/resolvers/zod.js"},"require":{"types":"./dist/resolvers/zod.d.cts","default":"./dist/resolvers/zod.cjs"}},"./resolvers/valibot":{"import":{"types":"./dist/resolvers/valibot.d.ts","default":"./dist/resolvers/valibot.js"},"require":{"types":"./dist/resolvers/valibot.d.cts","default":"./dist/resolvers/valibot.cjs"}}},"gitHead":"0f91cb1048ccdcfb57a7a5d39ab4ca777785fd41","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/preact-signal-hook-forms.git","type":"git"},"_npmVersion":"11.6.2","description":"Performant, fully-typed forms for Preact — a react-hook-form analogue built entirely on @preact/signals. Signal-first, zero re-render.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"yup":"^1.6.1","zod":"^3.24.1","tsup":"^8.3.5","jsdom":"^25.0.1","preact":"^10.25.4","vitest":"^2.1.8","valibot":"^1.0.0","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@preact/signals":"^2.0.1","@preact/preset-vite":"^2.10.1","@testing-library/preact":"^3.2.4"},"peerDependencies":{"preact":">=10.11.0","@preact/signals":">=1.2.0 || ^2.0.0"},"peerDependenciesMeta":{"yup":{"optional":true},"zod":{"optional":true},"valibot":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/preact-signal-hook-forms_0.1.0_1784319328895_0.7869667337821884","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@dmytromykhailiuk/preact-signal-hook-forms","version":"0.1.1","keywords":["preact","preact-signals","signals","forms","form","form-validation","validation","react-hook-form","hooks","useform","usefieldarray","field-array","reactive","zero-rerender","typescript","typed","resolver","schema-validation","zod","valibot","yup"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/preact-signal-hook-forms@0.1.1","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/preact-signal-hook-forms#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-hook-forms/issues"},"dist":{"shasum":"c4837ecdec67e81ee0697f3c336d05e10246f00c","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-hook-forms/-/preact-signal-hook-forms-0.1.1.tgz","fileCount":33,"integrity":"sha512-+2paElCmXSWLgux4Gx9lNxb/42tjzlTbJ2vUk/cJxzVuLjSGRcc7yZcNfr6l8rPkgf2vWVhRgZyFY3CrGFUtAA==","signatures":[{"sig":"MEQCIC739TZO8tgq/rvRx0s9Pbf8dtF7I5wZ7DHvSZaZnnq/AiA+s6ooTh8w0bALIaA7SN6axo2oXHZljBurDfun50t2hg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":372370},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json","./resolvers/yup":{"import":{"types":"./dist/resolvers/yup.d.ts","default":"./dist/resolvers/yup.js"},"require":{"types":"./dist/resolvers/yup.d.cts","default":"./dist/resolvers/yup.cjs"}},"./resolvers/zod":{"import":{"types":"./dist/resolvers/zod.d.ts","default":"./dist/resolvers/zod.js"},"require":{"types":"./dist/resolvers/zod.d.cts","default":"./dist/resolvers/zod.cjs"}},"./resolvers/valibot":{"import":{"types":"./dist/resolvers/valibot.d.ts","default":"./dist/resolvers/valibot.js"},"require":{"types":"./dist/resolvers/valibot.d.cts","default":"./dist/resolvers/valibot.cjs"}}},"gitHead":"15344f2e06201f2f039d88f4688cea462c6563fe","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"vite --config vite.playground.config.ts","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/preact-signal-hook-forms.git","type":"git"},"_npmVersion":"11.6.2","description":"Performant, fully-typed forms for Preact — a react-hook-form analogue built entirely on @preact/signals. Signal-first, zero re-render.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"yup":"^1.6.1","zod":"^3.24.1","tsup":"^8.3.5","jsdom":"^25.0.1","preact":"^10.25.4","vitest":"^2.1.8","valibot":"^1.0.0","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@preact/signals":"^2.0.1","@preact/preset-vite":"^2.10.1","@testing-library/preact":"^3.2.4"},"peerDependencies":{"preact":">=10.11.0","@preact/signals":">=1.2.0 || ^2.0.0"},"peerDependenciesMeta":{"yup":{"optional":true},"zod":{"optional":true},"valibot":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/preact-signal-hook-forms_0.1.1_1784582431685_0.576462068380649","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@dmytromykhailiuk/preact-signal-hook-forms","version":"1.0.0","description":"Performant, fully-typed forms for Preact — a react-hook-form analogue built entirely on @preact/signals. Signal-first, zero re-render.","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["preact","preact-signals","signals","forms","form","form-validation","validation","react-hook-form","hooks","useform","usefieldarray","field-array","reactive","zero-rerender","typescript","typed","resolver","schema-validation","zod","valibot","yup"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./resolvers/zod":{"import":{"types":"./dist/resolvers/zod.d.ts","default":"./dist/resolvers/zod.js"},"require":{"types":"./dist/resolvers/zod.d.cts","default":"./dist/resolvers/zod.cjs"}},"./resolvers/valibot":{"import":{"types":"./dist/resolvers/valibot.d.ts","default":"./dist/resolvers/valibot.js"},"require":{"types":"./dist/resolvers/valibot.d.cts","default":"./dist/resolvers/valibot.cjs"}},"./resolvers/yup":{"import":{"types":"./dist/resolvers/yup.d.ts","default":"./dist/resolvers/yup.js"},"require":{"types":"./dist/resolvers/yup.d.cts","default":"./dist/resolvers/yup.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"vite --config vite.playground.config.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"peerDependencies":{"@preact/signals":">=1.2.0 || ^2.0.0","preact":">=10.11.0"},"peerDependenciesMeta":{"valibot":{"optional":true},"yup":{"optional":true},"zod":{"optional":true}},"devDependencies":{"@biomejs/biome":"^1.9.4","@preact/preset-vite":"^2.10.1","@preact/signals":"^2.0.1","@testing-library/preact":"^3.2.4","@types/node":"^22.10.5","jsdom":"^25.0.1","preact":"^10.25.4","tsup":"^8.3.5","typescript":"^5.7.3","valibot":"^1.0.0","vitest":"^2.1.8","yup":"^1.6.1","zod":"^3.24.1"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-hook-forms.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-hook-forms/issues"},"homepage":"https://dmytromykhailiuk.github.io/preact-signal-hook-forms/","gitHead":"64fc40d1a7d783775a141c59543676bc871a0674","_id":"@dmytromykhailiuk/preact-signal-hook-forms@1.0.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-lCWAf0WMSsVLB6yJe8BtWa4KT0RzOW5cZszK2IDmTOcP5fDBbjKBVGChknPhaqL99mGUyG9TyrEuXY4iLlZvKQ==","shasum":"b52052d225f2bb30f9fda4caddc7ef0c3bed848f","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/preact-signal-hook-forms/-/preact-signal-hook-forms-1.0.0.tgz","fileCount":33,"unpackedSize":372363,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBfbU1ZJ/Vw/SpEsfEf1jUE+0sdKDgpH1I3v675Lblw2AiEAhx7I3kTaYxlz3z2Jab1U75msPtKnMTUPHBhoJJS6JEQ="}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/preact-signal-hook-forms_1.0.0_1786639013479_0.3186784262605018"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-17T20:15:28.798Z","modified":"2026-08-13T16:36:53.989Z","0.1.0":"2026-07-17T20:15:29.044Z","0.1.1":"2026-07-20T21:20:31.836Z","1.0.0":"2026-08-13T16:36:53.618Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/preact-signal-hook-forms/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/preact-signal-hook-forms/","keywords":["preact","preact-signals","signals","forms","form","form-validation","validation","react-hook-form","hooks","useform","usefieldarray","field-array","reactive","zero-rerender","typescript","typed","resolver","schema-validation","zod","valibot","yup"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/preact-signal-hook-forms.git"},"description":"Performant, fully-typed forms for Preact — a react-hook-form analogue built entirely on @preact/signals. Signal-first, zero re-render.","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# preact-signal-hook-forms\n\n> Performant, fully-typed forms for **Preact**, built entirely on `@preact/signals`.\n> A functional analogue of [react-hook-form](https://react-hook-form.com) — **signal-first, zero re-render.**\n\n> **Full documentation:** open [Docs](https://dmytromykhailiuk.github.io/preact-signal-hook-forms/)\n> in a browser — every option, with examples, a table of contents and cross-links.\n> This README is the short form.\n\n- ⚡️ **Zero re-render.** The component body runs **once**. Every value, error and\n  flag flows through signals straight to the DOM.\n- 🎯 **Signal-first.** Field state _is_ a signal you can drop anywhere:\n  `<input {...register(\"email\")} />`, `{form.watch(\"email\")}`, `{errorSignal}`.\n- 🧩 **Two APIs.** Imperative `register()` **and** declarative `<Field>` / `<Controller>`.\n- 🛡 **Fully typed.** Dot-path autocompletion (`user.address.city`, `items.0.name`).\n- ✅ **Validation.** Built-in rules **plus** schema resolvers (Zod / Valibot / Yup).\n- 🌲 **Tiny & tree-shakeable.** ESM + CJS, resolvers in separate entry points.\n\n## Installation\n\n```bash\nnpm install @dmytromykhailiuk/preact-signal-hook-forms\n```\n\nRequires **Node.js ≥ 18** and the peer dependencies every Preact app already has:\n\n| Peer dependency   | Version               |\n| ----------------- | --------------------- |\n| `preact`          | `>=10.11.0`           |\n| `@preact/signals` | `>=1.2.0` or `^2.0.0` |\n\nSchema libraries (`zod`, `valibot`, `yup`) are **optional** — install one only if\nyou use its resolver.\n\n## Table of contents\n\n- [Quick start](#quick-start)\n- [Why signals?](#why-signals)\n- [Core concepts](#core-concepts)\n  - `[useForm(options)](#useformoptions)`\n  - `[register(name, rules?)](#registername-rules)`\n  - [Reading values:](#reading-values-values-watch-getvalues) `values`[,](#reading-values-values-watch-getvalues) `watch`[,](#reading-values-values-watch-getvalues) `getValues`\n  - [Writing values:](#writing-values-setfieldvalue-setvalue) `setFieldValue`[,](#writing-values-setfieldvalue-setvalue) `setValue`\n  - `[reset(values?, options?)](#resetvalues-options)`\n  - `[formState](#formstate)`\n  - [Showing errors](#showing-errors)\n- [Validation](#validation)\n- [Declarative components](#declarative-components)\n- [Dynamic lists —](#dynamic-lists--usefieldarray) `useFieldArray`\n- [Other hooks](#other-hooks)\n- [Migrating from react-hook-form](#migrating-from-react-hook-form)\n- [API reference](#api-reference)\n- [Development](#development)\n- [License](#license)\n\n## Quick start\n\n```tsx\nimport { useForm } from \"@dmytromykhailiuk/preact-signal-hook-forms\";\n\nfunction LoginForm() {\n  // 👇 This function runs exactly ONCE.\n  const form = useForm<{ email: string; password: string }>({\n    defaultValues: { email: \"\", password: \"\" },\n    mode: \"onBlur\",\n  });\n\n  const onSubmit = form.handleSubmit((values) => {\n    console.log(values); // fully typed { email, password }\n  });\n\n  return (\n    <form onSubmit={onSubmit}>\n      {/* value={signal} binds straight to the DOM attribute — no re-render */}\n      <input {...form.register(\"email\", { required: \"Email is required\" })} />\n      {/* the value signal, rendered as a live text node */}\n      <p>You typed: {form.watch(\"email\")}</p>\n\n      <input type=\"password\" {...form.register(\"password\", { minLength: 8 })} />\n\n      {/* computed signals go straight into attributes */}\n      <button disabled={form.formState.isSubmitting}>Sign in</button>\n    </form>\n  );\n}\n```\n\n## Why signals?\n\nreact-hook-form is fast because it is _uncontrolled_ — it avoids re-renders by\nreading the DOM through refs. This library reaches the same goal from the other\ndirection: **the field value is a signal, and Preact binds signals directly to\nthe DOM** (as an attribute or a text node). The value is fully controlled, yet\nnothing re-renders.\n\n## Core concepts\n\n### `useForm(options)`\n\nCreates a form controller that stays **stable for the component's lifetime**\n(options are read once, on the first render). All options:\n\n| Option             | Type               | Default  |\n| ------------------ | ------------------ | -------- | -------------- | ------------ | ------ | ------------ |\n| `defaultValues`    | `Partial<Values>`  | `{}`     |\n| `mode`             | `\"onChange\"        | \"onBlur\" | \"onSubmit\"     | \"onTouched\"  | \"all\"` | `\"onSubmit\"` |\n| `reValidateMode`   | `\"onChange\"        | \"onBlur\" | \"onSubmit\"`    | `\"onChange\"` |\n| `resolver`         | `Resolver<Values>` | –        |\n| `criteriaMode`     | `\"firstError\"      | \"all\"`   | `\"firstError\"` |\n| `shouldFocusError` | `boolean`          | `true`   |\n| `shouldUnregister` | `boolean`          | `false`  |\n| `delayError`       | `number` (ms)      | –        |\n\n`delayError` debounces the _appearance_ of a new error: validation still runs\nimmediately, but the message is held back for the given delay so it never\nflashes mid-keystroke. Clearing is always instant — the moment the field\nbecomes valid the error disappears, and a pending message is cancelled.\n`setError` bypasses the delay.\n\n`mode` decides when a field is validated **for the first time**;\n`reValidateMode` takes over once the field already shows an error (or the form\nwas submitted). The default pair — validate on submit, re-validate on every\nchange — means users aren't nagged while typing, but errors disappear the\nmoment the input becomes valid.\n\n### `register(name, rules?)`\n\nReturns props to spread onto a DOM element:\n\n```tsx\n<input {...form.register(\"email\", { required: true, pattern: /@/ })} />\n```\n\nThe returned `value` is a **read-only signal bound directly to the element's**\n`value` **attribute**. Nullish model values surface as `\"\"` so an empty field\nrenders empty (never the string `\"undefined\"`); read the actual model value\nwith `getValues(name)` or `watch(name)`.\n\nCheckbox / radio / file inputs are driven through the `ref` (their `checked` /\n`files` state) — spreading `register` just works:\n\n```tsx\n<input type=\"checkbox\" {...form.register(\"acceptTerms\", { required: true })} />\n<input type=\"radio\" {...form.register(\"plan\")} value=\"pro\" />\n<input type=\"number\" {...form.register(\"age\", { valueAsNumber: true, min: 18 })} />\n```\n\n### Reading values: `values`, `watch`, `getValues`\n\n- `form.values` → a `ReadonlySignal<Values>` holding the **whole model**.\n  Render it directly to track the form without a re-render.\n- `form.watch(name?)` → a `ReadonlySignal` of one field (or the whole model\n  when called with no argument — the same signal as `form.values`).\n- `form.getValues(name?)` → a plain **snapshot** (uses `.peek()`, no\n  subscription). Use it inside event handlers.\n\n```tsx\n<pre>{JSON.stringify(form.values.value, null, 2)}</pre>; // reactive\nconst email = form.getValues(\"email\"); // one-off read\n```\n\n### Writing values: `setFieldValue`, `setValue`\n\n- `form.setFieldValue(name, value, options?)` → write **one** field.\n- `form.setValue(values, options?)` → replace the **whole model**. Paths\n  missing from `values` become `undefined`, array fields collapse to `[]`, and\n  values for paths that were never registered as fields are kept.\n\nBoth accept `{ shouldValidate, shouldDirty, shouldTouch }`.\n\n`setValue` swaps the values but leaves `defaultValues` untouched, so the form\ngoes **dirty** — it is not a reset. Use `form.reset(values?)` when you want the\nnew values to become the baseline that `isDirty` compares against.\n\n> **Note:** this differs from react-hook-form, where `setValue` is the\n> single-field writer. Here that is `setFieldValue`.\n\n### `reset(values?, options?)`\n\nRestores the model to `defaultValues` (or to `values`, which then **becomes**\nthe new default). Field arrays are realigned: items added via `append` that the\ndefaults don't contain are dropped, and an array with no entry in the defaults\nresets to `[]`. With no `defaultValues` at all, the model resets to an empty\nobject.\n\nOptions: `keepValues`, `keepDirty`, `keepErrors`, `keepTouched`,\n`keepIsSubmitted`, `keepSubmitCount`.\n\n`keepDirty` preserves the user's in-progress edits through the reset: every\nfield that is dirty (differs from the _old_ defaults) keeps its current value,\nwhile clean fields adopt the new ones — the classic \"background refetch must\nnot clobber unsaved edits\" case. Kept fields stay dirty against the new\nbaseline. This includes field arrays: structural changes and item edits\nsurvive, except edits to items the new defaults no longer contain.\n\n```tsx\nform.reset(); // back to the original defaults\nform.reset(serverData); // adopt server data as the new baseline\nform.reset(serverData, { keepDirty: true }); // refetch without losing edits\nform.reset(undefined, { keepValues: true }); // clear errors/touched, keep values\n```\n\n### `formState`\n\nEvery property is a signal — drop it straight into JSX:\n\n```tsx\n<button disabled={form.formState.isSubmitting}>Save</button>;\n{\n  form.formState.isDirty.value && <span>Unsaved changes</span>;\n}\n```\n\n`errors`, `isDirty`, `isValid`, `isValidating`, `isSubmitting`, `isSubmitted`,\n`isSubmitSuccessful`, `submitCount`, `dirtyFields`, `touchedFields`,\n`defaultValues`, `shared`.\n\n`shared` is a **writable** scratch signal (`Signal<{ [key: string]: any }>`)\nthe library never touches: use it to pass ad-hoc state between distant fields\nor components that already hold the form control, without wiring up your own\ncontext.\n\n```tsx\nform.formState.shared.value = { activeSection: \"billing\" };\n```\n\n### Showing errors\n\n`getFieldState(name)` returns reactive signals per field: `error`, `isDirty`,\n`isTouched`, `isValidating`, `invalid`.\n\n```tsx\nimport { computed } from \"@preact/signals\";\n\nconst nameError = computed(\n  () => form.getFieldState(\"name\").error.value?.message\n);\n// Rendered as a live text node — no re-render:\n<span class=\"error\">{nameError}</span>;\n```\n\n## Validation\n\n### Built-in rules\n\n```tsx\nform.register(\"email\", {\n  required: \"Email is required\",\n  pattern: { value: /^[^@]+@[^@]+$/, message: \"Invalid email\" },\n  minLength: { value: 5, message: \"Too short\" },\n});\n```\n\n`required`, `min`, `max`, `minLength`, `maxLength`, `pattern`, `validate`,\n`deps`, `disabled`, plus `valueAsNumber` / `valueAsDate` / `setValueAs`.\n\n### Custom validators\n\n`validate` takes a function receiving the **field value** and the **whole\nmodel** — return `true`/`undefined` when valid, or a message string:\n\n```tsx\nform.register(\"confirm\", {\n  validate: (value, values) =>\n    value === values.password || \"Passwords do not match\",\n});\n```\n\nAsync validators are awaited (`validate: async (v) => …`), and out-of-order\nresults are race-safe: when a newer validation starts while an older one is\nstill in flight, the stale run's result is discarded **and its** `AbortSignal`\n**fires** — pass it to `fetch` so the server request is actually cancelled, not\njust ignored:\n\n```tsx\nform.register(\"username\", {\n  validate: async (value, _values, signal) => {\n    const res = await fetch(`/api/check?u=${value}`, { signal });\n    const { taken } = await res.json();\n    return taken ? \"Already taken\" : true;\n  },\n});\n```\n\nThe signal also aborts when the field is reset, cleared or unregistered. A\nvalidator that rejects after its signal aborted (the normal `fetch` behaviour)\nends quietly. The same guard applies to async resolvers — a custom resolver\nreceives the signal as its second argument. To run several named\nchecks, pass a record — the failing key becomes `error.type`, and with\n`criteriaMode: \"all\"` every failure is collected into `error.types`:\n\n```tsx\nform.register(\"username\", {\n  validate: {\n    noSpaces: (v) => !v.includes(\" \") || \"No spaces allowed\",\n    lowercase: (v) => v === v.toLowerCase() || \"Must be lowercase\",\n  },\n});\n```\n\nCross-field checks need `deps` — \"when **this** field changes, re-validate\n**those**\":\n\n```tsx\nform.register(\"password\", { deps: [\"confirm\", \"passwordHint\"] });\n```\n\n> **Note:** configuring a `resolver` disables built-in rules (including\n> `validate`) entirely — the schema becomes the single source of truth.\n\n### Schema resolvers\n\nResolvers ship as separate entry points, so the schema library never lands in\nyour main bundle:\n\n```tsx\nimport { useForm } from \"@dmytromykhailiuk/preact-signal-hook-forms\";\nimport { zodResolver } from \"@dmytromykhailiuk/preact-signal-hook-forms/resolvers/zod\";\nimport { z } from \"zod\";\n\nconst schema = z.object({\n  email: z.string().email(),\n  age: z.number().min(18),\n});\n\nconst form = useForm({ resolver: zodResolver(schema) });\n```\n\nAlso available: `…/resolvers/valibot` and `…/resolvers/yup`.\n\n## Declarative components\n\n### `<Form>`\n\nOptional `<form>` wrapper: wires `handleSubmit` and shares the control through\ncontext, so descendants don't need prop-drilling:\n\n```tsx\n<Form control={form.control} onSubmit={(values) => save(values)}>\n  <Field name=\"email\" as=\"input\" type=\"email\" />\n  <button>Save</button>\n</Form>\n```\n\n### `<Field>`\n\nRender-prop or auto-binding, using the `control` prop or a surrounding\n`<Form>` / `<FormProvider>`:\n\n```tsx\n{\n  /* render-prop — full control over the markup */\n}\n<Field name=\"email\" rules={{ required: true }}>\n  {({ field, fieldState }) => <input {...field} type=\"email\" />}\n</Field>;\n\n{\n  /* auto-binding — `as` picks the element, extra props pass through */\n}\n<Field name=\"plan\" as=\"select\">\n  <option value=\"free\">Free</option>\n  <option value=\"pro\">Pro</option>\n</Field>;\n```\n\n### `<Controller>`\n\nFor third-party controlled components that can't bind a signal directly (this\none _does_ re-render locally on change):\n\n```tsx\n<Controller\n  control={form.control}\n  name=\"color\"\n  render={({ field }) => (\n    <ColorPicker value={field.value} onChange={field.onChange} />\n  )}\n/>\n```\n\n### `<FormProvider>` / `useFormContext`\n\n```tsx\n<FormProvider control={form.control}>\n  <DeeplyNestedFields />\n</FormProvider>;\n\nfunction DeeplyNestedFields() {\n  const form = useFormContext<Values>();\n  return <input {...form.register(\"email\")} />;\n}\n```\n\n## Dynamic lists — `useFieldArray`\n\n`fields` is a `ReadonlySignal` and the hook's return object is stable (created\nonce). Iterate it with the built-in `<For>` from `@preact/signals/utils` — then\neven adding/removing rows never re-renders the host component:\n\n```tsx\nimport { For } from \"@preact/signals/utils\";\n\nconst { fields, append, remove, move } = useFieldArray({\n  control: form.control,\n  name: \"items\",\n});\n\nreturn (\n  <>\n    <For each={fields} getKey={(f) => f.id}>\n      {(item, i) => (\n        <div>\n          <input {...form.register(`items.${i}.name`)} />\n          <button type=\"button\" onClick={() => remove(i)}>\n            ×\n          </button>\n        </div>\n      )}\n    </For>\n    <button type=\"button\" onClick={() => append({ name: \"\" })}>\n      Add\n    </button>\n  </>\n);\n```\n\nPrefer plain JSX? Read the signal with `fields.value.map(...)` — that subscribes\nthe component, so it re-renders on structural changes only (never on value edits).\n\nMethods: `append`, `prepend`, `insert`, `remove`, `swap`, `move`, `replace`,\n`update`, `clear`. `fields` recomputes only on structural changes; individual\nfield edits always flow through signals without a re-render.\n\n`clear()` empties the array outright, dropping the default items along with\nanything `append` added. To go back to the defaults instead, call\n`form.reset()` — see `[reset](#resetvalues-options)`.\n\n> **Note:** item `id`s are regenerated by `reset` (its child field nodes are\n> re-created), so don't persist them outside the form.\n\n## Other hooks\n\n- `useWatch({ control, name? })` — a `ReadonlySignal` of one field or the whole\n  model; identical to `control.watch(name?)`, handy when only the control is in\n  scope.\n- `useFormState({ control })` — the reactive `formState` object.\n- `useController({ control, name, rules?, defaultValue? })` — the imperative\n  core of `<Controller>`, for building custom bound components.\n- `useFormContext<Values>()` — the nearest control provided by `<Form>` or\n  `<FormProvider>`.\n\n## Migrating from react-hook-form\n\n| react-hook-form                                | preact-signal-hook-forms                                             |\n| ---------------------------------------------- | -------------------------------------------------------------------- |\n| `watch(\"x\")` → value (re-renders)              | `watch(\"x\")` → **signal** (no re-render)                             |\n| `formState.isDirty` → boolean                  | `formState.isDirty` → **signal** (`.value`)                          |\n| `errors.x?.message`                            | `getFieldState(\"x\").error.value?.message`                            |\n| `<Controller>` for everything                  | `register` for native inputs, `<Controller>` for the rest            |\n| `register(\"x\")` returns `{ onChange, ref, … }` | also returns a bindable `value` **signal**                           |\n| `setValue(\"x\", v)` → writes one field          | `setFieldValue(\"x\", v)`; `setValue(values)` replaces the whole model |\n| `getValues()` / `watch()`                      | also `form.values` — the whole model as one signal                   |\n\nThe mental shift: **read** `.value` **(or render the signal directly) instead of\nexpecting a re-render.**\n\n## API reference\n\nFull TSDoc ships with the package (hover in your editor). Public surface:\n\n- Hooks: `useForm`, `useWatch`, `useFormState`, `useController`, `useFieldArray`,\n  `useFormContext`\n- Components: `Form`, `Field`, `Controller`, `FormProvider`\n- Core: `createFormControl` (framework-agnostic controller factory),\n  `FormControlContext`\n- Resolvers: `zodResolver`, `valibotResolver`, `yupResolver`\n- Types: `FieldValues`, `FieldPath`, `FieldPathValue`, `RegisterOptions`,  \n  `RegisterReturn`, `FieldError`, `FieldErrors`, `FormState`, `FieldState`,  \n  `Resolver`, `SetValueOptions`, `ResetOptions`, `UseFormOptions`, …\n\n## License\n\n[MIT](./LICENSE) © Dmytro Mykhailiuk\n","readmeFilename":"README.md"}