useControl
The
useControlhook 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 provideserialize, 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.