Releases: pavelivanov/formular
Release list
v4.0.0-beta.3
4.0.0-beta.3 β unreleased
Added
formular/testingsubpath export with acreateTestForm<T>(options)
helper. Returns{ form, Provider }β aForminstance you can read
and mutate directly from test code, plus a minimal React wrapper that
puts that form on context for rendered components. Renderer-agnostic
(works with@testing-library/react, Enzyme, barereact-dom, etc.).FormContextProvidernow accepts an optionalformprop. When
provided, the Provider puts the caller-controlled instance on context
and skips its internal lifecycle (nosetOptions/setInitialValues
re-seed/destroy-on-unmount). Additive β existing usage is unchanged.
Primarily for tests and advanced users who want to own the form's
lifetime.
Bundle
| Bundle | Limit (brotli) | Actual |
|---|---|---|
| ESM entry | 6.5 kB | 5.99 kB (+30 B for the provider split) |
| CJS entry | 7 kB | 6.25 kB |
| Tree-shaken core | 5 kB | 4.35 kB |
| Devtools subpath | 5 kB | 3.12 kB |
| Testing subpath | 5 kB | 4.21 kB (new) |
v4.0.0-beta.2
4.0.0-beta.2 β unreleased
Added
formular/devtoolssubpath export with a floating
<FormularDevtools />inspection panel. Three tabs: state (values,
errors, flags), fields (registered paths + per-field flags), events
(rolling log of every form event). Self-contained inline styles, no
portals, no global CSS, opt-in via explicit render. Props for
position, default open/tab, and anenabledkill-switch.- Shipping as a subpath export of the same package (not a separate
npm name) β single version, and tree-shakes out of the main bundle
when not imported. The devtools bundle weighs 3.1 KB brotli;
main bundle is unchanged. - Integrated into
examples/vite-reactso you can poke at it via
npm run dev.
Changed
tsdown.config.tsswitched from a single multi-entry build to two
independent build passes (dist/index.js+dist/devtools/index.js).
The multi-entry approach produced a shared chunk that every entry
had to load, inflating the main bundle for consumers that don't use
devtools. Two passes produce self-contained bundles β devtools pays
a small code-duplication cost, main bundle stays lean.
Bundle
| Bundle | Limit (brotli) | Actual |
|---|---|---|
| ESM entry | 6.5 kB | 5.96 kB (unchanged) |
| CJS entry | 7 kB | 6.22 kB (unchanged) |
| Tree-shaken core | 5 kB | 4.32 kB (unchanged) |
| Devtools subpath | 5 kB | 3.12 kB (new) |
v4.0.0-beta.1
4.0.0-beta.1
First published release of the v4 line. The pre-release line under the
next dist-tag; latest remains on v3 until external adopters
confirm stability. Rollup of everything built across six in-tree alphas.
Summary
- Ground-up rewrite. The public API from v3 is not preserved β see
MIGRATION.md for the v3 β v4 mapping. - Class-based
Form<T>+FieldManager<T>pair.FormContextProvider
puts a single form on React context; hooks subscribe to it via
useSyncExternalStore. Components re-render only on the slice they
read. - Peer dep:
react >=18. Bundle: 5.96 KB brotli (ESM, everything),
4.32 KB (tree-shaken core). Under CI size budgets.
What's in this release
Core
Form<T>/FieldManager<T>replace v3'sForm/Field/
FormGrouptrio. Nesting is expressed via field names;FormGroup
is not ported.FormContextProvider<T>+useFormContext<T>for an ambient form.useSyncExternalStore-based hooks βuseForm,useFormState,
useFieldRegister,useField,useFormValidationβ each
subscribes to a specific slice so siblings don't thrash.- Async validation sequenced by a monotonic counter (replaces v3's
CancelablePromise). Stale runs discard themselves. handleSubmit(onSuccess?)returns a React-safe submit handler.
Distinctsubmitandsubmit errorevents with a
phase: 'submit' | 'onSuccess'tag on errors.setValues/setInitialValuesbuffer writes for fields that
haven't registered yet, so async-hydrated data doesn't race field
mounts.
Typed API (the headline)
-
createForm<T>()factory closes the form shape over a namespace
of hooks βuseFieldRegister,useFieldArray,useField, plus the
Provider β so literal path arguments narrow at the call site:const contact = createForm<ContactForm>() const street = contact.useFieldRegister('address.street') // ^? FieldManager<string>
TypeScript's partial-explicit-generic inference doesn't narrow
consttype parameters in hook signatures; the factory sidesteps
that by keeping only one generic in play at call-sites. -
Typed nested paths:
Path<T>,PathValue<T, P>,ArrayPath<T>,
andDeepPartial<T>are exported.setValues/setInitialValues
takeDeepPartial<T>and walk down to each registered field.
getValues()reconstructs the nested shape.
Validation
- Standard Schema v1 accepted directly at the field level
(Zod 3.24+, Valibot 0.40+, ArkType, β¦). TheFieldOptions.schema
option runs the schema beforevalidators; the first issue message
becomes the field error. No@hookform/resolvers-style adapter
package required β the interface is inlined at
src/standard-schema.tsand re-exported asStandardSchemaV1. - Pipeline order:
requiredβschemaβvalidators. Each stage
short-circuits on the first error. - 15 built-in function validators kept for BYO (
minLength,
maxLength,pattern,email,url,phoneNumber,numeric,
min,max,creditCard,dateFormat,minAge,confirmField,
asyncValidator,compose). Recommendation in the README: prefer
schemafor new projects.
Field arrays
useFieldArray(path)with the usual list operations:append,
prepend,insert,remove,swap,move,replace,clear.
Returns{ fields, ...ops }wherefields[i].idis stable across
mutations so React keys survive reorders.- Per-row field reindexing: sub-fields registered at
items.<N>.<rest>are atomically renamed when list operations run.
A field's state (value, error, touched, subscriptions) carries over
to its new index rather than being orphaned.replaceandclear
destroy the sub-fields; new rows register fresh. Observable via the
field renamedevent.
Components
- Headless
FieldLabelandFieldError. Plain<label>and an
aria-live="polite"div. No styling opinions β pass your own
classNames.
Tooling
-
Build: tsdown. ESM + CJS output with a proper
exportsmap and
sideEffects: false. -
Test: Vitest. 74 tests colocated under
src/**/*.spec.ts. -
Lint: ESLint 9 flat config.
-
CI matrix: Node 18 / 20 / 22 running
typecheck+lint+buildtest+size.
-
Bundle-size budget in CI via
size-limit:Bundle Limit (brotli) Actual ESM entry 6.5 kB 5.96 kB CJS entry 7 kB 6.22 kB Tree-shaken core 5 kB 4.32 kB
Notable fixes discovered during the alpha series
setByPathused to mutate caller-owned arrays and objects when
reconstructing form state, which silently corrupted field-array
internals (out.rows[2] = {}on a 2-item field would expand the
live array to 3 items).setByPathnow shallow-copies each
container on descend.unregisterFieldnow takes an optionalexpectedFieldreference
and skips destruction if a different FieldManager occupies the
path β otherwise a staleuseEffectcleanup could destroy a field
that a concurrent array reindex moved there.FieldManager.validateno longer hangs when a validator throws
(v3 bug that survived into alpha.1; fixed in alpha.2).
Example app
examples/vite-react is a runnable Vite + React 19 contact form
exercising nested paths, useFieldArray, and Zod schemas (including
an async .refine()). See its README.
v3.0.0
Form.submit and FormGroup.submit result format changed. Beware updating to this version!
< 3.0.0
form.submit()
.then((values) => {
})
.catch((errors) => {
// validation errors
})
or
try {
const values = await form.submit()
}
catch (errors) {
// validation errors
}
>= 3.0.0
form.submit()
.then(({ values, errors }) => {
if (errors) {
// handle errors
}
else {
// do smth
}
})
or
const { values, errors } = await form.submit()
The Reason
try {
const values = await form.submit()
// other code that can cause an error
}
catch (errors) {
// errors can contain not only form validation errors but any type of error from code after form.submit()
}