@conform-to/react

React adapter for conform

useForm

By default, the browser calls the reportValidity() API on the form element when a submission is triggered. This checks the validity of all the fields and reports through the error bubbles.

This hook enhances the form validation behaviour by:

  • Enabling customizing validation logic.
  • Capturing error message and removes the error bubbles.
  • Preparing all properties required to configure the form elements.
1import { useForm } from '@conform-to/react';
2
3function LoginForm() {
4  const [form, { email, password }] = useForm({
5    /**
6     * If the form id is provided, Id for label,
7     * input and error elements will be derived.
8     */
9    id: undefined,
10
11    /**
12     * Define when conform should start validation.
13     * Support "onSubmit", "onChange", "onBlur".
14     *
15     * Default to `onSubmit`.
16     */
17    shouldValidate: 'onSubmit',
18
19    /**
20     * Define when conform should revalidate again.
21     * Support "onSubmit", "onChange", "onBlur".
22     *
23     * Default based on `shouldValidate`
24     */
25    shouldRevalidate: 'onInput',
26
27    /**
28     * An object representing the initial value of the form.
29     */
30    defaultValue: undefined,
31
32    /**
33     * The last submission result from the server
34     */
35    lastSubmission: undefined,
36
37    /**
38     * An object describing the constraint of each field
39     */
40    constraint: undefined,
41
42    /**
43     * Enable native validation before hydation.
44     *
45     * Default to `false`.
46     */
47    fallbackNative: false,
48
49    /**
50     * Accept form submission regardless of the form validity.
51     *
52     * Default to `false`.
53     */
54    noValidate: false,
55
56    /**
57     * A function to be called when the form should be (re)validated.
58     * Only sync validation is supported
59     */
60    onValidate({ form, formData }) {
61      // ...
62    },
63
64    /**
65     * The submit event handler of the form.
66     */
67    onSubmit(event, { formData, submission, action, encType, method }) {
68      // ...
69    },
70  });
71
72  // ...
73}
74
What is `form.props`?

It is a group of properties required to hook into form events. They can also be set explicitly as shown below:

1function RandomForm() {
2  const [form] = useForm();
3
4  return (
5    <form
6      ref={form.props.ref}
7      id={form.props.id}
8      onSubmit={form.props.onSubmit}
9      noValidate={form.props.noValidate}
10    >
11      {/* ... */}
12    </form>
13  );
14}
15
Does it work with custom form component like Remix Form?

Yes! It will fallback to native form submission as long as the submit event is not default prevented.

1import { useFrom } from '@conform-to/react';
2import { Form } from '@remix-run/react';
3
4function LoginForm() {
5  const [form] = useForm();
6
7  return (
8    <Form method="post" action="/login" {...form.props}>
9      {/* ... */}
10    </Form>
11  );
12}
13

useFieldset

This hook enables you to work with nested object by monitoring the state of each nested field and prepraing the config required.

1import { useForm, useFieldset } from '@conform-to/react';
2
3interface Address {
4  street: string;
5  zipcode: string;
6  city: string;
7  country: string;
8}
9
10function Example() {
11  const [form, { address }] = useForm<{ address: Address }>();
12  const { city, zipcode, street, country } = useFieldset(
13    form.ref,
14    address,
15  );
16
17  return (
18    <form {...form.props}>
19      <fieldset>
20        <legned>Address</legend>
21        <input name={street.name} />
22        <div>{street.error}</div>
23        <input name={zipcode.name} />
24        <div>{zipcode.error}</div>
25        <input name={city.name} />
26        <div>{city.error}</div>
27        <input name={country.name} />
28        <div>{country.error}</div>
29      </fieldset>
30      <button>Submit</button>
31    </form>
32  );
33}
34

If you don't have direct access to the form ref, you can also pass a fieldset ref.

1import { type FieldConfig, useFieldset } from '@conform-to/react';
2import { useRef } from 'react';
3
4function Fieldset(config: FieldConfig<Address>) {
5  const ref = useRef<HTMLFieldsetElement>(null);
6  const { city, zipcode, street, country } = useFieldset(ref, config);
7
8  return <fieldset ref={ref}>{/* ... */}</fieldset>;
9}
10
Why does `useFieldset` require a ref object of the form or fieldset?

conform utilises the DOM as its context provider / input registry, which maintains a link between each input / button / fieldset with the form through the form property. The ref object allows it to restrict the scope to form elements associated to the same form only.

1function ExampleForm() {
2  const formRef = useRef();
3  const inputRef = useRef();
4
5  useEffect(() => {
6    // Both statements will log `true`
7    console.log(formRef.current === inputRef.current.form);
8    console.log(formRef.current.elements.namedItem('title') === inputRef.current)
9  }, []);
10
11  return (
12    <form ref={formRef}>
13      <input ref={inputRef} name="title">
14    </form>
15  );
16}
17

useFieldList

This hook enables you to work with array and support the list intent button builder to modify a list. It can also be used with useFieldset for nested list at the same time.

1import { useForm, useFieldList, list } from '@conform-to/react';
2
3/**
4 * Consider the schema as follow:
5 */
6type Schema = {
7  items: string[];
8};
9
10function Example() {
11  const [form, { items }] = useForm<Schema>();
12  const itemsList = useFieldList(form.ref, items);
13
14  return (
15    <fieldset ref={ref}>
16      {itemsList.map((item, index) => (
17        <div key={item.key}>
18          {/* Setup an input per item */}
19          <input name={item.name} />
20
21          {/* Error of each item */}
22          <span>{item.error}</span>
23
24          {/* Setup a delete button (Note: It is `items` not `item`) */}
25          <button {...list.remove(items.name, { index })}>Delete</button>
26        </div>
27      ))}
28
29      {/* Setup a button that can append a new row with optional default value */}
30      <button {...list.insert(items.name, { defaultValue: '' })}>add</button>
31    </fieldset>
32  );
33}
34

useInputEvent

It returns a set of helpers that dispatch corresponding dom event.

1import { useForm, useInputEvent } from '@conform-to/react';
2import { Select, MenuItem } from '@mui/material';
3import { useState, useRef } from 'react';
4
5function MuiForm() {
6  const [form, { category }] = useForm();
7  const [value, setValue] = useState(category.defaultValue ?? '');
8  const baseInputRef = useRef<HTMLInputElement>(null);
9  const customInputRef = useRef<HTMLInputElement>(null);
10  const control = useInputEvent({
11    ref: baseInputRef,
12    // Reset the state on form reset
13    onReset: () => setValue(category.defaultValue ?? ''),
14  });
15
16  return (
17    <form {...form.props}>
18      {/* Render a base input somewhere */}
19      <input
20        ref={baseInputRef}
21        {...conform.input(category, { hidden: true })}
22        onChange={(e) => setValue(e.target.value)}
23        onFocus={() => customInputRef.current?.focus()}
24      />
25
26      {/* MUI Select is a controlled component */}
27      <TextField
28        label="Category"
29        inputRef={customInputRef}
30        value={value}
31        onChange={control.change}
32        onBlur={control.blur}
33        select
34      >
35        <MenuItem value="">Please select</MenuItem>
36        <MenuItem value="a">Category A</MenuItem>
37        <MenuItem value="b">Category B</MenuItem>
38        <MenuItem value="c">Category C</MenuItem>
39      </TextField>
40    </form>
41  );
42}
43

conform

It provides several helpers to:

1// Before
2function Example() {
3  const [form, { title }] = useForm();
4
5  return (
6    <form {...form.props}>
7      <input
8        type="text"
9        name={title.name}
10        form={title.form}
11        defaultValue={title.defaultValue}
12        autoFocus={title.initialError ? true : undefined}
13        required={title.required}
14        minLength={title.minLength}
15        maxLength={title.maxLength}
16        min={title.min}
17        max={title.max}
18        multiple={title.multiple}
19        pattern={title.pattern}
20        aria-invalid={title.error ? true : undefined}
21        aria-describedby={title.error ? `${title.name}-error` : undefined}
22      />
23    </form>
24  );
25}
26
27// After:
28function Example() {
29  const [form, { title, description, category }] = useForm();
30
31  return (
32    <form {...form.props}>
33      <input
34        {...conform.input(title, {
35          type: 'text',
36        })}
37      />
38    </form>
39  );
40}
41

parse

It parses the formData based on the naming convention with the validation result from the resolver.

1import { parse } from '@conform-to/react';
2
3const formData = new FormData();
4const submission = parse(formData, {
5  resolve({ email, password }) {
6    const error: Record<string, string[]> = {};
7
8    if (typeof email !== 'string') {
9      error.email = ['Email is required'];
10    } else if (!/^[^@]+@[^@]+$/.test(email)) {
11      error.email = ['Email is invalid'];
12    }
13
14    if (typeof password !== 'string') {
15      error.password = ['Password is required'];
16    }
17
18    if (error.email || error.password) {
19      return { error };
20    }
21
22    return {
23      value: { email, password },
24    };
25  },
26});
27

validateConstraint

This is a client only API

This enable Constraint Validation with ability to enable custom constraint using data-attribute and customizing error messages. By default, the error message would be the attribute that triggered the error (e.g. required / type / 'minLength' etc).

1import { useForm, validateConstraint } from '@conform-to/react';
2import { Form } from 'react-router-dom';
3
4export default function SignupForm() {
5    const [form, { email, password, confirmPassword }] = useForm({
6        onValidate(context) {
7            // This enables validating each field based on the validity state and custom constraint if defined
8            return validateConstraint(
9              ...context,
10              constraint: {
11                // Define custom constraint
12                match(value, { formData, attributeValue }) {
13                    // Check if the value of the field match the value of another field
14                    return value === formData.get(attributeValue);
15                },
16            });
17        }
18    });
19
20    return (
21        <Form method="post" {...form.props}>
22            <div>
23                <label>Email</label>
24                <input
25                    name="email"
26                    type="email"
27                    required
28                    pattern="[^@]+@[^@]+\\.[^@]+"
29                />
30                {email.error === 'required' ? (
31                    <div>Email is required</div>
32                ) : email.error === 'type' ? (
33                    <div>Email is invalid</div>
34                ) : null}
35            </div>
36            <div>
37                <label>Password</label>
38                <input
39                    name="password"
40                    type="password"
41                    required
42                />
43                {password.error === 'required' ? (
44                    <div>Password is required</div>
45                ) : null}
46            </div>
47            <div>
48                <label>Confirm Password</label>
49                <input
50                    name="confirmPassword"
51                    type="password"
52                    required
53                    data-constraint-match="password"
54                />
55                {confirmPassword.error === 'required' ? (
56                    <div>Confirm Password is required</div>
57                ) : confirmPassword.error === 'match' ? (
58                    <div>Password does not match</div>
59                ) : null}
60            </div>
61            <button>Signup</button>
62        </Form>
63    );
64}
65

list

It provides serveral helpers to configure an intent button for modifying a list.

1import { list } from '@conform-to/react';
2
3function Example() {
4  return (
5    <form>
6      {/*
7        To insert a new row with optional defaultValue at a given index.
8        If no index is given, then the element will be appended at the end of the list.
9      */}
10      <button {...list.insert('name', { index, defaultValue })}>Insert</button>
11
12      {/* To remove a row by index */}
13      <button {...list.remove('name', { index })}>Remove</button>
14
15      {/* To replace a row with another defaultValue */}
16      <button {...list.replace('name', { index, defaultValue })}>
17        Replace
18      </button>
19
20      {/* To reorder a particular row to an another index */}
21      <button {...list.reorder('name', { from, to })}>Reorder</button>
22    </form>
23  );
24}
25

validate

It returns the properties required to configure an intent button for validation.

1import { validate } from '@conform-to/react';
2
3function Example() {
4  return (
5    <form>
6      {/* To validate a single field by name */}
7      <button {...validate('email')}>Validate email</button>
8
9      {/* To validate the whole form */}
10      <button {...validate()}>Validate</button>
11    </form>
12  );
13}
14

requestIntent

It lets you trigger an intent without requiring users to click on a button. It supports both list and validate intent.

1import {
2  useForm,
3  useFieldList,
4  conform,
5  list,
6  requestIntent,
7} from '@conform-to/react';
8import DragAndDrop from 'awesome-dnd-example';
9
10export default function Todos() {
11  const [form, { tasks }] = useForm();
12  const taskList = useFieldList(form.ref, tasks);
13
14  const handleDrop = (from, to) =>
15    requestIntent(form.ref.current, list.reorder({ from, to }));
16
17  return (
18    <form {...form.props}>
19      <DragAndDrop onDrop={handleDrop}>
20        {taskList.map((task, index) => (
21          <div key={task.key}>
22            <input {...conform.input(task)} />
23          </div>
24        ))}
25      </DragAndDrop>
26      <button>Save</button>
27    </form>
28  );
29}
30

isFieldElement

This is an utility for checking if the provided element is a form element (input / select / textarea or button) which also works as a type guard.

1function Example() {
2  return (
3    <form
4      onFocus={(event) => {
5        if (isFieldElement(event.target)) {
6          // event.target is now considered one of the form elements type
7        }
8      }}
9    >
10      {/* ... */}
11    </form>
12  );
13}
14