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 syncs custom UI components with Conform by bridging them to a hidden base control. Use it when integrating components like date pickers, rich selects, or toggles from UI libraries.

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

import { useControl } from '@conform-to/react/future';

const control = useControl(options);

#Options

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

Initial value for the control.

// e.g. Text input
const control = useControl({
  defaultValue: 'my default value',
});

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

// e.g. Hidden fieldset / structured control
const control = useControl({
  defaultValue: {
    start: '2026-01-01',
    end: '2026-01-07',
  },
  parse(payload) {
    return DateRangeSchema.parse(payload);
  },
});

defaultChecked?: boolean

Whether the base control should be checked by default.

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

value?: string

The submitted value of a checkbox or radio control when checked.

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

parse?: (payload: unknown) => Shape

Optional parser for coercing the current payload snapshot into a typed shape.

Use this when the payload needs a stricter shape than the normalized value provides.

const control = useControl({
  defaultValue: { start: '2026-01-01', end: '2026-01-07' },
  parse(payload) {
    return DateRangeSchema.parse(payload);
  },
});

serialize?: (value: Shape) => FormValue

Optional serializer for converting value passed to change() back into form values for the base control.

const control = useControl({
  defaultValue: { start: '2026-01-01', end: '2026-01-07' },
  parse(payload) {
    return {
      start: parseDate(payload.start),
      end: parseDate(payload.end),
    };
  },
  serialize(value) {
    return {
      start: value.start.toString(),
      end: value.end.toString(),
    };
  },
});

onFocus?: () => void

A callback function that is triggered when the base control 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 base control with helpers to emulate native form events.

value: string | undefined

Current string value derived from the control payload.

options: string[] | undefined

Current string array derived from the control payload.

checked: boolean | undefined

Checked state derived from the control payload.

files: File[] | undefined

Current file array derived from the control payload.

defaultValue: DefaultValue | null | undefined

Current default value for the base control.

payload: Payload | null | undefined

Current value derived from the registered base control(s).

For native form controls, this often matches the element's value. For structural controls, this is reconstructed from descendant fields under the registered <fieldset> name and then parsed if parse is provided.

formRef: React.RefObject<HTMLFormElement | null>

Ref object for the form associated with the registered base control.

register(element): void

Registers the base control element. Accepts <input>, <select>, <textarea>, <fieldset>, or a collection of checkbox / radio inputs sharing the same name.

change(value: Value | null): void

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

  • For standard controls, it expects a value that matches the same type as the defaultValue (e.g. string, string[], File[]).
  • For checked controls, it expects a boolean.
  • For custom controls, it expects a value that matches the custom Shape. If you provide serialize, the value is converted back to a form value before updating the base control.

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 base control. Use element.focus() instead if you want to move focus to it.

#Example

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    defaultValue: {
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    defaultValue: {
8      categories: ['tutorial', 'blog'],
9    },
10  });
11  const control = useControl({
12    defaultValue: 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.attachments.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}

Structural field (fieldset)

1import { BaseControl, useControl, useField } from '@conform-to/react/future';
2
3function DateRangeField(props: { name: string }) {
4  const field = useField(props.name);
5  const control = useControl({
6    defaultValue: field.defaultPayload,
7    parse(payload) {
8      return DateRangeSchema.parse(payload);
9    },
10  });
11
12  return (
13    <>
14      <BaseControl
15        type="fieldset"
16        name={field.name}
17        ref={control.register}
18        defaultValue={control.defaultValue}
19      />
20      <CustomDateRangePicker
21        value={control.payload}
22        onChange={(value) => control.change(value)}
23        onBlur={() => control.blur()}
24      />
25    </>
26  );
27}

#Tips

Progressive enhancement

If you care about supporting form submissions before JavaScript loads, set defaultValue, defaultChecked, or value directly on the base control.

// 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.