Future APIs / useControl

useControl

The useControl hook is part of Conform’s future export. These APIs are experimental and may change in minor versions. Learn more

A React hook that lets you sync the state of an input and dispatch native form events from it. This is useful when emulating native input behavior — typically by rendering a hidden base input and syncing it with a custom input.

For details on when you need this hook, see the UI Libraries Integration Guide.

const control = useControl(options);

#Options

defaultValue?: string | string[] | File | File[]

The initial value of the base input. It will be used to set the value when the input is first registered.

// e.g. Text input
const control = useControl({
  defaultValue: 'my default value',
});
// e.g. Multi-select
const control = useControl({
  defaultValue: ['option-1', 'option-3'],
});

defaultChecked?: boolean

Whether the base input should be checked by default. It will be applied when the input is first registered.

const control = useControl({
  defaultChecked: true,
});

value?: string

The value of a checkbox or radio input when checked. This sets the value attribute of the base input.

const control = useControl({
  defaultChecked: true,
  value: 'option-value',
});

onFocus?: () => void

A callback function that is triggered when the base input is focused. Use this to delegate focus to a custom input.

const control = useControl({
  onFocus() {
    controlInputRef.current?.focus();
  },
});

#Returns

A control object. This gives you access to the state of the input with helpers to emulate native form events.

value: string | undefined

Current value of the base input. Undefined if the registered input is a multi-select, file input, or checkbox group.

options: string[] | undefined

Selected options of the base input. Defined only when the registered input is a multi-select or checkbox group.

checked: boolean | undefined

Checked state of the base input. Defined only when the registered input is a single checkbox or radio input.

files: File[] | undefined

Selected files of the base input. Defined only when the registered input is a file input.

register: (element: HTMLInputElement | HTMLSelectElement | HTMLTextareaElement | Array<HTMLInputElement>) => void

Registers the base input element(s). Accepts a single input or an array for groups.

change(value: string | string[] | File | File[] | FileList | boolean): void

Programmatically updates the input value and emits both change and input events.

blur(): void

Emits blur and focusout events. Does not actually move focus.

focus(): void

Emits focus and focusin events. This does not move the actual keyboard focus to the input. Use element.focus() instead if you want to move focus to the input.

#Example Usage

Checkbox / Switch

1import { useControl } from '@conform-to/react/future';
2import { useForm } from '@conform-to/react';
3import { Checkbox } from './custom-checkbox-component';
4
5function Example() {
6  const [form, fields] = useForm({
7    defaultValues: {
8      newsletter: true,
9    },
10  });
11  const control = useControl({
12    defaultChecked: fields.newsletter.defaultChecked,
13  });
14
15  return (
16    <>
17      <input
18        type="checkbox"
19        name={fields.newsletter.name}
20        ref={control.register}
21        hidden
22      />
23      <Checkbox
24        checked={control.checked}
25        onChange={(checked) => control.change(checked)}
26        onBlur={() => control.blur()}
27      >
28        Subscribe to newsletter
29      </Checkbox>
30    </>
31  );
32}

Multi-select

1import { useControl } from '@conform-to/react/future';
2import { useForm } from '@conform-to/react';
3import { Select, Option } from './custom-select-component';
4
5function Example() {
6  const [form, fields] = useForm({
7    defaultValues: {
8      categories: ['tutorial', 'blog'],
9    },
10  });
11  const control = useControl({
12    defualtValue: fields.categories.defaultOptions,
13  });
14
15  return (
16    <>
17      <select
18        name={fields.categories.name}
19        ref={control.register}
20        multiple
21        hidden
22      />
23      <Select
24        value={control.options}
25        onChange={(options) => control.change(options)}
26        onBlur={() => control.blur()}
27      >
28        <Option value="blog">Blog</Option>
29        <Option value="tutorial">Tutorial</Option>
30        <Option value="guide">Guide</Option>
31      </Select>
32    </>
33  );
34}

File input

1import { useControl } from '@conform-to/react/future';
2import { useForm } from '@conform-to/react';
3import { DropZone } from './custom-file-input-component';
4function Example() {
5  const [form, fields] = useForm();
6  const control = useControl();
7
8  return (
9    <>
10      <input
11        type="file"
12        name={fields.attachements.name}
13        ref={control.register}
14        hidden
15      />
16      <DropZone
17        files={control.files}
18        onChange={(files) => control.change(files)}
19        onBlur={() => control.blur()}
20      />
21    </>
22  );
23}

#Tips

Progressive enhancement

If you care about supporting form submissions before JavaScript loads, set defaultValue, defaultChecked, or value directly on the base input. This ensures correct values are included in the form submission. Otherwise, useControl will handle it once the app is hydrated.

// Input
<input
  type="email"
  name={fields.email.name}
  defaultValue={fields.email.defaultValue}
  ref={control.register}
  hidden
/>

// Select
<select
  name={fields.categories.name}
  defaultValue={fields.categories.defaultOptions}
  ref={control.register}
  hidden
>
  <option value=""></option>
  {fields.categories.defaultOptions.map(option => (
    <option key={option} value={option}>
      {option}
    </option>
  ))}
</select>

// Textarea
<textarea
  name={fields.description.name}
  defaultValue={fields.description.defaultValue}
  ref={control.register}
  hidden
/>

Checkbox / Radio groups

You can register multiple checkbox or radio inputs as a group by passing an array of elements to register(). This is useful when the setup renders a set of native inputs that you want to re-use without re-implementing the group logic:

<CustomCheckboxGroup
  ref={(el) => control.register(el?.querySelectorAll('input'))}>
  value={control.options}
  onChange={(options) => control.change(options)}
  onBlur={() => control.blur()}
/>

If you don’t need to re-use the existing native inputs, you can always represent the group with a single hidden multi-select or text input. For complete examples, see the checkbox and radio group implementations in the React Aria example.