Integrations

In this guide, we will show you how Conform can works with different form controls, including custom input components.

Native form controls

Native form controls are supported out of the box. There is no need to setup any event handlers on <input />, <select /> or <textarea /> element, as Conform utilizes event delegation and listens to events on the form level instead.

1function Example() {
2  const [form, { title, description, color }] = useForm();
3
4  return (
5    <form {...form.props}>
6      <div>
7        <label>Title</label>
8        <input type="text" name="title" />
9        <div>{title.error}</div>
10      </div>
11      <div>
12        <label>Description</label>
13        <textarea name="description" />
14        <div>{description.error}</div>
15      </div>
16      <div>
17        <label>Color</label>
18        <select name="color">
19          <option>Red</option>
20          <option>Green</option>
21          <option>Blue</option>
22        </select>
23        <div>{color.error}</div>
24      </div>
25      <button>Submit</button>
26    </form>
27  );
28}
29

Custom input component

Integrating Conform with a UI component's library, however, might require integration depending on how it is implemented. For example, an <Input /> component could be just a styled input element. As the user will continue typing on a native input element, there is no additional integration needed.

However, custom control such as <Select /> or <DatePicker /> will likely require users to interact with custom elements instead with no focus / input / blur event dispatched from the native form control element. This is where the useInputEvent hook comes in handy.

Here is an example integrating with react-select:

1import { useForm, useInputEvent } from '@conform-to/react';
2import Select from 'react-select';
3
4function Example() {
5  const [form, { currency }] = useForm();
6  const shadowInputRef = useRef<HTMLInputElement>(null);
7  const control = useInputEvent({
8    ref: shadowInputRef,
9  });
10
11  return (
12    <form {...form.props}>
13      <div>
14        <label>Currency</label>
15        {/*
16          This is a shadow input which will be validated by Conform
17        */}
18        <input
19          ref={shadowInputRef}
20          {...conform.input(currency, {
21            hidden: true,
22          })}
23        />
24        {/*
25          This makes the corresponding events to be dispatched
26          from the element that the `shadowInputRef` is assigned to.
27        */}
28        <Select
29          options={
30            [
31              /*...*/
32            ]
33          }
34          defaultValue={currency.defaultValue ?? ''}
35          onChange={control.change}
36          onFocus={control.focus}
37          onBlur={control.blur}
38        />
39        <div>{currency.error}</div>
40      </div>
41      <button>Submit</button>
42    </form>
43  );
44}
45

To reuse the integration across several forms, you can consider creating a wrapper as well:

1import type { FieldConfig } from '@conform-to/react';
2import { useForm, useInputEvent } from '@conform-to/react';
3import ReactSelect from 'react-select';
4
5function Example() {
6  const [form, { currency }] = useForm();
7
8  return (
9    <form {...form.props}>
10      <div>
11        <label>Currency</label>
12        <Select
13          options={
14            [
15              /*...*/
16            ]
17          }
18          {...currency}
19        />
20        <div>{currency.error}</div>
21      </div>
22      <button>Submit</button>
23    </form>
24  );
25}
26
27// The `FieldConfig` type includes common attributes
28// like id, name, defaultValue, required etc
29interface SelectProps extends FieldConfig<string> {
30  options: Array<{ label: string; value: string }>;
31}
32
33function Select({ options, ...config }: SelectProps) {
34  const shadowInputRef = useRef<HTMLInputElement>(null);
35  const control = useInputEvent({
36    ref: shadowInputRef,
37  });
38
39  return (
40    <>
41      <input
42        ref={shadowInputRef}
43        {...conform.input(config, {
44          hidden: true,
45        })}
46      />
47      <ReactSelect
48        options={options}
49        defaultValue={config.defaultValue}
50        onChange={control.change}
51        onFocus={control.focus}
52        onBlur={control.blur}
53      />
54    </>
55  );
56}
57

If the custom control support manual focus, you can also hook it with the shadow input and let Conform focus on it when there is any error:

1function Select({ options, ...config }: SelectProps) {
2  const shadowInputRef = useRef<HTMLInputElement>(null);
3  const control = useInputEvent({
4    ref: shadowInputRef,
5  });
6  // The type of the ref might be different depends on the UI library
7  const customInputRef = useRef<HTMLInputElement>(null);
8
9  return (
10    <>
11      {/*
12          Conform will focus on the shadow input which will then be "forwarded"
13          to the Select component.
14        */}
15      <input
16        ref={shadowInputRef}
17        {...conform.input(config, {
18          hidden: true,
19        })}
20        onFocus={() => customInputRef.current?.focus()}
21      />
22      <Select
23        innerRef={customInputRef}
24        options={options}
25        defaultValue={config.defaultValue}
26        onChange={control.change}
27        onBlur={control.blur}
28      />
29    </>
30  );
31}
32

The hook also provides support for the reset event if needed:

1function Select({ options, .. }: SelectProps) {
2  const [value, setValue] = useState(config.defaultValue ?? '');
3  const shadowInputRef = useRef<HTMLInputElement>(null);
4  const control = useInputEvent({
5    ref: shadowInputRef,
6    onReset: () => setValue(config.defaultValue ?? ''),
7  });
8
9  return (
10    <>
11      <input
12        ref={shadowInputRef}
13        {...conform.input(config, {
14          hidden: true,
15        })}
16        onChange={(e) => setValue(e.target.value)}
17      />
18      <Select
19        options={options}
20        value={value}
21        onChange={control.change}
22        onBlur={control.blur}
23      />
24    </>
25  );
26}
27

Here you can find more examples: