@conform-to/react
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:
- Minimize the boilerplate when configuring a form control
- Derive aria attributes for accessibility
- Helps focus management
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