@conform-to/react
React adapter for conform
API Reference
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.
import { useForm } from '@conform-to/react';
function LoginForm() {
const [form, { email, password }] = useForm({
id: undefined,
shouldValidate: 'onSubmit',
shouldRevalidate: 'onInput',
defaultValue: undefined,
lastSubmission: undefined,
constraint: undefined,
fallbackNative: false,
noValidate: false,
onValidate({ form, formData }) {
},
onSubmit(event, { formData, submission, action, encType, method }) {
},
});
}
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:
function RandomForm() {
const [form] = useForm();
return (
<form
ref={form.props.ref}
id={form.props.id}
onSubmit={form.props.onSubmit}
noValidate={form.props.noValidate}
>
{/* ... */}
</form>
);
}
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.
import { useFrom } from '@conform-to/react';
import { Form } from '@remix-run/react';
function LoginForm() {
const [form] = useForm();
return (
<Form method="post" action="/login" {...form.props}>
{/* ... */}
</Form>
);
}
useFieldset
This hook enables you to work with nested object by monitoring the state of each nested field and prepraing the config required.
import { useForm, useFieldset } from '@conform-to/react';
interface Address {
street: string;
zipcode: string;
city: string;
country: string;
}
function Example() {
const [form, { address }] = useForm<{ address: Address }>();
const { city, zipcode, street, country } = useFieldset(
form.ref,
address,
);
return (
<form {...form.props}>
<fieldset>
<legned>Address</legend>
<input name={street.name} />
<div>{street.error}</div>
<input name={zipcode.name} />
<div>{zipcode.error}</div>
<input name={city.name} />
<div>{city.error}</div>
<input name={country.name} />
<div>{country.error}</div>
</fieldset>
<button>Submit</button>
</form>
);
}
If you don't have direct access to the form ref, you can also pass a fieldset ref.
import { type FieldConfig, useFieldset } from '@conform-to/react';
import { useRef } from 'react';
function Fieldset(config: FieldConfig<Address>) {
const ref = useRef<HTMLFieldsetElement>(null);
const { city, zipcode, street, country } = useFieldset(ref, config);
return <fieldset ref={ref}>{/* ... */}</fieldset>;
}
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.
function ExampleForm() {
const formRef = useRef();
const inputRef = useRef();
useEffect(() => {
console.log(formRef.current === inputRef.current.form);
console.log(formRef.current.elements.namedItem('title') === inputRef.current)
}, []);
return (
<form ref={formRef}>
<input ref={inputRef} name="title">
</form>
);
}
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.
import { useForm, useFieldList, list } from '@conform-to/react';
type Schema = {
items: string[];
};
function Example() {
const [form, { items }] = useForm<Schema>();
const itemsList = useFieldList(form.ref, items);
return (
<fieldset ref={ref}>
{itemsList.map((item, index) => (
<div key={item.key}>
{/* Setup an input per item */}
<input name={item.name} />
{/* Error of each item */}
<span>{item.error}</span>
{/* Setup a delete button (Note: It is `items` not `item`) */}
<button {...list.remove(items.name, { index })}>Delete</button>
</div>
))}
{/* Setup a button that can append a new row with optional default value */}
<button {...list.append(items.name, { defaultValue: '' })}>add</button>
</fieldset>
);
}
useInputEvent
It returns a set of helpers that dispatch corresponding dom event.
import { useForm, useInputEvent } from '@conform-to/react';
import { Select, MenuItem } from '@mui/material';
import { useState, useRef } from 'react';
function MuiForm() {
const [form, { category }] = useForm();
const [value, setValue] = useState(category.defaultValue ?? '');
const baseInputRef = useRef<HTMLInputElement>(null);
const customInputRef = useRef<HTMLInputElement>(null);
const control = useInputEvent({
ref: baseInputRef,
onReset: () => setValue(category.defaultValue ?? ''),
});
return (
<form {...form.props}>
{/* Render a base input somewhere */}
<input
ref={baseInputRef}
{...conform.input(category, { hidden: true })}
onChange={(e) => setValue(e.target.value)}
onFocus={() => customInputRef.current?.focus()}
/>
{/* MUI Select is a controlled component */}
<TextField
label="Category"
inputRef={customInputRef}
value={value}
onChange={control.change}
onBlur={control.blur}
select
>
<MenuItem value="">Please select</MenuItem>
<MenuItem value="a">Category A</MenuItem>
<MenuItem value="b">Category B</MenuItem>
<MenuItem value="c">Category C</MenuItem>
</TextField>
</form>
);
}
conform
It provides several helpers to:
function Example() {
const [form, { title }] = useForm();
return (
<form {...form.props}>
<input
type="text"
name={title.name}
form={title.form}
defaultValue={title.defaultValue}
autoFocus={title.initialError ? true : undefined}
requried={title.required}
minLength={title.minLength}
maxLength={title.maxLength}
min={title.min}
max={title.max}
multiple={title.multiple}
pattern={title.pattern}
aria-invalid={title.error ? true : undefined}
aria-describedby={title.error ? `${title.name}-error` : undefined}
/>
</form>
);
}
function Example() {
const [form, { title, description, category }] = useForm();
return (
<form {...form.props}>
<input
{...conform.input(title, {
type: 'text',
})}
/>
</form>
);
}
parse
It parses the formData based on the naming convention with the validation result from the resolver.
import { parse } from '@conform-to/react';
const formData = new FormData();
const submission = parse(formData, {
resolve({ email, password }) {
const error: Record<string, string[]> = {};
if (typeof email !== 'string') {
error.email = ['Email is required'];
} else if (!/^[^@]+@[^@]+$/.test(email)) {
error.email = ['Email is invalid'];
}
if (typeof password !== 'string') {
error.password = ['Password is required'];
}
if (error.email || error.password) {
return { error };
}
return {
value: { email, password },
};
},
});
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).
import { useForm, validateConstraint } from '@conform-to/react';
import { Form } from 'react-router-dom';
export default function SignupForm() {
const [form, { email, password, confirmPassword }] = useForm({
onValidate(context) {
return validateConstraint(
...context,
constraint: {
match(value, { formData, attributeValue }) {
return value === formData.get(attributeValue);
},
});
}
});
return (
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<input
name="email"
type="email"
required
pattern="[^@]+@[^@]+\\.[^@]+"
/>
{email.error === 'required' ? (
<div>Email is required</div>
) : email.error === 'type' ? (
<div>Email is invalid</div>
) : null}
</div>
<div>
<label>Password</label>
<input
name="password"
type="password"
required
/>
{password.error === 'required' ? (
<div>Password is required</div>
) : null}
</div>
<div>
<label>Confirm Password</label>
<input
name="confirmPassword"
type="password"
required
data-constraint-match="password"
/>
{confirmPassword.error === 'required' ? (
<div>Confirm Password is required</div>
) : confirmPassword.error === 'match' ? (
<div>Password does not match</div>
) : null}
</div>
<button>Signup</button>
</Form>
);
}
list
It provides serveral helpers to configure an intent button for modifying a list.
import { list } from '@conform-to/react';
function Example() {
return (
<form>
{/* To append a new row with optional defaultValue */}
<button {...list.append('name', { defaultValue })}>Append</button>
{/* To prepend a new row with optional defaultValue */}
<button {...list.prepend('name', { defaultValue })}>Prepend</button>
{/* To remove a row by index */}
<button {...list.remove('name', { index })}>Remove</button>
{/* To replace a row with another defaultValue */}
<button {...list.replace('name', { index, defaultValue })}>
Replace
</button>
{/* To reorder a particular row to an another index */}
<button {...list.reorder('name', { from, to })}>Reorder</button>
</form>
);
}
validate
It returns the properties required to configure an intent button for validation.
import { validate } from '@conform-to/react';
function Example() {
return (
<form>
{/* To validate a single field by name */}
<button {...validate('email')}>Validate email</button>
{/* To validate the whole form */}
<button {...validate()}>Validate</button>
</form>
);
}
requestIntent
It lets you trigger an intent without requiring users to click on a button. It supports both list and validate intent.
import {
useForm,
useFieldList,
conform,
list,
requestIntent,
} from '@conform-to/react';
import DragAndDrop from 'awesome-dnd-example';
export default function Todos() {
const [form, { tasks }] = useForm();
const taskList = useFieldList(form.ref, tasks);
const handleDrop = (from, to) =>
requestIntent(form.ref.current, list.reorder({ from, to }));
return (
<form {...form.props}>
<DragAndDrop onDrop={handleDrop}>
{taskList.map((task, index) => (
<div key={task.key}>
<input {...conform.input(task)} />
</div>
))}
</DragAndDrop>
<button>Save</button>
</form>
);
}
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.
function Example() {
return (
<form
onFocus={(event) => {
if (isFieldElement(event.target)) {
// event.target is now considered one of the form elements type
}
}}
>
{/* ... */}
</form>
);
}