Getting Started / Upgrading to v1

Upgrading to v1

In this guide, we will walk you through all the changes that were introduced in v1 and how to upgrade your existing codebase.

#Minimum React version

Conform now requires React 18 or higher. If you are using an older version of React, you will need to upgrade your react version first.

#The conform object is removed

First, all helpers are renamed and can be imported individually:

If you are using conform.VALIDATION_UNDEFINED and conform.VALIDATION_SKIPPED before, you will find them on our zod integration (@conform-to/zod) instead.

Be aware that conform.INTENT is no longer exported. If you need to setup an intent button, you can name it intent (or anything you preferred) in combination with z.discriminatedUnion() from zod for better type safety.

There are also some breaking changes on the options:

  • The type option on getInputProps is now required.
1<input {...getInputProps(fields.title, { type: 'text' })} />
  • The description option is renamed to ariaDescribedBy and expects a string (the id of the description element) instead of a boolean.
1<input
2  {...getInputProps(fields.title, {
3    ariaDescribedBy: fields.title.descriptionId,
4  })}
5/>

#Form setup changes

First,form.props is removed. You can use the getFormProps() helper instead.

1import { useForm, getFormProps } from '@conform-to/react';
2
3function Example() {
4  const [form] = useForm();
5
6  return <form {...getFormProps(form)} />;
7}

Both useFieldset and useFieldList hooks are removed. You can call the getFieldset() or getFieldList() method on the field metadata instead.

1function Example() {
2  const [form, fields] = useForm();
3
4  // Before: useFieldset(form.ref, fields.address)
5  const address = fields.address.getFieldset();
6  // Before: useFieldList(form.ref, fields.tasks)
7  const tasks = fields.tasks.getFieldList();
8
9  return (
10    <form>
11      <ul>
12        {tasks.map((task) => {
13          // It is no longer necessary to define an additional component
14          // with nested list as you can access the fieldset directly
15          const taskFields = task.getFieldset();
16
17          return <li key={task.key}>{/* ... */}</li>;
18        })}
19      </ul>
20    </form>
21  );
22}

Both validate and list exports are merged into the form metadata object:

1function Example() {
2  const [form, fields] = useForm();
3  const tasks = fields.tasks.getFieldList();
4
5  return (
6    <form>
7      <ul>
8        {tasks.map((task) => {
9          return <li key={task.key}>{/* ... */}</li>;
10        })}
11      </ul>
12      <button {...form.insert.getButtonProps({ name: fields.tasks.name })}>
13        Add (Declarative API)
14      </button>
15      <button onClick={() => form.insert({ name: fields.tasks.name })}>
16        Add (Imperative API)
17      </button>
18    </form>
19  );
20}

Here are all the equivalent methods:

  • validate -> form.validate
  • list.insert -> form.insert
  • list.remove -> form.remove
  • list.reorder -> form.reorder
  • list.replace -> form.update
  • list.append and list.prepend are removed. You can use form.insert instead.

#Schema integration

We have also renamed the APIs on each of the integrations with an unique name to avoid confusion. Here are the equivalent methods:

@conform-to/zod

@conform-to/yup

#Improved submission handling

We have redesigned the submission object to simplify the setup.

1export async function action({ request }: ActionArgs) {
2  const formData = await request.formData();
3  const submission = parseWithZod(formData, { schema });
4
5  /**
6   * The submission status could be either "success", "error" or undefined
7   * If the status is undefined, it means that the submission is not ready (i.e. `intent` is not `submit`)
8   */
9  if (submission.status !== 'success') {
10    return json(submission.reply(), {
11      // You can also use the status to determine the HTTP status code
12      status: submission.status === 'error' ? 400 : 200,
13    });
14  }
15
16  const result = await save(submission.value);
17
18  if (!result.successful) {
19    return json(
20      submission.reply({
21        // You can also pass additional error to the `reply` method
22        formErrors: ['Submission failed'],
23        fieldErrors: {
24          address: ['Address is invalid'],
25        },
26
27        // or avoid sending the the field value back to client by specifying the field names
28        hideFields: ['password'],
29      }),
30    );
31  }
32
33  // Reply the submission with `resetForm` option
34  return json(submission.reply({ resetForm: true }));
35}
36
37export default function Example() {
38  const lastResult = useActionData<typeof action>();
39  const [form, fields] = useForm({
40    // `lastSubmission` is renamed to `lastResult` to avoid confusion
41    lastResult,
42  });
43
44  // We can now find out the status of the submission from the form metadata as well
45  console.log(form.status); // "success", "error" or undefined
46}

#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 through control.change(value). The value will also be reset when a form reset happens

1import { useForm, useInputControl } from '@conform-to/react';
2import { CustomSelect } from './some-ui-library';
3
4function Example() {
5  const [form, fields] = useForm();
6  const control = useInputControl(fields.title);
7
8  return (
9    <CustomSelect
10      name={fields.title.name}
11      value={control.value}
12      onChange={(e) => control.change(e.target.value)}
13      onFocus={control.focus}
14      onBlur={control.blur}
15    />
16  );
17}