Releases: edmundhung/conform
v1.19.0
Breaking Changes (Future APIs)
-
Removed deprecated exports from
@conform-to/react/future(#1183)FormOptionsProvidercomponentBaseMetadatatypeBaseErrorShapetypeCustomTypestypeCustomMetadatatypeCustomMetadataDefinitiontype
The deprecated
schemaoption in the futureuseFormhook is also removed. If you were validating with a schema, pass the schema as the first argument:useForm(schema, options)instead ofuseForm({ schema, ... }). -
Improved
ErrorShapehandling through out the future APIs.form.errorsandfield.errorsnow use yourErrorShapedirectly instead of always wrapping it in an array. (#1183)If you were using a custom
ErrorShape, either by specifying it in the generics or through theisErroroption fromconfigureForms, update it fromErrorShapetoErrorShape[]to keep the array structure you had before. For example, if you previously usedstring, change it tostring[]to keep the errors as arrays of strings:const forms = configureForms({ - isError: shape<string>(), + isError: shape<string[]>(), }); -
report()now accepts either schemaissuesor an explicit error payload, but not both in the sameerrorobject. If you were combining them before, append the extra message as another issue instead. (#1183)const result = schema.safeParse(submission.payload); const isEmailUnique = await checkEmailUnique(submission.payload.email); if (!result.success || !isEmailUnique) { const issues = !result.success ? [...result.error.issues] : []; + if (!isEmailUnique) { + issues.push({ + message: 'Email is already taken', + path: ['email'], + }); + } return { result: report(submission, { error: { issues, - fieldErrors: { - email: !isEmailUnique ? ['Email is already taken'] : [], - }, }, }), }; }
What's Changed
-
Added support for customizing serializer based on field name (#1184)
The
serializeoptions now let you customize serialization based onctx.name, both globally throughconfigureForms()and per form throughuseForm(), while delegating everything else back to Conform's default serializer withctx.defaultSerialize.This changes the serializer callback signature to:
serialize(value, ctx) { // Custom serialization for a specific field name if (ctx.name === 'metadata') { return typeof value === 'string' || value == null ? value : JSON.stringify(value); } return ctx.defaultSerialize(value); }
Full Changelog: v1.18.0...v1.19.0
v1.18.0
Breaking Changes (Future APIs)
-
Renamed the path helpers in
@conform-to/dom/futureexports for consistency across the API surface. If you're using these helpers directly, you'll need to update your imports and call sites. (#1172)getPathSegments->parsePath
formatPathSegments->formatPath
appendPathSegment->appendPath
getValueAtPath->getPathValue
setValueAtPath->setPathValue
What's Changed
-
Added support for structured values in future
useControl(). Custom inputs can now register a hidden<fieldset>as their base control, which makes it much easier to build components that work with objects and nested arrays. This release also adds a newBaseControlcomponent to render those hidden native controls for you, and exposesfield.defaultPayloadso structured controls can be initialized from the field's default value. (#1173)const control = useControl({ defaultValue: field.defaultPayload, parse(payload) { return DateRangeSchema.parse(payload); }, }); <BaseControl type="fieldset" name={field.name} ref={control.register} defaultValue={control.defaultValue} /> <DateRangePicker value={control.payload} onChange={(value) => control.change(value)} />
-
Added
coerceStructure()to the future Zod and Valibot APIs. UnlikecoerceFormValue(), this API focuses on converting form string values into typed data without running the schema's validation rules, which makes it useful when you want to read the current form state as typed values during rendering or interaction. (#1169)import { coerceStructure } from '@conform-to/zod/v4/future'; const schema = z.object({ age: z.number().min(0).max(120), subscribe: z.boolean(), }); const value = coerceStructure(schema).parse({ age: '42', subscribe: '', }); // { age: 42, subscribe: false }
-
Added
configureCoercion()to the future Zod and Valibot APIs so coercion behavior can be customized in one place and reused across bothcoerceFormValue()andcoerceStructure(). This replaces the olderdefaultCoercionandcustomizeoptions oncoerceFormValue(). (#1169)import { configureCoercion } from '@conform-to/zod/v4/future'; const { coerceFormValue, coerceStructure } = configureCoercion({ stripEmptyString: (value) => { const trimmed = value.trim(); return trimmed === '' ? undefined : trimmed; }, type: { number: (text) => Number(text.trim().replace(/,/g, '')), }, });
-
Improved
datetime-localhandling in future APIs. Date values now serialize to datetime strings without a trailingZ, and timezone-less datetime strings are now coerced as UTC by the Zod and Valibot integrations. This better matches howdatetime-localinputs behave in the browser and avoids the confusion that can come from mixing local-looking inputs with explicit UTC suffixes. No changes were made to the v1 APIs. (#1174) -
Improved recursive schema support across the schema integrations. Zod constraint resolution now works correctly for nested recursive schemas, including getter-based schemas and
z.lazy(). Valibot now also resolves constraints more reliably for recursive schemas and normalizes indexed lookups likeitems[0].nameto the corresponding structural path.coerceFormValue()for Valibot also now handlesv.lazy()schemas correctly. (#1166)This means nested fields inside tree-like structures, recursive collections, and lazily-defined schemas should now derive constraints and coercion behavior more reliably than before.
-
parseSubmission()now preserves empty strings and files by default by changingstripEmptyValuestofalse. This is a behavioral improvement because empty values can carry meaning, such as distinguishing "the user cleared this field" from "this field was never submitted". The option is now deprecated and will be removed in a future minor release. If you're already using Zod or Valibot integration, empty-value stripping is already handled during validation where appropriate. (#1171) -
Fixed the Valibot
coerceFormValue()return type to match the schema object it actually returns. Previously, TypeScript could imply that the returned schema still exposed properties from the original schema shape even when that was not true at runtime. There are no runtime behavior changes here, but the typings are now more honest and prevent misleading property access in TypeScript. (#1169) -
Fixed native form submission after async client validation. Previously, Conform could re-dispatch the submit event immediately after async validation finished, but browsers could still ignore that follow-up submission. Submission is now resumed on the next task so the original native form flow can continue more reliably. (#1177)
This primarily helps forms that rely on async client-side validation but still expect normal browser submission semantics once validation succeeds.
Documentation / Examples
- Added an Astro example (#1177)
Full Changelog: v1.17.1...v1.18.0
v1.17.1
What's Changed
- Fixed
coerceFormValuereturn type to no longer pretend to be the original schema type in TypeScript. This means properties like.shapethat don't exist on the coerced schema will now correctly produce a type error instead of failing silently at runtime. (#1164) - Fixed
coerceFormValueandgetZodConstraintcausing stack overflow when used with Zod v4's getter-based recursive schemas (#1165)
Full Changelog: v1.17.0...v1.17.1
v1.17.0
What's Changed
-
Added
PreserveBoundarycomponent for preserving form values when fields are unmounted during client-side navigation (e.g., multi-step wizards, form dialogs, virtualized lists). (#1148)import { PreserveBoundary } from '@conform-to/react/future'; {step === 1 ? ( <PreserveBoundary name="step-1"> <input name="name" /> <input name="email" /> </PreserveBoundary> ) : ( <PreserveBoundary name="step-2"> <input name="address" /> <input name="city" /> </PreserveBoundary> )}
-
Added dynamic behavior options for array intents.
insertandremovecan now change behavior based on validation errors usingonInvalidandfromoptions. (#1154)intent.insert({ name: fields.tags.name, // Read and validate a value from another field before inserting. // If valid, the value is inserted and the source field is cleared. // If invalid, the error is shown on that field. from: fields.newTag.name, // Cancel the insert if it causes array validation errors (e.g., exceeding max items) onInvalid: 'revert', }); intent.remove({ name: fields.tags.name, index: 0, // Cancel the removal if it causes validation errors (e.g., going below min items) onInvalid: 'revert', // Or use 'insert' to continue removing but insert a new item at the end // onInvalid: 'insert', });
These options require the validation error to be available synchronously. See the docs for guidance on combining with async validation.
-
getConstraints(zod v4 and valibot) now derives anacceptconstraint from file schemas with MIME type configuration by @transparent-citizen. (#1151)import { getConstraints } from '@conform-to/zod/v4/future'; const schema = z.object({ avatar: z.file().mime(['image/png', 'image/jpeg']), }); getConstraints(schema); // { avatar: { required: true, accept: 'image/png,image/jpeg' } }
-
Fixed missing
'use client'directive on internal modules that caused React Server Component frameworks to incorrectly treat parts of@conform-to/reactas server code. (#1157)
New Contributors
- @transparent-citizen made their first contribution in #1151
Full Changelog: v1.16.0...v1.17.0
v1.16.0
Breaking Changes (Future APIs)
-
The
useFormData()selector is now only called when the form is available. When unavailable, the hook returnsundefinedor the newfallbackoption if provided. (#1142)// Before const value = useFormData(formRef, (formData) => { if (formData === null) return ''; return formData.get('name') ?? ''; }); // After const value = useFormData(formRef, (formData) => formData.get('name') ?? '', { fallback: '', });
What's Changed
-
Introduced the
configureFormsfuture API, a factory function that creates form hooks with custom configuration and full type inference. It replacesFormOptionsProvider(now deprecated). (#1131)import { configureForms } from '@conform-to/react/future'; import { getConstraints } from '@conform-to/zod/v3/future'; const { useForm, useField, FormProvider } = configureForms({ shouldValidate: 'onBlur', getConstraints, extendFieldMetadata(metadata) { return { get textFieldProps() { return { name: metadata.name, defaultValue: metadata.defaultValue, 'aria-invalid': !metadata.valid, }; }, }; }, });
-
Added
getConstraintsandisSchemato@conform-to/zodand@conform-to/valibotfuture exports.getConstraintsreplacesgetZodConstraintandgetValibotConstraint(now deprecated). (#1131) -
Fixed unbound-method TypeScript ESLint error by using arrow function syntax for
change,focus, andblurmethods in theControltype (#1129)
Documentation
- Fixed the number of properties returned by
useFormhook in the docs by @DarkstarXDD (#1136) - Added guidance for Zod v4 import path (#1146)
New Contributors
- @DarkstarXDD made their first contribution in #1136
Full Changelog: v1.15.1...v1.16.0
v1.15.1
What's Changed
- Added support for nullable constraints in
getZodConstraint(#1126) - Fixed
useControlnot reflecting the input's default value in the DOM (#1121) - Fixed
useControlnot dispatching a change event when callingcontrol.change()with the input's default value (#1122) - Fixed
parseWithZodandparseWithValibotincorrectly treating falsy result values as errors (#1115)
New Contributors
Full Changelog: v1.15.0...v1.15.1
v1.15.0
What's Changed
-
Added a getFieldValue helper to extract and validate field values from FormData or URLSearchParams. (#1112)
import { getFieldValue } from '@conform-to/react/future'; // Basic: returns `unknown` const email = getFieldValue(formData, 'email'); // With type guard: returns `string`, throws if not a string const name = getFieldValue(formData, 'name', { type: 'string' }); // File type: returns `File`, throws if not a File const avatar = getFieldValue(formData, 'avatar', { type: 'file' }); // Object type: parses nested fields into `{ city: unknown, ... }` const address = getFieldValue<Address>(formData, 'address', { type: 'object' }); // Array: returns `unknown[]` const tags = getFieldValue(formData, 'tags', { array: true }); // Array of objects: returns `Array<{ name: unknown, ... }>` const items = getFieldValue<Item[]>(formData, 'items', { type: 'object', array: true, }); // Optional: returns `string | undefined`, no error if missing const bio = getFieldValue(formData, 'bio', { type: 'string', optional: true });
It also infers types from the field name:
import { useForm, useFormData, getFieldValue } from '@conform-to/react/future'; function Example() { const { form, fields } = useForm(); // Retrieves the value of the `address` fieldset as an object, e.g. `{ city: unknown; ... }` const address = useFormData(form.id, (formData) => getFieldValue(formData, fields.address.name, { type: 'object' }), ); // ... }
Full Changelog: v1.14.1...v1.15.0
v1.14.1
What's Changed
- Relaxed the
FormConfigtype to allow bothlastResultandonSubmitto be optional (#1116)
Full Changelog: v1.14.0...v1.14.1
v1.14.0
Breaking Changes (Future APIs)
-
The
intendedValueoption in the report helper has been renamed tovalueand now works as thedefaultValuewhen resetting the form. Previously, this option was ignored when resetting and the form would always reset to the default value. You can now use thevalueoption to update or reset forms to a specific value. (#1079)// Update form to a specific value after submission return { result: report(submission, { value: updatedValue, }), }; // Reset form to a specific value after submission return { result: report(submission, { reset: true, value: defaultValue, }), };
-
parseSubmissionnow strips empty values by default. This makes it easier to work with schemas directly (withoutcoerceFormValue) since you no longer need extra validation like.min(1)for required fields. You can setstripEmptyValues: falseto preserve empty values if needed. (#1110)const formData = new FormData(); // Empty text input formData.append('name', ''); // Empty file input formData.append('files[]', new File([], '')); parseSubmission(formData); // { payload: {} } parseSubmission(formData, { stripEmptyValues: false }); // { payload: { name: '', files: [new File([], '')] } }
What's Changed
-
Schema-first future
useFormhook with improved type inference (#1106)The
schemaoption is now promoted to the first argument ofuseFormfor better type inference:// Before: schema in options const { form, fields } = useForm({ schema: mySchema, onSubmit(event, { value }) { // value type inference could be inconsistent }, }); // After: schema as first argument const { form, fields } = useForm(mySchema, { onSubmit(event, { value }) { // value is fully typed based on your schema }, });
onValidateis now required when not using a schemaEither(Relaxed the type to allow both to be optional in v1.14.1)onSubmitorlastResultmust be provided
The old API with
schemain options still works but is now deprecated. It will be removed in the next minor release. -
Fixed
parseSubmissionarray handling for entries ending with[]. Previously, when multiple form entries had the same name ending with[](e.g.,todos[]), all items were incorrectly pushed as a single nested array element. Now they are correctly spread as individual array items. (#1108)const formData = new FormData(); formData.append('todos[]', 'Buy milk'); formData.append('todos[]', 'Walk dog'); formData.append('todos[]', 'Write tests'); parseSubmission(formData); // Before (incorrect): { todos: [['Buy milk', 'Walk dog', 'Write tests']] } // After (correct): { todos: ['Buy milk', 'Walk dog', 'Write tests'] }
Improvements
- You can now import from
@conform-to/zod/v3if you need to work with v3 schema using zod v4. (Thanks @kesoji - #1090) - Fixed type inference for
getFieldset()with interface declarations (#1097) - Moved
lastResultlogic from an effect to the render phase. Your form component may now render twice within a single lifecycle when needed, but state updates that previously spanned two separate lifecycles now complete in one. (#1103) - Improved
FormOptionsandValidationAttributestypes compatibility withexactOptionalPropertyTypessetting in tsconfig. (#1105)
Full Changelog: v1.13.3...v1.14.0
v1.13.3
What's Changed
- Fixed Zod v4
.required()support with coerceFormValue by @chimame (#1084)
Full Changelog: v1.13.2...v1.13.3