v1.0.0-rc.0
Pre-releaseThis release candidate is a complete rewrite of the library.
You can find the update remix example at https://stackblitz.com/github/edmundhung/conform/tree/v1.0.0-rc.0/examples/remix
or try it out locally using the following command:
npm install @conform-to/react@next @conform-to/zod@next
Breaking Changes
-
The minimum react version supported is now React 18
-
All conform helpers are renamed.
conform.input
->getInputProps
conform.select
->getSelectProps
conform.textarea
->getTextareaProps
conform.fieldset
->getFieldsetProps
conform.collection
->getCollectionProps
-
The
type
option ongetInputProps
is now required.
function Example() {
return <input {...getInputProps(fields.title, { type: 'text' })} />;
}
form.props
is removed. You can use the helpergetFormProps()
instead.
import { getFormProps } from '@conform-to/react';
function Example() {
const [form] = useForm();
return <form {...getFormProps(form)} />;
}
-
conform.INTENT
is removed. If you need to setup an intent button, please use the name "intent" or anything you preferred. -
You will find
conform.VALIDATION_UNDEFINED
andconform.VALIDATION_SKIPPED
on our zod integration (@conform-to/zod
) instead.conform.VALIDATION_UNDEFINED
->conformZodMessage.VALIDATION_UNDEFINED
conform.VALIDATION_SKIPPED
->conformZodMessage.VALIDATION_SKIPPED
.
-
The
parse
helper on@conform-to/zod
is now calledparseWithZod
withgetFieldsetConstraint
renamed togetZodConstraint
-
The
parse
helper on@conform-to/yup
is now calledparseWithYup
withgetFieldsetConstraint
renamed togetYupConstraint
-
Both
useFieldset
anduseFieldList
hooks are removed. You can now usemeta.getFieldset()
ormeta.getFieldList()
instead.
function Example() {
const [form, fields] = useForm();
// Instead of `useFieldset(form.ref, fields.address)`, it is now:
const address = fields.address.getFieldset();
// Instead of `useFieldList(form.ref, fields.tasks)`, it is now:
const tasks = fields.tasks.getFieldList();
return (
<form>
<ul>
{tasks.map((task) => {
// It is no longer necessary to define an addtional component
// As you can access the fieldset directly
const taskFields = task.getFieldset();
return <li key={task.key}>{/* ... */}</li>;
})}
</ul>
</form>
);
}
Improved submission handling
We have redesigned the submission object received after parsing the formdata to simplify the setup with a new reply
API for you to set additional errors or reset the form.
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, { schema });
/**
* The submission status could be either "success", "error" or undefined
* If the status is undefined, it means that the submission is not ready (i.e. `intent` is not `submit`)
*/
if (submission.status !== 'success') {
return json(submission.reply(), {
// You can also use the status to determine the HTTP status code
status: submission.status === 'error' ? 400 : 200,
});
}
const result = await save(submission.value);
if (!result.successful) {
return json(
submission.reply({
// You can also pass additional error to the `reply` method
formError: ["Submission failed"],
fieldError: {
address: ["Address is invalid"],
},
// or avoid sending the the field value back to client by specifying the field names
hideFields: ['password'],
})
);
}
// Reply the submission with `resetForm` option
return json(submission.reply({ resetForm: true }));
}
export default function Example() {
const lastResult = useActionData<typeof action>();
const [form, fields] = useForm({
// `lastSubmission` is renamed to `lastResult` to avoid confusion
lastResult,
});
// We can now find out the status of the submission from the form metadata as well
console.log(form.status); // "success", "error" or undefined
}
Simplified integration with the useInputControl
hook
The useInputEvent
hook is replaced by the useInputControl
hook with some new features.
-
There is no need to provide a ref of the inner input element anymore. It looks up the input element from the DOM and will insert one for you if it is not found.
-
You can now use
control.value
to integrate a custom input as a controlled input and update the value state throughcontrol.change(value)
. The value will also be reset when a form reset happens
import { useInputControl } from '@conform-to/react';
import { CustomSelect } from './some-ui-library';
function Example() {
const [form, fields] = useForm();
const control = useInputControl(fields.title);
return (
<CustomSelect
name={fields.title.name}
value={control.value}
onChange={(e) => control.change(e.target.value)}
onFocus={control.focus}
onBlur={control.blur}
/>
);
}
Refined intent button setup
- Both
validate
andlist
exports are removed in favor of setting up through the form metadata object.validate
->form.validate
list.insert
->form.insert
list.remove
->form.remove
list.reorder
->form.reorder
list.replace
->form.update
function Example() {
const [form, fields] = useForm();
const tasks = fields.tasks.getFieldList();
return (
<form>
<ul>
{tasks.map((task) => {
return <li key={task.key}>{/* ... */}</li>;
})}
</ul>
<button {...form.insert.getButtonProps({ name: fields.tasks.name })}>
Add (Declarative API)
</button>
<button onClick={() => form.insert({ name: fields.tasks.name })}>
Add (Imperative API)
</button>
</form>
);
}
- You can now reset a form with
form.reset
or update any field value withform.update
Form Context
By setting up a react context with the <FormProvider />
, we will now be able to subscribe to the form metadata using the useField()
hook. This not only avoids prop drilling but also prevent unneccessary re-renders by tracking the usage of indivudal metadata through a proxy and only rerender it if the relevant metadata is changed.
The <FormProvider />
can also be nesteded with different form context and Conform will look up the closest form context unless a formId
is provided.
import { type FieldName, FormProvider, useForm, useField } from '@conform-to/react';
function Example() {
const [form, fields] = useForm({ ... });
return (
<FormProvider context={form.context}>
<form>
<AddressFieldset name={fields.address.name} />
</form>
</FormProvider>
);
}
// The `FieldName<Schema>` type is basically a string with additional type information encoded
type AddressFieldsetProps = {
name: FieldName<Address>
}
export function AddressFieldset({ name }: AddressFieldsetProps) {
const [meta] = useField(name);
const address = meta.getFieldset();
// ...
}
If you want to create a custom input component, it is now possible too!
import { type FieldName, FormProvider, useForm, useField, getInputProps } from '@conform-to/react';
function Example() {
const [form, fields] = useForm({ ... });
return (
<FormProvider context={form.context}>
<form>
<CustomInput name={fields.title.name} />
</form>
</FormProvider>
);
}
type InputProps = {
name: FieldName<string>
}
// Make your own custom input component!
function CustomInput({ name }: InputProps) {
const [
meta,
form, // You can also access the form metadata directly
] = useField(name);
return (
<input {...getInputProps(meta)} />
);
}
Similarly, you can access the form metadata on any component using the useFormMetadata()
hook:
import { type FormId, FormProvider, useForm, getFormProps } from '@conform-to/react';
function Example() {
const [form, fields] = useForm({ ... });
return (
<FormProvider context={form.context}>
<CustomForm id={form.id}>
{/* ... */}
</CustomForm>
</FormProvider>
);
}
function CustomForm({ id, children }: { id: FormId; children: ReactNode }) {
const form = useFormMetadata(id);
return (
<form {...getFormProps(form)}>
{children}
</form>
);
}