@conform-to/react
React adapter for conform
API Reference
useForm
By default, the browser calls the reportValidity() API on the form element when it is submitted. This checks the validity of all the fields in it and reports if there are errors through the bubbles.
This hook enhances the form validation behaviour in 3 parts:
- It lets you hook up custom validation logic into different form events. For example, revalidation will be triggered whenever something changed.
- It provides options for you to decide the best timing to start reporting errors. This could be as earliest as the user start typing, or also as late as the user try submitting the form.
- It exposes the state of each field in the form of data attributes, such as
data-conform-touched
, allowing flexible styling across your form without the need to manipulate the class names.
import { useForm } from '@conform-to/react';
function LoginForm() {
const formProps = useForm({
initialReport: 'onBlur',
fallbackNative: false,
noValidate: false,
validate(form, submitter) {
},
onSubmit(event) {
},
});
return (
<form {...formProps}>
<input type="email" name="email" required />
<input type="password" name="password" required />
<button type="submit">Login</button>
</form>
);
}
What is `formProps`?
It is a group of properties properties required to hook into form events. They can also be set explicitly as shown below:
function RandomForm() {
const formProps = useForm();
return (
<form
ref={formProps.ref}
onSubmit={formProps.onSubmit}
noValidate={formProps.noValidate}
>
{/* ... */}
</form>
);
}
Does it work with custom form component like Remix Form?
Yes! It will fallback to native form submission if the submit event handler is omitted or the event is not default prevented.
import { useFrom } from '@conform-to/react';
import { Form } from '@remix-run/react';
function LoginForm() {
const formProps = useForm();
return (
<Form method="post" action="/login" {...formProps}>
{/* ... */}
</Form>
);
}
Is the `validate` function required?
The validate
function is not required if the validation logic can be fully covered by the native constraints, e.g. required / min / pattern etc.
import { useForm, useFieldset } from '@conform-to/react';
function LoginForm() {
const formProps = useForm();
const { email, password } = useFieldset(formProps.ref);
return (
<form {...formProps}>
<label>
<input type="email" name="email" required />
{email.error}
</label>
<label>
<input type="password" name="password" required />
{password.error}
</label>
<button type="submit">Login</button>
</form>
);
}
useFieldset
This hook can be used to monitor the state of each field and help fields configuration. It lets you:
- Capturing errors at the form/fieldset level, removing the need to setup invalid handler on each field.
- Defining config in one central place. e.g. name, default value and constraint, then distributing it to each field using the conform helpers.
import { useForm, useFieldset } from '@conform-to/react';
type Book = {
name: string;
isbn: string;
};
function BookFieldset() {
const formProps = useForm();
const { name, isbn } = useFieldset<Book>(
formProps.ref,
{
name: 'book',
defaultValue: {
isbn: '0340013818',
},
initialError: {
isbn: 'Invalid ISBN',
},
constraint: {
isbn: {
required: true,
pattern: '[0-9]{10,13}',
},
},
form: 'remote-form-id',
},
);
console.log(isbn.error);
console.log(isbn.config.name);
console.log(isbn.config.defaultValue);
console.log(isbn.config.initialError);
console.log(isbn.config.form);
console.log(isbn.config.required);
console.log(isbn.config.pattern);
return <form {...formProps}>{/* ... */}</form>;
}
If you don't have direct access to the form ref, you can also pass a fieldset ref.
import { useFieldset } from '@conform-to/react';
import { useRef } from 'react';
function Fieldset() {
const ref = useRef();
const fieldset = useFieldset(ref);
return <fieldset ref={ref}>{/* ... */}</fieldset>;
}
Is it required to provide the FieldsetConfig to `useFieldset`?
No. The only thing required is the ref object. All the config is optional. You can always pass them to each fields manually.
import { useForm, useFieldset } from '@conform-to/react';
function SubscriptionForm() {
const formProps = useForm();
const { email } = useFieldset(formProps.ref);
return (
<form {...formProps}>
<input
type="email"
name={email.config.name}
defaultValue="support@conform.dev"
required
/>
</form>
);
}
Why does `useFieldset` require a ref object of the form or fieldset?
Unlike most of the form validation library out there, conform use the DOM as its context provider. As the dom maintains a link between each input / button / fieldset with the form through the form property of these elements. The ref object allows us restricting the scope to 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
It returns a list of key and config, with a group of helpers configuring buttons for list manipulation
import { useFieldset, useFieldList } from '@conform-to/react';
import { useRef } from 'react';
type Book = {
name: string;
isbn: string;
};
type Collection = {
books: Book[];
};
function CollectionFieldset() {
const ref = useRef();
const { books } = useFieldset<Collection>(ref);
const [bookList, control] = useFieldList(ref, books.config);
return (
<fieldset ref={ref}>
{bookList.map((book, index) => (
<div key={book.key}>
{/* To setup the fields */}
<input
name={`${book.config.name}.name`}
defaultValue={book.config.defaultValue.name}
/>
<input
name={`${book.config.name}.isbn`}
defaultValue={book.config.defaultValue.isbn}
/>
{/* To setup a delete button */}
<button {...control.remove({ index })}>Delete</button>
</div>
))}
{/* To setup a button that can append a new row with optional default value */}
<button {...control.append({ defaultValue: { name: '', isbn: '' } })}>
add
</button>
</fieldset>
);
}
This hook can also be used in combination with useFieldset
to distribute the config:
import { useForm, useFieldset, useFieldList } from '@conform-to/react';
import { useRef } from 'react';
function CollectionFieldset() {
const ref = useRef();
const { books } = useFieldset<Collection>(ref);
const [bookList, control] = useFieldList(ref, books.config);
return (
<fieldset ref={ref}>
{bookList.map((book, index) => (
<div key={book.key}>
{/* `book.config` is a FieldConfig object similar to `books` */}
<BookFieldset {...book.config} />
{/* To setup a delete button */}
<button {...control.remove({ index })}>Delete</button>
</div>
))}
{/* To setup a button that can append a new row */}
<button {...control.append()}>add</button>
</fieldset>
);
}
function BookFieldset({ name, form, defaultValue, error }) {
const ref = useRef();
const { name, isbn } = useFieldset(ref, {
name,
form,
defaultValue,
error,
});
return <fieldset ref={ref}>{/* ... */}</fieldset>;
}
What can I do with `controls`?
<button {...controls.append({ defaultValue })}>Append</button>;
<button {...controls.prepend({ defaultValue })}>Prepend</button>;
<button {...controls.remove({ index })}>Remove</button>;
<button {...controls.replace({ index, defaultValue })}>Replace</button>;
<button {...controls.reorder({ from, to })}>Reorder</button>;
useControlledInput
It returns the properties required to configure a shadow input for validation. This is particular useful when integrating dropdown and datepicker whichs introduces custom input mode.
import { useFieldset, useControlledInput } from '@conform-to/react';
import { Select, MenuItem } from '@mui/material';
import { useRef } from 'react';
function MuiForm() {
const ref = useRef();
const { category } = useFieldset(schema);
const [inputProps, control] = useControlledInput(category.config);
return (
<fieldset ref={ref}>
{/* Render a shadow input somewhere */}
<input {...inputProps} />
{/* MUI Select is a controlled component */}
<Select
label="Category"
inputRef={control.ref}
value={control.value}
onChange={control.onChange}
onBlur={control.onBlur}
inputProps={{
onInvalid: control.onInvalid
}}
>
<MenuItem value="">Please select</MenuItem>
<MenuItem value="a">Category A</MenuItem>
<MenuItem value="b">Category B</MenuItem>
<MenuItem value="c">Category C</MenuItem>
</TextField>
</fieldset>
)
}
createValidate
This help you configure a validate function to check the validity of each fields and setup custom messages using the Constraint Validation APIs.
import { useForm, createValidate } from '@conform-to/react';
export default function SignupForm() {
const formProps = useForm({
validate: createValidate((field, formData) => {
switch (field.name) {
case 'email':
if (field.validity.valueMissing) {
field.setCustomValidity('Email is required');
} else if (field.validity.typeMismatch) {
field.setCustomValidity('Please enter a valid email');
} else {
field.setCustomValidity('');
}
break;
case 'password':
if (field.validity.valueMissing) {
field.setCustomValidity('Password is required');
} else if (field.validity.tooShort) {
field.setCustomValidity(
'The password should be at least 10 characters long',
);
} else {
field.setCustomValidity('');
}
break;
case 'confirm-password': {
if (field.validity.valueMissing) {
field.setCustomValidity('Confirm Password is required');
} else if (field.value !== formData.get('password')) {
field.setCustomValidity('The password does not match');
} else {
field.setCustomValidity('');
}
break;
}
}
}),
});
return <form {...formProps}>{/* ... */}</form>;
}
conform
It provides several helpers to configure a native input field quickly:
import { useFieldset, conform } from '@conform-to/react';
import { useRef } from 'react';
function RandomForm() {
const ref = useRef();
const { category } = useFieldset(ref);
return (
<fieldset ref={ref}>
<input {...conform.input(category.config, { type: 'text' })} />
<textarea {...conform.textarea(category.config)} />
<select {...conform.select(category.config)}>{/* ... */}</select>
</fieldset>
);
}
This is equivalent to:
function RandomForm() {
const ref = useRef();
const { category } = useFieldset(ref);
return (
<fieldset ref={ref}>
<input
type="text"
name={category.config.name}
form={category.config.form}
defaultValue={category.config.defaultValue}
requried={category.config.required}
minLength={category.config.minLength}
maxLength={category.config.maxLength}
min={category.config.min}
max={category.config.max}
multiple={category.config.multiple}
pattern={category.config.pattern}
>
<textarea
name={category.config.name}
form={category.config.form}
defaultValue={category.config.defaultValue}
requried={category.config.required}
minLength={category.config.minLength}
maxLength={category.config.maxLength}
/>
<select
name={category.config.name}
form={category.config.form}
defaultValue={category.config.defaultValue}
requried={category.config.required}
multiple={category.config.multiple}
>
{/* ... */}
</select>
</fieldset>
);
}